@uniflowed/ui
variable
Field
export const Field = {
Root: FieldRoot,
Label: FieldLabel,
Control: FieldControl,
Description: FieldDescription,
Status: FieldStatus,
Error: FieldError,
};
An accessible form field.
<Field.Root invalid={error != null}> <Field.Label>Email</Field.Label> <Field.Control render={(props) => <input type="email" {...props} />} /> <Field.Description>We will not share it.</Field.Description> <Field.Status>{saving ? "Saving…" : ""}</Field.Status> <Field.Error>{error}</Field.Error> </Field.Root>
Inside a form, field replaces the hand-written invalid: the form says whether the field is wrong and what the message is, and the field composes every aria-* from that in one place. It also marks the control busy during submit, validation, or async default loading. @uniflowed/form's useFieldSource is what produces one, and field.js's header says why the hook lives there rather than here.
const email = useFieldSource(form, "email", { required: "We need one" }); <Field.Root field={email}>…<Field.Error /></Field.Root>
group is for a set with no single control to point a <label for> at — a radio group, a checkbox group, three selects making a date. The root becomes role="group" named by the label, and the description and the error describe the set.
variable
Tabs
export const Tabs = {
Root: TabsRoot,
List: TabsList,
Tab: TabsTab,
Panel: TabsPanel,
};
Tabs, with the arrow-key behaviour the pattern requires.
activationMode="manual" moves focus without selecting, for panels that cost something to show.
<Tabs.Root defaultValue="one"> <Tabs.List aria-label="Sections"> <Tabs.Tab value="one">One</Tabs.Tab> <Tabs.Tab value="two">Two</Tabs.Tab> </Tabs.List> <Tabs.Panel value="one">…</Tabs.Panel> <Tabs.Panel value="two">…</Tabs.Panel> </Tabs.Root>
Every part takes render, so a tab that is also a route — <Tabs.Tab render={(props) => <a href="#billing" {...props} />}> — is still a tab, with the roving tab stop and the aria-controls a tab has. Tabs.List's renders* Tabs.Tab is unaffected, because it is the *part* it constrains.
variable
Collapsible
export const Collapsible = {
Root: CollapsibleRoot,
Trigger: CollapsibleTrigger,
Content: CollapsibleContent,
};
A button and the region it shows, with the three attributes that say so.
The content stays in the document while it is closed, so the browser's find-in-page can still reach the text in it.
<Collapsible.Root> <Collapsible.Trigger>Details</Collapsible.Trigger> <Collapsible.Content>…</Collapsible.Content> </Collapsible.Root>
variable
Accordion
export const Accordion = {
Root: AccordionRoot,
Item: AccordionItem,
Header: AccordionHeader,
Trigger: AccordionTrigger,
Content: AccordionContent,
};
A stack of disclosures that know about each other.
Accordion.Header takes the heading level, because which heading an accordion's sections are depends on where the accordion sits. Each panel is a region named after the header that opens it.
<Accordion.Root type="single"> <Accordion.Item value="shipping"> <Accordion.Header level={3}> <Accordion.Trigger>Shipping</Accordion.Trigger> </Accordion.Header> <Accordion.Content>…</Accordion.Content> </Accordion.Item> </Accordion.Root>
variable
export const NavigationMenu = {
Root: NavigationMenuRoot,
List: NavigationMenuList,
Item: NavigationMenuItem,
Trigger: NavigationMenuTrigger,
Body: NavigationMenuBody,
Link: NavigationMenuLink,
};
Site navigation: a list of links behind buttons, and not a menu.
<NavigationMenu.Root aria-label="Main"> <NavigationMenu.List> <NavigationMenu.Item value="docs"> <NavigationMenu.Trigger>Docs</NavigationMenu.Trigger> <NavigationMenu.Body> <NavigationMenu.Link href="/guide">Guide</NavigationMenu.Link> </NavigationMenu.Body> </NavigationMenu.Item> </NavigationMenu.List> </NavigationMenu.Root>
variable
RadioGroup
export const RadioGroup = {
Root: RadioGroupRoot,
Item: RadioGroupItem,
Indicator: RadioGroupIndicator,
};
One answer out of several, with the arrow keys that check as they move.
Tab reaches the chosen answer, or the first one while there is none, and leaves the whole group in one press. name puts the answer where a form can submit it.
<Field.Root> <Field.Label>Plan</Field.Label> <Field.Control render={(props) => ( <RadioGroup.Root {...props} defaultValue="free" name="plan"> <RadioGroup.Item value="free"> Free <RadioGroup.Indicator>●</RadioGroup.Indicator> </RadioGroup.Item> <RadioGroup.Item value="pro">Pro</RadioGroup.Item> </RadioGroup.Root> )} /> </Field.Root>
variable
ToggleGroup
export const ToggleGroup = {
Root: ToggleGroupRoot,
Item: ToggleGroupItem,
};
A row of toggle buttons that behaves as one control.
type="multiple" is a group of toggle buttons, any number of them pressed. type="single" is a radio group drawn as segments, and is rendered by RadioGroup rather than written a second time.
<ToggleGroup.Root aria-label="Formatting" type="multiple"> <ToggleGroup.Item value="bold">B</ToggleGroup.Item> <ToggleGroup.Item value="italic">I</ToggleGroup.Item> </ToggleGroup.Root>
variable
Dialog
export const Dialog = {
Root: DialogRoot,
Trigger: DialogTrigger,
Overlay: DialogOverlay,
Body: DialogBody,
Header: DialogHeader,
Footer: DialogFooter,
Title: DialogTitle,
Description: DialogDescription,
Close: DialogClose,
};
A modal dialog: focus moved in, kept in, and given back.
<Dialog.Root> <Dialog.Trigger>Delete</Dialog.Trigger> <Dialog.Overlay /> <Dialog.Body> <Dialog.Header> <Dialog.Title>Delete this project?</Dialog.Title> <Dialog.Description>This cannot be undone.</Dialog.Description> </Dialog.Header> <Dialog.Footer> <Dialog.Close>Cancel</Dialog.Close> </Dialog.Footer> </Dialog.Body> </Dialog.Root>
Every part takes render. Dialog.Title is an <h2> by default and the level is a fact about the page around it rather than about the dialog, so render={(props) => <h3 {...props} />} is how a caller says which — without losing the id aria-labelledby points at. AlertDialog, Sheet and Drawer are made of these parts and pass render straight through.
variable
AlertDialog
export const AlertDialog = {
Root: AlertDialogRoot,
Trigger: AlertDialogTrigger,
Overlay: AlertDialogOverlay,
Body: AlertDialogBody,
Header: AlertDialogHeader,
Footer: AlertDialogFooter,
Title: AlertDialogTitle,
Description: AlertDialogDescription,
Action: AlertDialogAction,
Cancel: AlertDialogCancel,
};
The confirmation: modal, announced as an alert, and not dismissible by a press beside it.
Focus lands on Cancel rather than on the first thing in the dialog, and the description is required — role="alertdialog" exists to announce one, so an alert dialog without it interrupts the reader to say nothing.
<AlertDialog.Root> <AlertDialog.Trigger>Delete</AlertDialog.Trigger> <AlertDialog.Overlay /> <AlertDialog.Body> <AlertDialog.Header> <AlertDialog.Title>Delete this project?</AlertDialog.Title> <AlertDialog.Description>This cannot be undone.</AlertDialog.Description> </AlertDialog.Header> <AlertDialog.Footer> <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> <AlertDialog.Action onClick={remove}>Delete</AlertDialog.Action> </AlertDialog.Footer> </AlertDialog.Body> </AlertDialog.Root>
variable
Sheet
export const Sheet = {
Root: SheetRoot,
Trigger: SheetTrigger,
Overlay: SheetOverlay,
Body: SheetBody,
Header: SheetHeader,
Footer: SheetFooter,
Title: SheetTitle,
Description: SheetDescription,
Close: SheetClose,
};
A modal dialog attached to an edge of the viewport.
side is a union rather than a class name, and every part reports it as data-side — the same attribute Popover.Body writes, so one stylesheet rule covers every overlay in this package.
<Sheet.Root side="left"> <Sheet.Trigger>Filters</Sheet.Trigger> <Sheet.Overlay /> <Sheet.Body> <Sheet.Title>Filters</Sheet.Title> <Sheet.Close>Done</Sheet.Close> </Sheet.Body> </Sheet.Root>
variable
Drawer
export const Drawer = {
Root: DrawerRoot,
Trigger: DrawerTrigger,
Overlay: DrawerOverlay,
Body: DrawerBody,
Handle: DrawerHandle,
Header: DrawerHeader,
Footer: DrawerFooter,
Title: DrawerTitle,
Description: DrawerDescription,
Close: DrawerClose,
};
The sheet you can drag away, with the keyboard that can do everything the drag can.
Drawer.Handle is a role="slider" over the snap points: the arrow keys move between them, Home and End go to the ends, and the closing key at the smallest snap point closes it. WCAG 2.5.7 also wants a single-pointer alternative, so a drawer with a handle and no Drawer.Close raises.
<Drawer.Root side="bottom" snapPoints={[0.4, 1]}> <Drawer.Trigger>Details</Drawer.Trigger> <Drawer.Overlay /> <Drawer.Body> <Drawer.Handle label="Resize the details" /> <Drawer.Title>Details</Drawer.Title> <Drawer.Close>Close</Drawer.Close> </Drawer.Body> </Drawer.Root>
variable
export const Sidebar = {
Root: SidebarRoot,
Trigger: SidebarTrigger,
Header: SidebarHeader,
Body: SidebarBody,
Footer: SidebarFooter,
Item: SidebarItem,
};
Navigation beside the page, which becomes a modal sheet on a narrow one.
Sidebar.Item takes a label and keeps it as the button's accessible name the moment the sidebar collapses to icons — which is the whole reason a collapsing sidebar is a component rather than a class.
<Sidebar.Root defaultOpen={fromCookie}> <Sidebar.Trigger>Menu</Sidebar.Trigger> <Sidebar.Body label="Main"> <Sidebar.Item label="Settings"> <Gear /> Settings </Sidebar.Item> </Sidebar.Body> </Sidebar.Root>
variable
Carousel
export const Carousel = {
Root: CarouselRoot,
Content: CarouselContent,
Item: CarouselItem,
Pause: CarouselPause,
Previous: CarouselPrevious,
Next: CarouselNext,
};
Slides, one at a time, that a reader can stop and cannot fall into.
Carousel.Pause is WCAG 2.2.2's mechanism and must be the first focusable thing inside the carousel; the slides that are not showing are inert, so Tab cannot reach a link nobody can see.
<Carousel.Root autoplay={5000} count={3} label="Featured"> <Carousel.Pause /> <Carousel.Content> <Carousel.Item index={0}>…</Carousel.Item> <Carousel.Item index={1}>…</Carousel.Item> <Carousel.Item index={2}>…</Carousel.Item> </Carousel.Content> <Carousel.Previous /> <Carousel.Next /> </Carousel.Root>
variable
export const ScrollArea = {
Root: ScrollAreaRoot,
Viewport: ScrollAreaViewport,
Scrollbar: ScrollAreaScrollbar,
};
An overflow container a keyboard can actually scroll.
role="region", a name and tabindex="0", because a scroll container is not focusable in every browser and one that is not is one a keyboard reader can see the top of and nothing else.
<ScrollArea.Root label="Release notes"> <ScrollArea.Viewport>…</ScrollArea.Viewport> <ScrollArea.Scrollbar orientation="vertical" /> </ScrollArea.Root>
variable
export const InputOtp = {
Root: InputOtpRoot,
Group: InputOtpGroup,
Slot: InputOtpSlot,
Separator: InputOtpSeparator,
};
A one-time code: six boxes drawn over one real <input>.
One input, so autocomplete="one-time-code" works, a paste fills every box, and a form submits one value under one name.
<InputOtp.Root label="One-time code" length={6} name="code"> <InputOtp.Group> <InputOtp.Slot index={0} /> <InputOtp.Slot index={1} /> <InputOtp.Slot index={2} /> </InputOtp.Group> <InputOtp.Separator>-</InputOtp.Separator> <InputOtp.Group> <InputOtp.Slot index={3} /> <InputOtp.Slot index={4} /> <InputOtp.Slot index={5} /> </InputOtp.Group> </InputOtp.Root>
variable
export const Menu = {
Root: MenuRoot,
Trigger: MenuTrigger,
Body: MenuBody,
Item: MenuItem,
CheckboxItem: MenuCheckboxItem,
RadioGroup: MenuRadioGroup,
RadioItem: MenuRadioItem,
Separator: MenuSeparator,
Group: MenuGroup,
Label: MenuLabel,
Sub: MenuSub,
SubTrigger: MenuSubTrigger,
};
A menu, with the keyboard map every native menu has had for thirty years.
<Menu.Root> <Menu.Trigger>File</Menu.Trigger> <Menu.Body> <Menu.Group> <Menu.Label>Recent</Menu.Label> <Menu.Item onSelect={open}>Open…</Menu.Item> </Menu.Group> <Menu.Separator /> <Menu.Sub> <Menu.SubTrigger>Export</Menu.SubTrigger> <Menu.Body> <Menu.Item onSelect={png}>PNG</Menu.Item> </Menu.Body> </Menu.Sub> </Menu.Body> </Menu.Root>
Every part takes render, which is what makes a menu of links possible — and a menu of links is the most ordinary menu there is:
<Menu.Item render={(props) => <a href="/settings" {...props} />}> Settings </Menu.Item>
The <a> keeps the middle click, the context menu and the status bar; the item keeps the role, the id, the roving tab stop and the press that closes the tree. See the module header for why that is the answer to "no copy step".
variable
export const ContextMenu = {
Root: ContextMenuRoot,
Trigger: ContextMenuTrigger,
Body: MenuBody,
Item: MenuItem,
CheckboxItem: MenuCheckboxItem,
RadioGroup: MenuRadioGroup,
RadioItem: MenuRadioItem,
Separator: MenuSeparator,
Group: MenuGroup,
Label: MenuLabel,
Sub: MenuSub,
SubTrigger: MenuSubTrigger,
};
The same menu, opened by the right button — and by the keyboard.
Shift+F10, the ContextMenu key and a long press all open it, because a command reachable only by right-click is reachable only by a pointer, which is a WCAG 2.1.1 failure. context-menu.js says why the trigger is in the tab order and when to take it out again.
The body needs an aria-label: its trigger is a table row or a canvas rather than a short name, so unlike Menu.Body it cannot name itself after one.
<ContextMenu.Root> <ContextMenu.Trigger>{row}</ContextMenu.Trigger> <ContextMenu.Body aria-label="Row actions"> <ContextMenu.Item onSelect={rename}>Rename…</ContextMenu.Item> <ContextMenu.CheckboxItem defaultChecked>Show hidden</ContextMenu.CheckboxItem> </ContextMenu.Body> </ContextMenu.Root>
variable
export const Menubar = {
Root: MenubarRoot,
Menu: MenubarMenu,
Trigger: MenubarTrigger,
// `Menu.Body` itself: a bar's menu is a root menu, and `menubar.js`'s header
// says why a wrapper with the same defaults would be a second place to drift.
Body: MenuBody,
Item: MenuItem,
CheckboxItem: MenuCheckboxItem,
RadioGroup: MenuRadioGroup,
RadioItem: MenuRadioItem,
Separator: MenuSeparator,
Group: MenuGroup,
Label: MenuLabel,
Sub: MenuSub,
SubTrigger: MenuSubTrigger,
};
A row of menus that behaves as one control: File, Edit, View.
One tab stop for the whole bar, arrows between the menus, and — the part that is always missing — arrows *while a menu is open* that close it and open the next one, so a reader walks File → Edit → View without pressing Escape.
<Menubar.Root aria-label="Main"> <Menubar.Menu value="file"> <Menubar.Trigger>File</Menubar.Trigger> <Menubar.Body> <Menubar.Item onSelect={open}>Open…</Menubar.Item> </Menubar.Body> </Menubar.Menu> </Menubar.Root>
variable
Combobox
export const Combobox = {
Root: ComboboxRoot,
Label: ComboboxLabel,
Input: ComboboxInput,
List: ComboboxList,
Option: ComboboxOption,
Group: ComboboxGroup,
GroupLabel: ComboboxGroupLabel,
Empty: ComboboxEmpty,
Status: ComboboxStatus,
};
A text field with a list of options, navigated without leaving the field.
The caller filters; the component keeps the ARIA wiring true while they do.
<Combobox.Root inputValue={query} onInputValueChange={setQuery}> <Combobox.Label>Country</Combobox.Label> <Combobox.Input /> <Combobox.List> <Combobox.Group> <Combobox.GroupLabel>Europe</Combobox.GroupLabel> {european.map((each) => ( <Combobox.Option key={each} value={each}>{each}</Combobox.Option> ))} </Combobox.Group> </Combobox.List> <Combobox.Empty>No matches.</Combobox.Empty> <Combobox.Status /> </Combobox.Root>
Combobox.Label names the field and Combobox.GroupLabel names a group of options, which is why there are two of them.
variable
Select
export const Select = {
Root: SelectRoot,
Label: SelectLabel,
Trigger: SelectTrigger,
Value: SelectValue,
List: SelectList,
Option: SelectOption,
Group: SelectGroup,
GroupLabel: SelectGroupLabel,
Separator: SelectSeparator,
};
The other half of the combobox pattern: a button, a list, and no typing.
Use a native <select> when a native <select> will do — select.js says so first and means it. This is for the popup a <select> cannot draw.
<Select.Root defaultValue="GB" name="country"> <Select.Label>Country</Select.Label> <Select.Trigger> <Select.Value placeholder="Choose one" /> </Select.Trigger> <Select.List> <Select.Group> <Select.GroupLabel>Europe</Select.GroupLabel> <Select.Option value="GB">United Kingdom</Select.Option> <Select.Option value="FR">France</Select.Option> </Select.Group> <Select.Separator /> <Select.Option value="JP">Japan</Select.Option> </Select.List> </Select.Root>
Select.Label names the field and Select.GroupLabel names a group of options. shadcn has one SelectLabel and it is the second of those; a select needs both, so they are two parts here.
variable
Popover
export const Popover = {
Root: PopoverRoot,
Trigger: PopoverTrigger,
Body: PopoverBody,
};
A dialog that is not modal, anchored to the button that opened it.
Focus moves in, Escape closes it and gives focus back, and Tab *leaves* — the page behind a popover is still there, still scrollable and still tabbable, which is every way in which it is not a Dialog.
<Popover.Root> <Popover.Trigger>Filters</Popover.Trigger> <Popover.Body align="start" side="bottom" sideOffset={8}> <label> Only mine <input type="checkbox" /> </label> </Popover.Body> </Popover.Root>
Popover.Body reports where it ended up as data-side and data-align, and writes the trigger's width and the room it had as custom properties, so a stylesheet can point an arrow and cap a height without measuring anything.
variable
Calendar
export const Calendar = {
Root: CalendarRoot,
Previous: CalendarPrevious,
Next: CalendarNext,
Month: CalendarMonth,
Day: CalendarDay,
};
A month of dates, as one stop in the page's tab order.
The grid is role="grid", the arrow keys move by a day and by a week, and PageUp and PageDown change the month - with Shift, the year. Running off the end of a month shows the next one and lands on its first day, and the month is announced in a live region when it changes.
<Calendar.Root defaultValue="2026-10-14" onValueChange={setWhen}> <Calendar.Previous>Previous month</Calendar.Previous> <Calendar.Next>Next month</Calendar.Next> <Calendar.Month /> </Calendar.Root>
Calendar.Month takes a function child when a day needs more than its number in it - a dot for an appointment, a price for a night - and it is handed the date and returns a Calendar.Day.
Dates are @uniflowed/temporal's PlainDate, or the ISO strings it reads. isDateDisabled marks a day unavailable *without* making it unreachable: it is aria-disabled and the arrow keys still land on it, which is the opposite of what a disabled menu item does and the only way a reader can find out which days are unavailable.
variable
DatePicker
export const DatePicker = {
Root: DatePickerRoot,
Input: DatePickerInput,
Trigger: DatePickerTrigger,
Calendar: DatePickerCalendar,
};
A field somebody types a date into, and a calendar for the times they would rather point at one.
<DatePicker.Root onValueChange={setWhen} value={when}> <DatePicker.Input aria-label="Arrive on" /> <DatePicker.Trigger>Choose a date</DatePicker.Trigger> <DatePicker.Calendar> <Calendar.Previous>Previous month</Calendar.Previous> <Calendar.Next>Next month</Calendar.Next> <Calendar.Month /> </DatePicker.Calendar> </DatePicker.Root>
The field is the control and the grid is the second way in: Escape and a chosen date both put focus back on the field. format and parse are ISO 8601 both ways unless a caller passes their own - date-picker.js says why a locale format is not this package's to guess.
variable
export const Tooltip = {
Provider: TooltipProvider,
Root: TooltipRoot,
Trigger: TooltipTrigger,
Body: TooltipBody,
};
A phrase about a control, on hover and on focus, that WCAG would accept.
Dismissible with Escape, hoverable — the pointer can travel onto it — and never focusable. It does not open on touch, deliberately, so the trigger must carry its own name for a reader holding a phone.
<Tooltip.Provider delayDuration={700} skipDelayDuration={300}> <Tooltip.Root> <Tooltip.Trigger aria-label="Bold">B</Tooltip.Trigger> <Tooltip.Body>Bold (⌘B)</Tooltip.Body> </Tooltip.Root> <Tooltip.Root> <Tooltip.Trigger aria-label="Italic">I</Tooltip.Trigger> <Tooltip.Body>Italic (⌘I)</Tooltip.Body> </Tooltip.Root> </Tooltip.Provider>
Tooltip.Provider is what makes the second icon in that toolbar answer at once instead of making the reader wait the delay again. A tooltip outside one is a complete tooltip with a delay of its own.
variable
HoverCard
export const HoverCard = {
Root: HoverCardRoot,
Trigger: HoverCardTrigger,
Body: HoverCardBody,
};
The preview a name expands into: hovered, focused, and full of links.
Not a tooltip — its contents are reachable, by pointer and by Tab — and not a dialog, because nothing about it is modal.
<HoverCard.Root> <HoverCard.Trigger render={(props) => <a href="/ada" {...props}>@ada</a>} /> <HoverCard.Body> <p>Ada Lovelace</p> <a href="/ada/notes">Notes</a> </HoverCard.Body> </HoverCard.Root>
variable
Toast
export const Toast = {
Region: ToastRegion,
Root: ToastRoot,
Title: ToastTitle,
Description: ToastDescription,
Action: ToastAction,
Close: ToastClose,
};
Notifications, in a live region that was watching before them.
Render Toast.Region once, in the layout; toast() from anywhere.
<Toast.Region> {(each) => ( <Toast.Root> <Toast.Title>{each.content}</Toast.Title> <Toast.Action onClick={undo}>Undo</Toast.Action> <Toast.Close /> </Toast.Root> )} </Toast.Region>
variable
Slider
export const Slider = {
Root: SliderRoot,
Track: SliderTrack,
Range: SliderRange,
Thumb: SliderThumb,
};
A value in a range, with role="slider" on the thumb where it belongs.
One thumb or two; a range is the same component with a second one, each bounded by its neighbour and each needing its own name.
<Slider.Root defaultValue={[20, 60]} valueText={(each) => £${each}}> <Slider.Track> <Slider.Range /> </Slider.Track> <Slider.Thumb aria-label="Minimum" index={0} /> <Slider.Thumb aria-label="Maximum" index={1} /> </Slider.Root>
variable
Resizable
export const Resizable = {
PanelGroup: ResizablePanelGroup,
Panel: ResizablePanel,
Handle: ResizableHandle,
};
Two panes and the splitter between them, operable from the keyboard.
<Resizable.PanelGroup defaultValue={30}> <Resizable.Panel primary>Files</Resizable.Panel> <Resizable.Handle label="Resize the file list" /> <Resizable.Panel>Editor</Resizable.Panel> </Resizable.PanelGroup>
variable
Table
export const Table = {
Root: TableRoot,
Caption: TableCaption,
Header: TableHeader,
Body: TableBody,
Row: TableRow,
Head: TableHead,
RowHeader: TableRowHeader,
Cell: TableCell,
SelectAll: TableSelectAll,
RowSelect: TableRowSelect,
};
A table, with the four things about one nobody gets right by hand.
A real <table>, deliberately not a role="grid" — table.js says why — and its own live region, so a re-sort is something a reader is told about rather than something that happens silently behind them.
<Table.Root onSortChange={setSort} rowCount={500} rowOffset={90} sort={sort}> <Table.Caption>People</Table.Caption> <Table.Header> <Table.Row> <Table.Head> <Table.SelectAll checked={all} onCheckedChange={setAll} /> </Table.Head> <Table.Head column="name">Name</Table.Head> </Table.Row> </Table.Header> <Table.Body> {page.map((person, at) => ( <Table.Row index={at} key={person.id}> <Table.Cell> <Table.RowSelect checked={chosen.has(person.id)} label={Select ${person.name}} onCheckedChange={(on) => choose(person.id, on)} /> </Table.Cell> <Table.RowHeader>{person.name}</Table.RowHeader> </Table.Row> ))} </Table.Body> </Table.Root>
variable
export const Pagination = {
Root: PaginationRoot,
Content: PaginationContent,
Item: PaginationItem,
Previous: PaginationPrevious,
Next: PaginationNext,
};
The navigation a paginated table needs, and the sentence that says it moved.
<Pagination.Root page={4} pageCount={25}> <Pagination.Content> <Pagination.Previous disabled={page === 1} href={hrefFor(page - 1)}>‹</Pagination.Previous> <Pagination.Item current href={hrefFor(4)}>4</Pagination.Item> <Pagination.Next href={hrefFor(page + 1)}>›</Pagination.Next> </Pagination.Content> </Pagination.Root>
variable
Breadcrumb
export const Breadcrumb = {
Root: BreadcrumbRoot,
List: BreadcrumbList,
Item: BreadcrumbItem,
Link: BreadcrumbLink,
Page: BreadcrumbPage,
Separator: BreadcrumbSeparator,
};
The trail above the page, read as places rather than as punctuation.
<Breadcrumb.Root> <Breadcrumb.List> <Breadcrumb.Item> <Breadcrumb.Link href="/">Home</Breadcrumb.Link> </Breadcrumb.Item> <Breadcrumb.Separator>/</Breadcrumb.Separator> <Breadcrumb.Item> <Breadcrumb.Page>Billing</Breadcrumb.Page> </Breadcrumb.Item> </Breadcrumb.List> </Breadcrumb.Root>
Pagination's shape one door along: a <nav> with a name, one aria-current="page", and the separators out of the accessibility tree so the trail is not announced as "Home slash Settings slash Billing". The last crumb is a Breadcrumb.Page and not a link, because it is where the reader already is.
variable
Alert
export const Alert = {
Root: AlertRoot,
Title: AlertTitle,
Description: AlertDescription,
};
A callout, and the live that decides whether anybody is interrupted by it.
<Alert.Root> <Alert.Title>Your trial ends on Friday</Alert.Title> <Alert.Description>Add a card to keep your projects.</Alert.Description> </Alert.Root>
{error != null && ( <Alert.Root live> <Alert.Title>Could not save</Alert.Title> <Alert.Description>{error}</Alert.Description> </Alert.Root> )}
The first has no role at all: it was there when the page loaded, so a live region would announce it on every load or never, and neither is what anybody wanted. The second appeared because something happened, which is what role="alert" is for. alert.js's header says why there is no polite version of this and why Toast is that instead.
variable
Avatar
export const Avatar = {
Root: AvatarRoot,
Image: AvatarImage,
Fallback: AvatarFallback,
};
A picture of a person, and the two states it is not in yet.
<Avatar.Root> <Avatar.Image src={person.photo} /> <Avatar.Fallback>{initials(person.name)}</Avatar.Fallback> </Avatar.Root>
The fallback is absent while the image is loading and present once it has failed, held back long enough that a cached image never flashes initials. alt defaults to "", because an avatar beside the name it belongs to is decorative and a component that helpfully puts the name there makes every screen reader say it twice; pass alt where the picture is the only thing identifying the person.
variable
Skeleton
export const Skeleton = {
Root: SkeletonRoot,
Box: SkeletonBox,
};
The grey boxes, and the sentence that stops them being an empty page.
<Skeleton.Root busy={pending}> {pending ? <Skeleton.Box /> : <Invoices rows={invoices} />} </Skeleton.Root>
The boxes are aria-hidden, the region is aria-busy, and a live region that was mounted empty for a commit says "Loading" — a skeleton screen is busy on its first render, so a region rendered with its message already in it announces nothing at all. Keep the root mounted across the load and toggle busy; unmounting it takes the region away before it can say the wait is over.
type
AccordionType
export type AccordionType = "single" | "multiple";
Whether one section is open at a time, or any number of them.
type
AvatarStatus
export type AvatarStatus = "loading" | "loaded" | "error";
Where an avatar's image is between having been asked for and being there.
Three members rather than a loaded boolean, because the fallback's whole job is to tell the middle one from the last one: an image that has not arrived *yet* must not be replaced, and one that is never arriving must.
type
DateValue
export type DateValue = PlainDate | string;
A date, however the caller had one to hand.
A string is accepted because value="2026-10-14" is what a form field, a URL parameter and a JSON payload all carry, and making every caller construct a PlainDate to pass one in would be ceremony. It is ISO 8601 — the format Temporal.PlainDate.from parses — and not a locale format: parsing 12/03/26 is a locale question that belongs to @uniflowed/temporal rather than to a UI package, and date-picker.js says what that means for a field a reader types into.
type
ActivationMode
export type ActivationMode = "automatic" | "manual";
When a tab becomes the selected one.
type
DialogRole
export type DialogRole = "dialog" | "alertdialog";
What a screen reader is told the dialog is.
A union rather than a string, so role="alertdailog" is a type error at the call rather than a dialog announced as a div with a name — which is what a misspelt role produces, silently, in markup that looks correct.
Two members and not the whole of ARIA: these are the two roles that carry aria-modal, and a Dialog.Body that is a region or a complementary is a different component rather than this one with another string.
type
Edge
export type Edge = "top" | "right" | "bottom" | "left";
Which edge of the viewport a sheet is attached to.
Physical, and deliberately not logical: a design that puts a navigation sheet against the left of the screen means the left of the screen in any writing direction, the same way internal/anchor.js's Side does and for the same reason. What the writing direction changes is the reading order inside the sheet, which is the page's business rather than this component's.
A union rather than a string, so a typo is a type error at the call rather than a data-side="lft" no stylesheet matches and no test notices.
type
FieldSource
export type FieldSource = {|
readonly invalid: boolean,
readonly required: boolean,
readonly disabled: boolean,
readonly busy: boolean,
/** What is wrong, or null while the field is valid. */
readonly message: string | null,
readonly control: Rest,
|};
What a form library tells a field about one of its fields.
Facts, and no attributes: see the module header for why an aria-describedby in here would be the collision this type exists to end. control is the binding — the name, the ref the store attaches through, onChange, onBlur and the constraint attributes a progressive form emits — spread onto whatever element Field.Control renders, underneath the attributes the field computes.
# It is declared here and produced there
@uniflowed/form's useFieldSource builds one of these, so this type is the seam between the two packages — and it lives on this side only because the dependency can only run this way today: this package is on npm and that one is not, and tools/release/publishable.sh refuses a published package that depends on an unpublished one.
That is backwards, and ubugeeei-prod/uf#614 says so. It stands because declaring the type twice trades a documented edge for silent drift — a Field.Root accepting a shape useFieldSource no longer produces would type-check on both sides and fail only where they meet — and because the import is type-only, so no project installing @uniflowed/form loads, bundles or runs any of this package. #210 is the trigger: once @uniflowed/form publishes, this moves there and @uniflowed/ui imports it.
type
export type InputOtpKind = "numeric" | "alphanumeric";
What a code is made of.
A union rather than a pattern string, because the answer decides three things at once — which characters survive typing and pasting, which keyboard a phone shows, and what the field's pattern says — and a caller who writes kind="numberic" should be told at the call rather than discover that their numeric field takes letters.
type
export type MenuSelect = {
readonly defaultPrevented: boolean,
readonly preventDefault: () => mixed,
...
};
The part of a click a menu item's onSelect may read and answer.
Inexact, because what arrives is React's synthetic event and this names only the two members the contract is about: calling preventDefault() keeps the menu open, and the component reads defaultPrevented afterwards to find out. A caller who wants the rest of the event has it — this is the promise, not the object.
type
export type SidebarSide = "left" | "right";
Which side of the layout the sidebar is on.
Two members and not sheet.js's four, because a sidebar is never attached to the top or the bottom: a navigation rail across the top of a page is a header, with different semantics and a different component. <Sidebar.Root side="top"> is a type error, which is the point of naming the union rather than reusing Edge.
type
Sort
export type Sort = {|
readonly column: string,
readonly direction: "ascending" | "descending",
|};
Which column a table is sorted by, and which way.
type
Urgency
export type Urgency = "polite" | "assertive";
How loudly a notification interrupts.
type
Notification
export type Notification = {|
readonly id: string,
/** What a reader is told. A node, so a caller may render their own markup. */
readonly content: React.Node,
readonly urgency: Urgency,
/** How long it stays, in milliseconds, or null for one that never expires. */
readonly duration: number | null,
|};
One notification in the queue.
type
ToastOptions
export type ToastOptions = {|
readonly urgency?: Urgency,
readonly duration?: number | null,
|};
What toast accepts beside the message.
type
ToastChanges
export type ToastChanges = {|
readonly content?: React.Node,
readonly urgency?: Urgency,
readonly duration?: number | null,
|};
What updateToast may change about a notification already queued.
type
ToggleGroupType
export type ToggleGroupType = "single" | "multiple";
Whether the set holds one answer or any number of them.
type
PointerType
export type PointerType = "mouse" | "pen" | "touch" | "keyboard" | "virtual";
The input that produced an interaction.
type
Modality
export type Modality = "keyboard" | "pointer" | "virtual";
Which kind of input a reader used most recently.
type
PhysicalPointer
export type PhysicalPointer = "mouse" | "pen" | "touch";
A pointer with a position: what a hover, a drag or a long press can come from.
type
InteractionEvent
export type InteractionEvent = {
readonly type: string,
readonly target: mixed,
readonly currentTarget: mixed,
readonly defaultPrevented: boolean,
readonly nativeEvent?: mixed,
readonly preventDefault: () => mixed,
readonly stopPropagation: () => mixed,
readonly key?: string,
readonly code?: string,
readonly repeat?: boolean,
readonly button?: number,
readonly buttons?: number,
readonly detail?: number,
readonly pointerId?: number,
readonly pointerType?: string,
readonly clientX?: number,
readonly clientY?: number,
readonly width?: number,
readonly height?: number,
readonly pressure?: number,
readonly relatedTarget?: mixed,
readonly altKey?: boolean,
readonly ctrlKey?: boolean,
readonly metaKey?: boolean,
readonly shiftKey?: boolean,
...
};
The part of an event these hooks read, from React or from the DOM.
Inexact and named for the reason internal/merge-props.js gives for PartEvent: what arrives is React's synthetic event, uf does not merge Flow's jsx.js environment so nothing models one, and these are the members the handlers here actually read. A handler written for this type accepts a native event as well, which is what lets a listener on the document share its reading of a key or a pointer with a handler on the element.
type
InteractionProps
export type InteractionProps = { readonly key?: empty, readonly [string]: mixed };
Props on their way onto an element, as mergeProps returns them.
key is named out of the indexer for the reason Rest in internal/merge-props.js gives at length: an indexer answers mixed for every name, React's key is string | number, and spreading one onto an element is rejected for a property that cannot be there.
type
PressEvent
export type PressEvent = {|
readonly type: "pressstart" | "pressend" | "pressup" | "press",
/** The input that made it. */
readonly pointerType: PointerType,
/** The element the press belongs to. */
readonly target: HTMLElement,
readonly altKey: boolean,
readonly ctrlKey: boolean,
readonly metaKey: boolean,
readonly shiftKey: boolean,
/** Where the pointer was, from the element's left edge; nought for a key or a screen reader. */
readonly x: number,
/** Where the pointer was, from the element's top edge; nought for a key or a screen reader. */
readonly y: number,
/**
* Let a pressable around this one receive the same press.
*
* By default a press belongs to the innermost pressable element; see the
* module header for why that is recorded rather than stopped.
*/
readonly continuePropagation: () => void,
|};
A moment in a press.
type
PressOptions
export type PressOptions = {|
/** Leave text selection alone while a pointer is down. */
readonly allowTextSelectionOnPress?: boolean,
/**
* No press, no hover state, and no activation of the element either: a click
* on a disabled link or submit button is prevented.
*/
readonly isDisabled?: boolean,
/** The press completed, over the element. */
readonly onPress?: (event: PressEvent) => mixed,
/** `isPressed` changed. */
readonly onPressChange?: (isPressed: boolean) => mixed,
/** The press ended, pressed or not: released, left, cancelled or taken away. */
readonly onPressEnd?: (event: PressEvent) => mixed,
/** A press began, or a pointer still down came back over the element. */
readonly onPressStart?: (event: PressEvent) => mixed,
/** A pointer or a key was released over the element, whether or not the press began there. */
readonly onPressUp?: (event: PressEvent) => mixed,
/** Keep focus where it is when a pointer presses the element. */
readonly preventFocusOnPress?: boolean,
/** A pointer that leaves takes the press back for good, rather than until it returns. */
readonly shouldCancelOnPointerExit?: boolean,
|};
What usePress is told.
type
PressProps
export type PressProps = {|
readonly onClick: (event: InteractionEvent) => void,
readonly onDragStart: (event: InteractionEvent) => void,
readonly onKeyDown: (event: InteractionEvent) => void,
readonly onMouseDown: (event: InteractionEvent) => void,
readonly onPointerDown: (event: InteractionEvent) => void,
readonly onPointerEnter: (event: InteractionEvent) => void,
readonly onPointerLeave: (event: InteractionEvent) => void,
readonly onPointerUp: (event: InteractionEvent) => void,
|};
The handlers usePress needs on the element.
type
PressResult
export type PressResult = {|
/** Whether a press is under way and over the element. */
readonly isPressed: boolean,
/** Spread onto the element, or merged with other hooks' props by `mergeProps`. */
readonly pressProps: PressProps,
|};
What usePress hands back.
hook
usePress
export hook usePress(options?: PressOptions): PressResult { ... }
A press, from a pointer, a key or assistive technology, with one set of events.
const { isPressed, pressProps } = usePress({ onPress: () => save() }); return <div {...pressProps} data-pressed={isPressed} role="button" tabIndex={0}>Save</div>;
The module header states every rule and what each one prevents. The events arrive in the order pressstart, pressup, pressend, press, and onPressChange reports every change to isPressed between them.
type
InteractOutsideRef
export type InteractOutsideRef = { readonly current: HTMLElement | null, ... };
A ref to an element a press may land in without being "outside".
type
InteractOutsideOptions
export type InteractOutsideOptions = {|
/** Hear nothing: what an overlay that is closed asks for. */
readonly isDisabled?: boolean,
/** A whole gesture began and ended outside every ref. */
readonly onInteractOutside: (event: Event) => mixed,
/**
* The elements that are not "outside".
*
* The overlay, and whatever opens it: a trigger sits outside the overlay's
* own box and is not "outside" for this purpose, because dismissing there
* and then letting the trigger's own click reopen makes a press on it a
* no-op that flickers.
*
* Read when an event arrives rather than when the listener is attached, so a
* ref that is still null on the commit that opened the overlay is not a
* listener that quietly never worked.
*/
readonly refs: $ReadOnlyArray<InteractOutsideRef>,
|};
What useInteractOutside is told.
hook
useInteractOutside
export hook useInteractOutside(options: InteractOutsideOptions): void { ... }
A press that began *and* ended outside an element: what dismisses an overlay.
useInteractOutside({ isDisabled: !open, onInteractOutside: close, refs: [bodyRef, triggerRef], });
The module header says what a bare pointerdown gets wrong and why this waits for the end of the gesture. One hook rather than a copy in each overlay, because a copy is how the answers drift apart: the case that a scroll must not dismiss is one rule, not five.
function
getInteractionModality
export function getInteractionModality(): Modality | null { ... }
The input used most recently, or null when nothing is listening for it.
For an event handler deciding something now — whether focus it is about to move should draw a ring. A render that depends on the answer reads useInteractionModality instead, which also keeps the listening on.
hook
useInteractionModality
export hook useInteractionModality(): Modality | null { ... }
The input used most recently, re-rendering when it changes; null before any.
type
FocusVisibleResult
export type FocusVisibleResult = {|
/** Whether focus, wherever it is, should be drawn: anything but a pointer came last. */
readonly isFocusVisible: boolean,
|};
What useFocusVisible hands back.
hook
useFocusVisible
export hook useFocusVisible(): FocusVisibleResult { ... }
Whether a focus ring should be drawn, for the page as a whole.
useFocusRing is the one a control wants — it adds whether the control has focus. This is for something that draws focus elsewhere, or an overlay that decides whether to draw one on what it focused.
type
FocusRingOptions
export type FocusRingOptions = {|
/** Count focus anywhere inside the element, not only on the element itself. */
readonly within?: boolean,
|};
What useFocusRing is told.
type
FocusRingProps
export type FocusRingProps = {|
readonly onBlur: (event: InteractionEvent) => void,
readonly onFocus: (event: InteractionEvent) => void,
|};
The handlers useFocusRing needs on the element.
type
FocusRingResult
export type FocusRingResult = {|
readonly focusProps: FocusRingProps,
/** Whether the element — or, with `within`, something inside it — has focus. */
readonly isFocused: boolean,
/** Whether it has focus and the input that came last was not a pointer. */
readonly isFocusVisible: boolean,
|};
What useFocusRing hands back.
hook
useFocusRing
export hook useFocusRing(options?: FocusRingOptions): FocusRingResult { ... }
Whether an element has focus, and whether that focus should be drawn.
const { focusProps, isFocusVisible } = useFocusRing(); return <button {...focusProps} data-focus-visible={isFocusVisible || undefined}>Save</button>;
React Aria's version takes isTextInput and autoFocus as well. Neither is needed here: typing into a text field is ignored for the whole document, and focus that arrives before any input is drawn already, because nothing has made it a pointer's.
type
HoverEvent
export type HoverEvent = {|
readonly type: "hoverstart" | "hoverend",
/** A mouse or a pen: a finger has no hover. */
readonly pointerType: "mouse" | "pen",
readonly target: HTMLElement,
|};
A hover beginning or ending.
type
HoverOptions
export type HoverOptions = {|
/** No hover; a hover in progress ends. */
readonly isDisabled?: boolean,
readonly onHoverChange?: (isHovering: boolean) => mixed,
readonly onHoverEnd?: (event: HoverEvent) => mixed,
readonly onHoverStart?: (event: HoverEvent) => mixed,
|};
What useHover is told.
type
HoverProps
export type HoverProps = {|
readonly onPointerEnter: (event: InteractionEvent) => void,
readonly onPointerLeave: (event: InteractionEvent) => void,
|};
The handlers useHover needs on the element.
type
HoverResult
export type HoverResult = {|
readonly hoverProps: HoverProps,
readonly isHovered: boolean,
|};
What useHover hands back.
hook
useHover
export hook useHover(options?: HoverOptions): HoverResult { ... }
Whether a mouse or a pen is over an element — and never a finger.
const { hoverProps, isHovered } = useHover({ onHoverStart: preview });
A touch pointer is ignored, and so is a mouse pointer within half a second of a touch; see the module header for the iOS behaviour that makes the second rule necessary.
type
LongPressEvent
export type LongPressEvent = {|
readonly type: "longpressstart" | "longpressend" | "longpress",
readonly pointerType: PhysicalPointer,
readonly target: HTMLElement,
readonly altKey: boolean,
readonly ctrlKey: boolean,
readonly metaKey: boolean,
readonly shiftKey: boolean,
readonly x: number,
readonly y: number,
|};
A moment in a long press.
type
LongPressOptions
export type LongPressOptions = {|
/**
* What a reader is told a long press does, as the element's description.
*
* A long press is invisible. A component that offers one owes a keyboard way
* to do the same thing, and this is where it says what that is — "Long press
* or press Shift+F10 for more actions".
*/
readonly accessibilityDescription?: string,
readonly isDisabled?: boolean,
/** The press lasted long enough. The press underneath is cancelled, and its click refused. */
readonly onLongPress?: (event: LongPressEvent) => mixed,
/** The press that might have been a long one ended, whichever it turned out to be. */
readonly onLongPressEnd?: (event: LongPressEvent) => mixed,
/** A press began that could become a long press. */
readonly onLongPressStart?: (event: LongPressEvent) => mixed,
/**
* Which pointers a long press may come from; every one of them by default.
*
* A context menu's long press is a touch's, because a mouse has a right
* button for it — and a mouse held down on a row is starting a text
* selection or a drag, not asking for a menu.
*/
readonly pointerTypes?: $ReadOnlyArray<PhysicalPointer>,
/** How long, in milliseconds. 500 by default. */
readonly threshold?: number,
|};
What useLongPress is told.
type
LongPressProps
export type LongPressProps = {|
...PressProps,
readonly "aria-describedby"?: string,
|};
The handlers and the description useLongPress needs on the element.
type
LongPressResult
export type LongPressResult = {|
readonly longPressProps: LongPressProps,
|};
What useLongPress hands back.
hook
useLongPress
export hook useLongPress(options?: LongPressOptions): LongPressResult { ... }
A press held long enough to mean something else.
const { longPressProps } = useLongPress({ accessibilityDescription: "Long press or press Shift+F10 for more actions", onLongPress: openMenu, pointerTypes: ["touch", "pen"], });
Merged with a usePress on the same element, a long press cancels the press — onPress does not follow onLongPress. See the module header for why it has no keyboard of its own.
type
MovePointerType
export type MovePointerType = PhysicalPointer | "keyboard";
The input a move came from.
type
MoveStartEvent
export type MoveStartEvent = {|
readonly type: "movestart",
readonly pointerType: MovePointerType,
...Modifiers,
|};
A move beginning: the first movement after a pointer went down, or an arrow key.
type
MoveMoveEvent
export type MoveMoveEvent = {|
readonly type: "move",
readonly pointerType: MovePointerType,
/** How far right since the last event; negative is left. */
readonly deltaX: number,
/** How far down since the last event; negative is up. */
readonly deltaY: number,
...Modifiers,
|};
A movement, in pixels for a pointer and in steps of one for a key.
type
MoveEndEvent
export type MoveEndEvent = {|
readonly type: "moveend",
readonly pointerType: MovePointerType,
...Modifiers,
|};
A move ending.
type
MoveOptions
export type MoveOptions = {|
readonly onMove?: (event: MoveMoveEvent) => mixed,
readonly onMoveEnd?: (event: MoveEndEvent) => mixed,
readonly onMoveStart?: (event: MoveStartEvent) => mixed,
|};
What useMove is told.
type
MoveProps
export type MoveProps = {|
readonly onKeyDown: (event: InteractionEvent) => void,
readonly onPointerDown: (event: InteractionEvent) => void,
|};
The handlers useMove needs on the element.
type
MoveResult
export type MoveResult = {|
readonly moveProps: MoveProps,
|};
What useMove hands back.
hook
useMove
export hook useMove(options?: MoveOptions): MoveResult { ... }
How far a pointer or an arrow key moved something, one event at a time.
const { moveProps } = useMove({ onMove: ({ deltaX }) => resizeBy(deltaX) });
A move begins with the first movement rather than with the press, so a click that does not move is not a drag. Physical directions, not reading ones: a caller whose axis runs the other way in a right-to-left page — a slider — turns deltaX round itself, because only it knows that its axis does.
type
KeyboardInteraction
export type KeyboardInteraction = {|
readonly type: "keydown" | "keyup",
readonly key: string,
readonly code: string,
readonly repeat: boolean,
readonly altKey: boolean,
readonly ctrlKey: boolean,
readonly metaKey: boolean,
readonly shiftKey: boolean,
/** The element the key went to, which may be inside the one listening. */
readonly target: mixed,
/** The element listening. */
readonly currentTarget: HTMLElement,
readonly isDefaultPrevented: () => boolean,
readonly preventDefault: () => void,
/**
* Let the key reach the elements around this one.
*
* Stopping is the default, and there is no `stopPropagation` to call: see
* the module header.
*/
readonly continuePropagation: () => void,
|};
A key, as useKeyboard hands it to a handler.
type
KeyboardOptions
export type KeyboardOptions = {|
/** Hear nothing and stop nothing. */
readonly isDisabled?: boolean,
readonly onKeyDown?: (event: KeyboardInteraction) => mixed,
readonly onKeyUp?: (event: KeyboardInteraction) => mixed,
|};
What useKeyboard is told.
type
KeyboardProps
export type KeyboardProps = {|
readonly onKeyDown?: (event: InteractionEvent) => void,
readonly onKeyUp?: (event: InteractionEvent) => void,
|};
The handlers useKeyboard needs on the element — only the ones it was given.
type
KeyboardResult
export type KeyboardResult = {|
readonly keyboardProps: KeyboardProps,
|};
What useKeyboard hands back.
hook
useKeyboard
export hook useKeyboard(options?: KeyboardOptions): KeyboardResult { ... }
Keys on an element, stopped there unless a handler passes them on.
const { keyboardProps } = useKeyboard({ onKeyDown: (event) => { if (event.key === "Delete") remove(); else event.continuePropagation(); }, });
A handler that is not given is not attached, so a useKeyboard with only onKeyDown stops no keyup.
function
mergeProps
export function mergeProps(
...sources: $ReadOnlyArray<?{ readonly [string]: mixed }>
): InteractionProps { ... }
Several hooks' props, for one element.
<button {...mergeProps(pressProps, hoverProps, focusProps)}>Save</button>
An event handler — a name that is on and a capital letter — present in more than one is called in the order given, each of them; a className present in more than one is joined; anything else is the last one given, and an undefined does not replace what came before it. See the module header for why this is not internal/merge-props.js.
component
I18nProvider
export component I18nProvider(
children: React.Node,
locale?: string,
direction?: "ltr" | "rtl",
render?: RenderProp,
...rest: Rest
) { ... }
An explicit locale keeps server and client output identical. Nested providers form islands.
hook
useCollator
export hook useCollator(options?: Intl$CollatorOptions): Intl$Collator { ... }
Share Intl's language-specific ordering with caller-owned collections.
hook
useFilter
export hook useFilter(): {
startsWith: (text: string, query: string) => boolean,
contains: (text: string, query: string) => boolean,
} { ... }
Filtering uses the same collation as typeahead; consumers own the result list.
hook
useDragAndDrop
export hook useDragAndDrop(options: {
onDrop: (drop: Drop) => void,
disabled?: boolean,
}): DragAndDrop { ... }
Pointer drag data and keyboard lift/drop share one validated payload.
function
parseNumber
export function parseNumber(text: string, formatter: NumberFormatter): number | null { ... }
Parse only the symbols the formatter emits, never arbitrary trailing text.
component
export component NumberFieldInput(render?: RenderProp, ...rest: Rest) { ... }
Field.Control may render this input to supply the label, description and error ids.
function
parseColor
export function parseColor(value: string): string | null { ... }
Canonical #rrggbb or #rrggbbaa, with CSS's short hex forms accepted at the boundary.
component
VisuallyHidden
export component VisuallyHidden(
children?: React.Node,
/** Show the content while focus is inside it: the skip-link pattern. */
focusable: boolean = false,
render?: RenderProp,
...rest: Rest
) { ... }
Content a screen reader reads and a sighted reader does not see.
<button><Icon name="trash" /><VisuallyHidden>Delete draft</VisuallyHidden></button> <VisuallyHidden focusable render={(props) => <a href="#main" {...props} />}> Skip to content </VisuallyHidden>
A caller's style is kept, underneath the hiding, so it applies again the moment a focusable one is shown.
function
announce
export function announce(message: string, options?: AnnounceOptions): void { ... }
Say something to a screen reader, from anywhere.
announce(${count} results); announce("Could not save the draft", { politeness: "assertive" });
A function rather than a hook, because what needs announcing comes from event handlers, effects and catch blocks — toast() makes the same choice. An empty message is ignored rather than announced as silence. On the server it returns before scheduling anything.
function
clearAnnouncements
export function clearAnnouncements(politeness?: Politeness): void { ... }
Take every message out, for one politeness or both: the ones in the regions and the ones still waiting to arrive, whose timers are cancelled. After it, nothing announce scheduled for that politeness runs.
Without a doc comment