Tucano v0.37.2

Keyboard and accessibility

Everything works without a mouse, and the keys are those of the native element and the ARIA APG — anyone who already knows how to use a <select> has nothing new to learn. This page gathers in one place what each component does with the keyboard and what it announces to screen reader users. The accessible labels the library writes on its own are in Portuguese by default, and Tucano.setTexts() replaces them; they are quoted below with their meaning.

Tab doesn't open panels

Reaching a field with Tab never opens a calendar, list or color picker. Opening is always explicit: ↓, a click, and in the select also Enter and Space.

Opening on focus looked convenient and was the source of two bugs. Panels piled up while tabbing — anyone going through a form to the save button got a calendar in their face at every field, with the panel covering the next field. And in the color field the panel covered the very field of someone who wanted to type the hex. It's the rule of the native <select>, and it's the ARIA APG's.

ComponentOpens with
Date picker↓, click; Space when the field is empty
Select↓, ↑, Enter, Space, click
Color picker↓ in the field or on the swatch; Enter and Space on the swatch, which is a <button>
Dropdown menu↓, ↑, Enter, Space or a click on the trigger

In the date field, Enter belongs to the form

The date picker is a text field inside a <form>, and Enter in a text field submits the form — which is what anyone expects after typing the date. Hijacking the key would silently break submission in every existing form, so there it's Space that opens. And only when the field is empty: in time mode you type 07/09/2026 14:30, and the space is part of the typing. With the panel open, Enter confirms what was typed.

Any panel that opened closes when focus leaves it, so tabbing through the page never leaves menus open behind you.

Date picker

KeyWhereAction
↓Field, closedOpens the calendar
SpaceEmpty field, closedOpens the calendar
EnterField, openConfirms the typed date and closes; when closed, submits the form
← →DayPrevious and next day
↑ ↓DaySame day in the previous and next week
PageUp PageDownDayPrevious and next month; with Shift, year
Home EndDayStart and end of the week, respecting the locale's first day
Enter SpaceDayPicks the day
↑ ↓Time columnPrevious and next value in the column
Home EndTime columnFirst and last value in the column
Enter SpaceTime columnPicks the value
TabPanelCycles inside the panel, without escaping to the page; each time column is a single stop
EscPanelCloses, discards what wasn't confirmed and returns focus to the field

The field is a combobox with aria-haspopup="dialog" and aria-expanded, and gets aria-controls pointing to the panel while it's open. The panel is role="dialog" labeled "Selecionar data" (select date) or "Selecionar período" (select period); each month is a role="grid" with the month's name, each week a row and each day a gridcell with aria-selected and the full label — "Saturday, September 12, 2026". A day outside the limits has aria-disabled: it receives focus but can't be picked. Changing months is announced by an aria-live="polite" region that stays fixed in the panel. Each hour, minute and second column is a group of buttons with a single Tab stop, and the chosen value has aria-pressed. Closing by clicking outside doesn't return focus to the field, so it isn't stolen from where the person clicked.

Select

KeyAction
↓ ↑ Enter SpaceWhen closed, opens the panel
↑ ↓When open, moves between options, skipping disabled ones and wrapping around
Home EndFirst and last option, skipping disabled ones
EnterPicks the active option; in multiple mode, checks or unchecks it
LettersWith search on, they filter the list ignoring accents. Search turns on by itself from 6 options, or with data-search="true"
BackspaceIn multiple mode, with an empty search, removes the last tag
Backspace DeleteIn single mode, with an empty search, clears the value, like the X — without the X (clearable: false), it does not clear
EscCloses and returns focus to the field

With the native <select> disabled, the field leaves the Tab order, is dimmed, doesn't open, doesn't accept text and hides the clear X and the tag X. An option inside <optgroup disabled> also counts as disabled.

The control is a combobox with aria-haspopup="listbox", aria-expanded and aria-controls pointing to the list while it's open — when closed, the list leaves the DOM and the attribute goes with it. The list is a listbox, with aria-multiselectable in multiple mode; each option is an option with aria-selected and aria-disabled. The active option reaches the screen reader through aria-activedescendant, without taking focus away from the search field. Each tag's X is called "Remover" (remove) followed by the option's name. The native <select> stays in the form with aria-hidden, so it isn't read twice.

Color picker

KeyWhereAction
↓Field or swatchOpens the panel with focus on the color area
Enter SpaceSwatchOpens the panel with focus on the color area, and closes it
← →Color areaSaturation, in steps of 2%; with Shift, steps of 10
↑ ↓Color areaBrightness, in the same steps
← ↓ / → ↑Hue and opacity slidersDecreases and increases by 1; with Shift, by 10
Home EndHue and opacity slidersMinimum and maximum
EscPanelCloses and returns focus to the swatch

The swatch is a <button> "Escolher cor" (choose color) with aria-haspopup="dialog" and aria-expanded; the panel is a role="dialog" "Seletor de cor" (color picker). The sliders are role="slider" with aria-valuenow, and each palette color is a button labeled with its own value. The text field is still the <input> where you type the hex.

KeyWhereAction
↓TriggerOpens and moves to the first item
↑TriggerOpens and moves to the last item
Enter SpaceTriggerOpens and closes
↑ ↓MenuPrevious and next item, wrapping around and skipping disabled ones
Home EndMenuFirst and last item
EnterItemActivates it; an item without href is a <button> and also accepts Space
EscMenuCloses and returns focus to the trigger
TabMenuCloses and moves on to the next element on the page

The trigger gets aria-haspopup="menu" and aria-expanded; the panel is role="menu", each item a menuitem, the separator a separator and the disabled item aria-disabled. Items have tabindex="-1" on purpose: inside a menu the arrows navigate, and tabbable items would make Tab leave the menu one item at a time.

Tabs

KeyAction
TabEnters the open tab and leaves the list straight to the panel — the list is a single stop
← →Previous and next tab, wrapping around and skipping disabled ones
Home EndFirst and last tab
Enter SpaceWith data-manual="true", switches the panel; without it, the arrow already switches

The script adds tablist, tab and tabpanel, links each tab to its panel through aria-controls and aria-labelledby, and keeps aria-selected and tabindex up to date. Manual mode exists for panels that load through HTMX: with automatic activation, crossing four tabs with the arrow would fire four requests.

Modal and drawer

KeyAction
Tab Shift+TabCycle inside the dialog, without escaping to the page
EscCloses with animation and returns focus to whatever opened it — unless closable: false or data-closable="false"

Both are a <dialog> opened with showModal(), and that's where the accessibility comes from: the page behind becomes inert, focus is trapped and returns to the trigger on close, all done by the browser and not by our JavaScript. The native Escape would close it instantly, without animation; the library intercepts cancel to close the same way the X does. With a title, the dialog is labeled by it through aria-labelledby, and the X is called "Fechar" (close). In Tucano.confirm(), closing with Esc is a refusal: the promise resolves to false.

Tooltip

Key or gestureAction
Tab to the elementShows the tip right away, without the pointer delay
Tab awayHides the tip
EscHides it, even with the pointer resting on top
TapOn a touchscreen, tapping opens and tapping again closes

The bubble is role="tooltip" and the element points to it through aria-describedby, so the screen reader reads the tip along with the button's name. An element that doesn't receive focus — a <span>, for example — gets tabindex="0", otherwise keyboard users would never see the tip. Esc with the pointer on top is what WCAG 1.4.13 asks for, for people who use screen magnification and need to dismiss a tip that covers the content.

Table

KeyAction
TabGoes through the sortable headers, the selection checkboxes and the row buttons
EnterOn a header, sorts — in server mode, follows the link
SpaceChecks the checkbox; in client mode, also sorts through the header button

The sorted header has aria-sort="ascending" or "descending" and the others "none". In server mode the trigger is an <a>, announced as a link; in client mode, a <button>. The header checkbox is "Selecionar todas as linhas desta página" (select all rows on this page), each row's is "Selecionar linha" (select row), and the mixed state is the native indeterminate.

The other components

These have no keyboard handling of their own: the native element underneath is what responds. Even so, each one has something to tell the screen reader.

ComponentKeyboardWhat it announces
AccordionEnter and Space on the <summary>, from the browserOpen and closed, through <details>; the arrow is aria-hidden
UploadEnter and Space on the drop zone open the file dialogThe zone is role="button" described by the hint about types and size; a rejected file shows up in a role="alert" notice
Mask with eyeEnter and Space on the eye button"Mostrar" (show) and "Ocultar" (hide), with aria-pressed; with data-validate, a field that fails gets aria-invalid
Text editorCtrl/⌘ + B, I, U, K; Tab in a table moves between cells and, in the last one, creates a rowThe area is a multiline role="textbox"; the bars are role="toolbar" and the formatting buttons have aria-pressed
ToastThe timer pauses when something inside it receives focusTwo live regions created before the first message: polite for normal ones and assertive for errors; each toast is status or alert
PaginationNative links<nav> "Paginação" (pagination), aria-current="page" on the current one; a disabled end is a <span>, out of the Tab path
Checkbox, radio and switchThose of the native <input>The browser's; the switch needs role="switch" in the template, because CSS can't add a role
Button groupThose of the native <input>: the radio group is a single Tab stop, and the arrow keys move through itThe group needs a name — role="group" with aria-label, or <fieldset> with <legend>; the <input> stays invisible, but focusable and announced

What applies to all of them

The decisions that repeat from component to component.

RuleWhy
The native element stays in the formName, required state, mixed state and the screen reader announcement are still the browser's
A field with an error is aria-invalid="true", not a classIt's the attribute the screen reader announces, and Django 5 writes it on its own. In components that replace the field with their own control, the attribute stays on the native element and the CSS reaches the control
A panel that opened closes when focus leavesWithout this, tabbing through the page left a trail of open panels
Esc closes and returns focus to whatever opened itClosing and dropping focus at the start of the page would make keyboard users start over from the top
Drawings get aria-hiddenArrows, icons and the pagination ellipsis are not read aloud
prefers-reduced-motion turns off animationsApplies to panels, dialogs, toasts, tabs, accordion, table and buttons