Composition
What Todae offers for building UI comes in four kinds. Use the questions below to place something new. Providers and their hooks, helper functions and constants, such as ThemeProvider, useDensity and composeRefs, sit outside this split.
- Native + CSS. HTML already does the job. Todae ships only CSS in its base styles, which may read the element's own tokens, and no export: styles on the element itself, like Link and Disclosure, or opt-in utility classes, like VisuallyHidden. Their pages sit with the components in the sidebar so they are easy to find.
- Component. One UI concept whose markup, props and behavior Todae can fix without knowing anything about the product. It may own behavior, like Menu and Tabs, or only styles from tokens, like Stack and Text. Most components ship as a compound: a namespace with a
Rootand any named parts, likeMenu.RootandMenu.Item. Components with no parts, likeStack,Text,LabelandCheckbox, are one export. - Hook. Behavior with no markup. Todae exports one when a recipe needs it or products need it under markup of their own. useRovingFocus and useCombobox are hooks. The sidebar lists them under Utilities.
- Recipe. A starting point for a company's own design system. It shows how to put Todae exports together where the result depends on the product: its data, its flow (what to search, how results load, what happens on pick) or how its screens are laid out. A recipe must be thin glue. Hard behavior (keyboard handling, focus management, ARIA wiring, measurement and positioning) must live in a component or hook, so a company can copy the recipe and restyle it without breaking accessibility. A recipe is never a package export, so there is no
Combobox.*.
Placing something new
Ask these in order.
- Does plain HTML do the job with only base CSS, either because every such element should look this way, like every
<a>, or as an opt-in utility class, like.todae-visually-hidden? Then it is native. Anything else goes on to question 2, including an element that needs props, a typed contract or another component's state. That is whyStack(a direction),Text(a size), Label (a requiredhtmlFor) andField.Select(its Field's label and error wiring) are not native. - Can Todae fix its markup, every prop and its behavior without knowing the product's data, fetching, routing, business rules or screen layout? Then it is a component.
Stackpasses: it fixes spacing from tokens, not where things go on a screen. If a recipe, or a product's own markup, needs that behavior, it is a hook too, like useRovingFocus. - Otherwise it belongs to the product. When it puts Todae exports together, Todae documents it as a recipe. Any hard behavior it needs moves into a component or hook so the recipe stays thin glue, such as useCombobox for Combobox. If the copied code would still carry hard behavior of its own, that behavior belongs in Todae.
A component can contain other components, as Menu.Trigger is a Button, and can share state with the children it holds, as Fieldset shares disabled and invalid. It is still a component because Todae can fix how those pieces fit together. A wrapper that adds nothing of its own, no behavior, styles or tokens, and only arranges other exports is never a Todae component. If the arrangement is worth showing, it is a recipe.
Components
Public siblings, for example Field, Label, Description, ErrorMessage, Checkbox, Radio, Switch, Button, Popover, Tooltip, Chip, OptionList, Dialog, and BottomSheet. Compose them side by side. Never nest namespaces, as in Field.Button.Root. One interactive role per component root.
Field.Icon, Field.Affix, and Button.Icon are compound parts. They are not free-floating atoms. Shared Icon, Affix, and Shell live under the hood. They are not public exports.
Field.Root is chrome only. Label is a sibling component. Adornments follow DOM order. No side prop.
A Field and a Popover sit next to each other. Neither wraps the other.
Field shell is not a button. This tree is never allowed.
<Field.Root role="button" onClick={onOpen}>
<Field.TextInput value={q} onChange={(event) => setQ(event.target.value)} />
</Field.Root>
Recipes
Worked examples that compose components and hooks. Combobox is a recipe, not a package namespace. The blocks below are static. They do not run. The hook owns behavior. Components own chrome. There is no shipped Trigger sugar.
Desktop (presentation="popover"). The closed control is a real Field. Spread getAnchorProps onto Field.Root. The list lives in Popover.Root. Worked recipes live on Combobox.
const cb = useCombobox({ presentation: 'popover' })
<div className="labelled-field">
<Label htmlFor={cb.ids.input}>Tags</Label>
<Field.Root {...cb.getAnchorProps()}>
<Field.Icon><Search /></Field.Icon>
<Field.ChipInput {...cb.getChipInputProps()} />
<Field.Icon><ChevronDown /></Field.Icon>
</Field.Root>
</div>
<Popover.Root {...cb.getPopoverProps()}>
<OptionList.Root {...cb.getListboxProps()}>...</OptionList.Root>
</Popover.Root>
anchorRef is the CSS anchor when the trigger is not the element getTriggerProps marks. Combobox uses getAnchorProps on Field.Root. Do not also spread usePopover getTriggerProps.
Touch (presentation="sheet"). The closed control is Button.Root. Search Field lives inside the sheet.
const cb = useCombobox({ presentation: 'sheet' })
<Button.Root {...cb.getTriggerProps()}>Pick…</Button.Root>
<BottomSheet.Root {...cb.getSheetProps()}>
<div className="labelled-field">
<Label htmlFor={cb.ids.input}>Search</Label>
<Field.Root id={cb.ids.input}>
<Field.Icon><Search /></Field.Icon>
<Field.TextInput {...cb.getInputProps()} />
</Field.Root>
</div>
<OptionList.Root {...cb.getListboxProps()}>...</OptionList.Root>
</BottomSheet.Root>
The touch trigger is Button.Root.
Hook
useCombobox is headless. It owns open, value, aria, keyboard, and listbox ids.
useCombobox({ presentation: 'popover' | 'sheet' | 'dialog' | 'auto' })getAnchorProps()onField.Root(desktop)getTriggerProps()onButton.Root(sheet)getInputProps()/getChipInputProps()on the field inputgetPopoverProps()onPopover.RootgetSheetProps()onBottomSheet.RootgetDialogProps()onDialog.RootgetListboxProps()onOptionList.Root
No Trigger sugar component. No Combobox.* chrome. See Combobox.
Never
Field.Root
└─ Field.Button.Root nested compound namespaces
Field.Root as a button that also hosts TextInput
two roles on one root
Combobox.Root, Combobox.Trigger, or Combobox.Icon
recipe chrome in the package
Dual-wiring aria or open outside useCombobox while also using the hook
OptionList and Radio or Checkbox bound to the same value
overlay listbox and permanent choice on one selection
Combobox behavior baked into Field
Touch closed control = Field pretending to be a button
Product trees importing raw Icon or Affix instead of Field.Icon or Field.Affix
Docs that treat Field.Icon or Button.Icon as free-floating atoms
See Combobox for the live recipe.