Skip to content

Accessibility

Pyzed apps can be used from the keyboard and respect the motion preference of the system. Screen readers are not supported yet: see Screen readers for why and what comes next.

Tab moves the keyboard focus to the next field of the window and Shift+Tab to the previous one, in the order they appear in view(). After the last field it goes back to the first. Fields are text_inputs, text_editors and code_editors (and the components built on them, like ui.input or ui.search), dropdowns (pz.pick_list and ui.select) and the controls wrapped in pz.focusable: the library’s ui.switch, ui.checkbox, ui.slider, ui.segmented and ui.radio_group. Each window cycles through its own fields only.

A code_editor with tab_size keeps Tab to indent, like a code editor. Editors without it pass the key on, so Tab leaves them.

on_key sees the key first ("tab" or "shift-tab"). If it returns something (a message or a Command), the app handled it and the focus stays; returning None lets the focus move. Turn the behavior off for a whole app or window with tab_focus = False:

class App(pz.App):
tab_focus = False # Tab does nothing unless on_key handles it
def on_key(self, combo):
if combo == "tab" and self.completion_open:
return self.accept_completion()

Command.focus_next() and Command.focus_previous() do the same from code, and Command.focus(id) jumps to a specific field (pass id= to the control).

A click or Tab gives a control the focus, and it keeps it after you use it, like a text field. A ring in the theme’s focus color shows around it once you use the keys (a click alone draws none, like a browser’s :focus-visible); a dropdown lights its border instead.

Control Keys while it has the focus
ui.select, pz.pick_list ↓/↑ (and →/←) pick the next or previous option without opening the menu, Home/End the first and last, letters jump to the option that starts with them. Enter, Space, Alt+↓ or F4 open the menu; there ↑/↓ move, Enter picks and Esc closes.
ui.switch Space or Enter flip it.
ui.checkbox Space toggles it (a mixed one becomes checked).
ui.segmented, ui.radio_group The arrows pick the previous or next choice, skipping disabled ones; Home/End the first and last.
ui.slider The arrows move one step, with Shift (or Page Up/Page Down) ten; Home/End go to min/max.
ui.number_field ↑/↓ step it (ten with Shift), Home/End go to min/max.

The keys a focused control uses do not reach the app’s on_key, so ↓ on a dropdown does not also scroll a list or move a selection elsewhere. Every other key (shortcuts like Ctrl+S) still does.

Your own controls get the same with pz.focusable(content, on_key, keys=[...]): while it has the focus, the combos in keys call on_key(combo) and nothing else sees them.

def stars(self):
t = self.theme
row = pz.row([pz.icon("star", color=t.warning if i < self.rating else t.text_muted)
for i in range(5)], spacing=2)
def rate(combo):
self.rating = max(0, min(5, self.rating + (1 if combo == "right" else -1)))
return pz.focusable(row, rate, keys=["left", "right"], id="rating",
ring_color=self.theme.border_focused)

When the system asks apps to reduce motion, Pyzed turns its animations off: every animate(), transition(), presence(), window transition and animated scroll shows its final state at once. The setting is read when the app starts:

System Setting
macOS System Settings → Accessibility → Display → Reduce motion
Windows Settings → Accessibility → Visual effects → Animation effects (off)
Linux (GNOME) Settings → Accessibility → Reduce animation (enable-animations)
Linux (KDE) System Settings → Animation speed set to Instant

pz.system_reduced_motion() returns what the system says (None if it can’t tell). Your app can still decide:

pz.set_reduced_motion(True) # always off, e.g. from your app's settings
pz.set_reduced_motion(False) # always on
pz.set_reduced_motion(None) # follow the system again (the default)

The PYZED_REDUCED_MOTION environment variable (1 or 0) overrides both. Screenshots always show the resting state. See Animation.

Pyzed draws its interface with Iced, which paints every widget itself on the GPU instead of using the native controls of the system. Screen readers (VoiceOver, Narrator, NVDA, Orca) need an accessibility tree that describes those widgets, and Iced 0.14 does not produce one. The usual way to provide it in Rust is AccessKit, and we evaluated adding it to Pyzed for 1.1:

  • Iced 0.14 has no AccessKit support. Neither iced nor iced_winit depend on it, and winit 0.30 leaves accessibility to adapters such as accesskit_winit.
  • The window is owned by Iced. accesskit_winit must be created before the window is shown and receive every window event; Iced’s event loop gives no place to do either. The platform adapters (accesskit_windows and accesskit_macos subclass the native window, accesskit_unix talks to AT-SPI) could be attached from the raw window handle, but that is three separate integrations, each tested with a real screen reader.
  • Widgets don’t describe themselves. Iced’s widget operations report text, text fields, scrollables and focus, but no roles (button, checkbox, slider), states or actions (“press”), so Pyzed would have to build the tree from its own widget descriptions and keep it in sync with the layout every frame.

That makes it a project of its own, not something to ship half done in a minor release. Accessibility is on Iced’s roadmap and forks of Iced already ship AccessKit, so the plan is:

  1. Follow Iced upstream and adopt its accessibility support when it lands; Pyzed’s widgets map one to one to Iced’s, so they would gain it with little extra work.
  2. Meanwhile, make the keyboard path complete (focus for buttons and other controls when Iced allows it) and keep labels in the UI text rather than only in icons.

If screen reader support matters for your app, please say so in the issues: it helps decide priorities.