Kanzo UI
Forms

Controls

Every form control in the library, grouped by what it captures. A table index — start here to find the right one.

Every control goes inside a Field the same way, so there is no per-control API to learn. This page is the index: find the control you need by what it captures, then follow the link for its own page.

The naming rule that explains half of these. A kebab-case file is the vendored primitive — the compound with all its parts exposed. A PascalCase one is our pre-assembled convenience, built on it: FacetFilter imports listbox and popover, FieldArray imports field. They are not competitors; one is made of the other. Default to the PascalCase one, and drop to the primitive when you need to rearrange the parts. The rule in full is on Philosophy.

One height per size

A single-line control is exactly as tall as its size token, and every one of them is. sm, md and lg are 7, 8 and 9 times --size-field, so 28, 32 and 36px at the default density and 24.5, 28 and 31.5px at compact — a text input, a number input, a native select, a select, a combobox, a tags input, a date picker and its time field, a language picker, a password input, a pin cell, a segment group, a toggle and a button all share it. Put any of them beside any other, in a row or a column, and their edges line up.

The height is written once, in packages/ui/src/lib/control-size.ts: a controlSizes fragment that assigns --size per token and a controlHeight that reads it. Every recipe spreads the first into its size variant and puts the second on its base, and control-height.test.ts fails when one stops, or when a size variant names a fixed height of its own. A new control gets the height by importing the fragment; it cannot drift by writing h-8.

  • A control that grows takes the height as its minimum. TagsInput and InputGroup are min-h-(--size): empty, or holding one tag, they are one input tall, and a second row of tags or a Textarea inside a group grows them. A Textarea on its own keeps a four-line minimum, which is above the height on purpose.
  • A control inside a group follows the group. The <input> in an InputGroup has no height of its own — it stretches to the group's — and a button in an addon is 24px, which leaves a 32px group 4px of inset around it.
  • A labelled row is one control tall. A Switch, a Checkbox or a RadioGroupItem with its words is 32px at rest, the indicator centred in it, so it lines up with an input beside it and with the rows above and below. Alone — no children, a cell in a table — it is the indicator and nothing else, 16 to 20px, and the height is not asked of it. The row is md; there is no size on these.
  • Not a box. A Badge is a status chip whose 20 to 26px is a smaller scale on purpose, and a Slider is a track. Neither is asked to be an input.
  • One measured exception. A button is 24 CSS pixels, not 1.5rem, because WCAG 2.5.8 states pixels and a rem is 21px at compact density. An sm group holding one — a Combobox, a DatePicker — is therefore 26px at compact, not 24.5. Every other cell of the table is exact.
one

Pick a size in it: every control is laid out in a grid with items-start, so one that is not the height of its neighbours makes its row taller and shows. Measured 2026-09-30 in Chromium, md, every row of the example: 32px at default density, 28px at compact and 36px at comfortable.

What would reverse it: a control that needs a different height per part rather than per size — which a custom property on the base cannot say — or a design that wants an xl field. Button has an xl that the shared scale lacks, and it is the one size a field does not offer.

Text

ControlWhat it isReach for it when
InputA single-line text input.The everyday field.
Input type="number"The same input, numeric, with a decimal keypad on mobile. No steppers — see NumberInput for those.A number typed straight in: a price, an amount.
TextareaMulti-line text.A description, a note, free text.
InputGroupAn input flanked by addons — a prefix, a unit, a button.The value needs an affix or an inline action.
EditableEdit-in-place: a preview that swaps for an input on click.The value is normally read, and editing it is the exception.

Choice

ControlWhat it isManyReach for it when
NativeSelectThe browser's own <select> element, styled to the input scale.no — it is the OS pickerFew options and plain text: it costs no JavaScript, and on mobile it opens the platform picker.
SelectA listbox in a popover, driven by a collection.multipleThe options need to look like more than plain text: icons, descriptions, groups.
ListboxThat same listbox with no popover of its own — you place it.selectionModeThe list must live somewhere Select cannot put it: inline in a panel, or in a popover you own.
ComboboxA text input plus that same listbox — typing filters the collection.multipleThe list is long enough that typing beats scrolling.
LanguagePickerA Combobox over BCP 47 tags, each named in its own language; closed when given a list, open otherwise.noThe value is a language tag.
FacetFilterA Listbox in a popover behind a trigger that counts what is picked.multipleNarrowing a table or a dashboard, where the options are data and carry counts.
TagsInputA list of removable tokens with no option collection — the user invents the values.alwaysThe values do not exist beforehand: free-form tags, keywords, addresses.
RadioGroupA few options, all visible at once.noOne of a few, no popover.
RadioGroupCardThat same item styled as a card, with room for an icon, a description or a preview.noOne of a few, where the options need explaining.
SegmentGroup2–4 options as a compact inline switch. Radio semantics, segmented look.noSwitching a view in place.

Rule of thumb: under ~7 options, NativeSelect; over ~15, Combobox, because that is where scrolling starts costing more than typing; Select in between, when the options need to look like more than plain text. Multiple selection is not the deciding axis — Select and Combobox both take multiple, and TagsInput is multiple by nature. Typing is.

Autocomplete is not a control here. It is the presentation of a Combobox with no trigger button — showTrigger={false} — so it opens on type rather than on click. Same machine, same props, one page: Combobox. A further face of that same machine, Command, is not a form control at all: what it captures is an action, so it is filed under actions.

A menu is a command; a listbox is a value

Half the "which control?" questions here are one question wearing several hats, and the answer is an ARIA role, not a look. Two surfaces open a popover over a list, and they are not interchangeable. role="menu" (Menu, MenuItem, MenuCheckboxItem) is a list of commands: you press one, something happens, the surface closes. It is not anybody's data. role="listbox" (inside Select and Combobox) is a value: it lives in state, and the popover is only its editor.

The test, in one line: if closing the surface leaves state, it is a listbox; if it leaves only an effect, it is a menu.

That is the only axis that matters. The rest of the Choice table is two orthogonal questions on top of it — can you type to filter? and how many can you pick? — which is why these controls look alike and are not.

Command is the honest exception, and reading it clarifies the rule rather than breaking it: a palette runs commands yet is built on a combobox, because the role follows the interaction, not the payload. Type to filter, arrow to highlight, enter to run is combobox behaviour, and a menu cannot filter at all. So: a value needs a listbox; a command needs a menu unless it needs to be searched.

The rule bites hardest on filters, because a filter is a value while the popover-with-checkboxes idiom looks like a menu. Getting the role wrong costs more than purity: a menuitemcheckbox list cannot announce "2 of 5 selected", and a menu has nowhere to grow a search field the day the list has 200 values.

Boolean

ControlWhat it isReach for it when
SwitchReads as on/off.A setting that takes effect immediately.
CheckboxReads as included/excluded, with a third indeterminate state.A value submitted with the form.
CheckboxGroupOne value array across several checkboxes.Related checkboxes you submit together.

Value

ControlWhat it isReach for it when
NumberInputA spinbutton: stepper triggers, arrow keys, drag-to-scrub.A bounded quantity the user nudges — a count, a page size.
SliderA value or range on a track.A bounded number where the range matters more than the digit.
RatingA row of stars on a fixed scale.A score out of five.
DatePickerA date input with a calendar popover. Takes Ark's DateValue; the ISO-string adapter is two functions at your seam.Picking a date in a form.
CalendarThe same machine inline, so the month grid is always visible.A calendar that is furniture, not a form value.
ColorPickerArea, hue and hex, with preset swatches.Picking a colour.

A row of colours the user is not editing is not a control at all: when the colour merely pictures a value that has a name, reach for Swatch, which is aria-hidden and never focusable.

Files

ControlWhat it isReach for it when
FileUploadA dropzone plus the list of what was picked, with previews.The value is a file, not text.

Credentials

ControlWhat it isReach for it when
PasswordInputA password input with a reveal toggle.Entering a password — sign-in, sign-up.
PasswordInput + hasStoredValueThe same input in stored-credential mode: renders empty when a secret exists, and submitting empty means "keep it".Editing a credential that already lives server-side.
PinInputOne cell per character; a pasted code spreads across them.A one-time password or verification code.

Repeating

ControlWhat it isReach for it when
FieldArrayN rows with add and remove, stateless — the caller owns the values.A field the user can have several of.

FieldArray's rowKey must be stable across a commit. If a row's key changes when its value is first saved, React remounts the input and the user loses focus mid-typing — do not "simplify" it to the array index.

AI-assisted

Not a control of their own: Assist, from @kanzo-tech/ai, wraps one of the controls above and lets a model propose its value. What it offers is decided by the control.

Wrapped controlWhat the model offersReach for it when
TextareaA continuation at the caret, as ghost text — Tab to accept.The user is writing prose and a streamed continuation helps.
InputWhole values, as chips under the field; one replaces the value.The user is choosing a value and needs options they did not think of.
TagsInputItems, as chips; one is added.The user is filing something and the model can see what fits.

Every one of these sets its state on the Field, never on the control — see Building a form, then Validation for how errors are shown.

RadioGroupProps is exported, so a wrapper can take the same props without restating them.

On this page