Design system/Components
Picker
There's no native <select> in Flowra. The browser draws its list with the system font, Latin digits, and no dark mode, so every selection comes from one designed list that's a menu on desktop and a bottom sheet on phones.
In codecomponents/text-field.tsxcomponents/select-menu.tsxcomponents/floating.tsx
Three shapes, one list
DropdownField is for forms: a full floating field with a label and icon. SelectField is the same thing with a <select> interface, meaning <option> children and an event-based onChange, so a form written for a native select can switch without a rewrite; the value is sent in a hidden input with the same name. SelectMenu is a small chip, for places where a full field is too big, like the time at the bottom of the calendar.
Selection chip
SelectMenu is 30px tall, its digits are tabular, and its list is the same OptionList.
Floating panel
The list isn't inside the field; Floating moves it to body and positions it from the field with position: fixed: below if there's room, above if there's more room there, always within the screen, and it follows the field as you scroll. That way no dialog or card clips half the list with its own scroll.
On phones, the same content rises as a bottom sheet, with the field's label as its heading.
Behavior
- When it opens, the selected option is visible and highlighted.
- Up and down arrows, Home and End, Enter or Space to select, Escape to close. Type a few letters in a row (within 600 milliseconds) to jump to the next option that starts with them.
- Escape closes only the topmost panel and doesn't reach the dialog beneath it.
- Groups sit under a heading; a disabled option is visible but can't be selected.
- searchable adds a filter box above the list; the filter understands Persian digits too.
- addNewLabel and onAddNew add an "Add new option" row at the bottom of the list.
- After a selection, focus returns to the field.
Rules
Do
A long list, searchable.
Don't
A 50-item list with no filter.
Do
Three or four short options that should all be visible? Pills.
Don't
A dropdown for two options.
Do
A value that's both displayed and compared gets tr() on both sides.
Don't
Comparing a translated label with a Persian value.
Code
<DropdownField
label={tr("مسئول")}
icon={paths.user}
value={owner}
options={members.map((m) => ({ value: m.id, label: m.name }))}
onChange={setOwner}
searchable
/>
<SelectMenu ariaLabel={tr("ساعت")} value={hour} options={HOURS} onChange={setHour} />Sizes
58px · 16px / 500The same floating fieldradius 14px · padding 6pxWidth = fieldmax-height 248px.option-rows; sheet 52vhpadding 10px 12px · radius 9px · 14px / 500Phone min 46px · 14.5px; sheet 48px10.5px / 700 · padding 8px 12px 4px30px · min-width 46px · radius 10px · 13px / 600Time: min-width 86px15px · rotate 180° · 0.22s springChip 12px · 0.2s6px8px from the screen edge; at least 120px of room44px · 16pxRow states
background: var(--muted)data-active · --muted--accent-text on --accent-soft · check 14pxinset 0 0 0 1px var(--accent)opacity 0.45 · cursor: default2px solid var(--accent) · offset 2pxBorder --accent · 0 0 0 3px var(--accent-soft)- The selected option is --accent-text on soft orange: 4.69 to 1 in light and 5.31 in dark, AA in both; it also has a checkmark, so selection isn't shown by color alone.
Keyboard and roles
Enter · Space · ↑ · ↓↑ ↓ · PageUp/PageDown = 8Home / EndWhen focus is on the listEnter · SpaceSpace while focus is on the listtypeahead · 600msWhen focus is on the listEscape · click outsidearia-haspopup="listbox" · aria-expandedrole="listbox" · aria-activedescendantrole="option" · aria-selected- On open, focus moves to the list, or to the filter box if it's searchable. On phones focus stays on the list so the keyboard doesn't cover the sheet.
- In the filter box, Home and End move the cursor, not the row.
- If the filter finds nothing, it shows “Nothing found” (12.5px, centered).
Do
SelectField when the form is submitted with name.
Don't
A native <select> next to floating fields.
Do
In right-to-left, the panel hangs from the field's right edge.
Don't
A fixed left for the panel; in Persian it sticks out from under the field.
Related pages