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 Root and any named parts, like Menu.Root and Menu.Item. Components with no parts, like Stack, Text, Label and Checkbox, 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.

  1. 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 why Stack (a direction), Text (a size), Label (a required htmlFor) and Field.Select (its Field's label and error wiring) are not native.
  2. 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. Stack passes: 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.
  3. 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.

USD

A Field and a Popover sit next to each other. Neither wraps the other.

Name
Recent

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() on Field.Root (desktop)
  • getTriggerProps() on Button.Root (sheet)
  • getInputProps() / getChipInputProps() on the field input
  • getPopoverProps() on Popover.Root
  • getSheetProps() on BottomSheet.Root
  • getDialogProps() on Dialog.Root
  • getListboxProps() on OptionList.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.