@jonmilner/ui 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +51 -0
- package/dist/AlertDialog/index.d.mts +21 -0
- package/dist/AlertDialog/index.mjs +29 -0
- package/dist/Badge/Badge.css +2 -0
- package/dist/Badge/Badge.css.mjs +24 -0
- package/dist/Badge/index.d.mts +11 -0
- package/dist/Badge/index.mjs +54 -0
- package/dist/Box/Box.css +2 -0
- package/dist/Box/Box.css.mjs +8 -0
- package/dist/Box/index.d.mts +8 -0
- package/dist/Box/index.mjs +15 -0
- package/dist/Button/Button.css +2 -0
- package/dist/Button/Button.css.mjs +25 -0
- package/dist/Button/index.d.mts +18 -0
- package/dist/Button/index.mjs +97 -0
- package/dist/Checkbox/Checkbox.css +2 -0
- package/dist/Checkbox/Checkbox.css.mjs +8 -0
- package/dist/Checkbox/index.d.mts +6 -0
- package/dist/Checkbox/index.mjs +45 -0
- package/dist/Chip/Chip.css +2 -0
- package/dist/Chip/Chip.css.mjs +17 -0
- package/dist/Chip/index.d.mts +13 -0
- package/dist/Chip/index.mjs +73 -0
- package/dist/Clickable/Clickable.css +2 -0
- package/dist/Clickable/Clickable.css.mjs +8 -0
- package/dist/Clickable/index.d.mts +11 -0
- package/dist/Clickable/index.mjs +28 -0
- package/dist/Dialog/Dialog.css +2 -0
- package/dist/Dialog/Dialog.css.d.mts +3 -0
- package/dist/Dialog/Dialog.css.mjs +12 -0
- package/dist/Dialog/client.d.mts +12 -0
- package/dist/Dialog/client.mjs +31 -0
- package/dist/Dialog/index.d.mts +30 -0
- package/dist/Dialog/index.mjs +85 -0
- package/dist/Field/Field.css +2 -0
- package/dist/Field/Field.css.mjs +8 -0
- package/dist/Field/client.mjs +26 -0
- package/dist/Field/index.d.mts +20 -0
- package/dist/Field/index.mjs +88 -0
- package/dist/Heading/Heading.css +2 -0
- package/dist/Heading/Heading.css.mjs +8 -0
- package/dist/Heading/index.d.mts +9 -0
- package/dist/Heading/index.mjs +27 -0
- package/dist/Icon/Icon.css +2 -0
- package/dist/Icon/Icon.css.mjs +17 -0
- package/dist/Icon/index.d.mts +12 -0
- package/dist/Icon/index.mjs +24 -0
- package/dist/IconButton/IconButton.css +2 -0
- package/dist/IconButton/IconButton.css.mjs +24 -0
- package/dist/IconButton/index.d.mts +15 -0
- package/dist/IconButton/index.mjs +25 -0
- package/dist/Inline/Inline.css +2 -0
- package/dist/Inline/Inline.css.mjs +8 -0
- package/dist/Inline/index.d.mts +14 -0
- package/dist/Inline/index.mjs +17 -0
- package/dist/Menu/Menu.css +2 -0
- package/dist/Menu/Menu.css.mjs +8 -0
- package/dist/Menu/client.d.mts +9 -0
- package/dist/Menu/client.mjs +27 -0
- package/dist/Menu/index.d.mts +34 -0
- package/dist/Menu/index.mjs +107 -0
- package/dist/MultiSelect/MultiSelect.css +2 -0
- package/dist/MultiSelect/MultiSelect.css.mjs +8 -0
- package/dist/MultiSelect/client.d.mts +8 -0
- package/dist/MultiSelect/client.mjs +93 -0
- package/dist/MultiSelect/index.d.mts +23 -0
- package/dist/MultiSelect/index.mjs +75 -0
- package/dist/SearchField/SearchField.css +2 -0
- package/dist/SearchField/SearchField.css.mjs +27 -0
- package/dist/SearchField/client.d.mts +5 -0
- package/dist/SearchField/client.mjs +74 -0
- package/dist/SearchField/index.d.mts +25 -0
- package/dist/SearchField/index.mjs +36 -0
- package/dist/Stack/Stack.css +2 -0
- package/dist/Stack/Stack.css.mjs +8 -0
- package/dist/Stack/index.d.mts +14 -0
- package/dist/Stack/index.mjs +15 -0
- package/dist/Tabs/Tabs.css +2 -0
- package/dist/Tabs/Tabs.css.mjs +8 -0
- package/dist/Tabs/index.d.mts +20 -0
- package/dist/Tabs/index.mjs +88 -0
- package/dist/Tag/Tag.css +2 -0
- package/dist/Tag/Tag.css.mjs +17 -0
- package/dist/Tag/index.d.mts +13 -0
- package/dist/Tag/index.mjs +82 -0
- package/dist/Text/Text.css +2 -0
- package/dist/Text/Text.css.mjs +32 -0
- package/dist/Text/index.d.mts +16 -0
- package/dist/Text/index.mjs +25 -0
- package/dist/Toast/Toast.css +2 -0
- package/dist/Toast/Toast.css.mjs +18 -0
- package/dist/Toast/client.d.mts +46 -0
- package/dist/Toast/client.mjs +219 -0
- package/dist/Toast/index.d.mts +7 -0
- package/dist/Toast/index.mjs +5 -0
- package/dist/base.css +6 -0
- package/dist/brands/brand.d.mts +9 -0
- package/dist/brands/brand.mjs +4 -0
- package/dist/brands/check-fonts.mjs +15 -0
- package/dist/brands/default.d.mts +4 -0
- package/dist/brands/default.mjs +74 -0
- package/dist/brands/font.d.mts +17 -0
- package/dist/brands/font.mjs +35 -0
- package/dist/brands/forced-colors.mjs +18 -0
- package/dist/brands/index.d.mts +8 -0
- package/dist/brands/index.mjs +5 -0
- package/dist/brands/metrics.mjs +40 -0
- package/dist/contrast/color.d.mts +3 -0
- package/dist/contrast/color.mjs +222 -0
- package/dist/contrast/contrast.d.mts +18 -0
- package/dist/contrast/contrast.mjs +52 -0
- package/dist/contrast/mixes.mjs +33 -0
- package/dist/docs/AlertDialog.md +113 -0
- package/dist/docs/Badge.md +76 -0
- package/dist/docs/Box.md +87 -0
- package/dist/docs/Button.md +98 -0
- package/dist/docs/Checkbox.md +107 -0
- package/dist/docs/Chip.md +92 -0
- package/dist/docs/Clickable.md +82 -0
- package/dist/docs/Dialog.md +166 -0
- package/dist/docs/Field.md +143 -0
- package/dist/docs/Heading.md +69 -0
- package/dist/docs/Icon.md +116 -0
- package/dist/docs/IconButton.md +83 -0
- package/dist/docs/Inline.md +79 -0
- package/dist/docs/Menu.md +181 -0
- package/dist/docs/MultiSelect.md +120 -0
- package/dist/docs/SearchField.md +127 -0
- package/dist/docs/Stack.md +79 -0
- package/dist/docs/Tabs.md +130 -0
- package/dist/docs/Tag.md +97 -0
- package/dist/docs/Text.md +90 -0
- package/dist/docs/Toast.md +115 -0
- package/dist/docs/llms.txt +33 -0
- package/dist/docs/manifest.json +2649 -0
- package/dist/docs/manifest.schema.json +96 -0
- package/dist/docs/migrations/README.md +27 -0
- package/dist/docs/theming.md +59 -0
- package/dist/icons/ArrowLeftIcon.d.mts +4 -0
- package/dist/icons/ArrowLeftIcon.mjs +11 -0
- package/dist/icons/ArrowRightIcon.d.mts +4 -0
- package/dist/icons/ArrowRightIcon.mjs +11 -0
- package/dist/icons/CheckIcon.d.mts +4 -0
- package/dist/icons/CheckIcon.mjs +11 -0
- package/dist/icons/ChevronDownIcon.d.mts +4 -0
- package/dist/icons/ChevronDownIcon.mjs +11 -0
- package/dist/icons/ChevronRightIcon.d.mts +4 -0
- package/dist/icons/ChevronRightIcon.mjs +11 -0
- package/dist/icons/CircleAlertIcon.d.mts +4 -0
- package/dist/icons/CircleAlertIcon.mjs +15 -0
- package/dist/icons/CircleCheckIcon.d.mts +4 -0
- package/dist/icons/CircleCheckIcon.mjs +15 -0
- package/dist/icons/CircleDotIcon.d.mts +4 -0
- package/dist/icons/CircleDotIcon.mjs +19 -0
- package/dist/icons/CircleSlashIcon.d.mts +4 -0
- package/dist/icons/CircleSlashIcon.mjs +15 -0
- package/dist/icons/GitMergeIcon.d.mts +4 -0
- package/dist/icons/GitMergeIcon.mjs +23 -0
- package/dist/icons/GitPullRequestClosedIcon.d.mts +4 -0
- package/dist/icons/GitPullRequestClosedIcon.mjs +23 -0
- package/dist/icons/GitPullRequestDraftIcon.d.mts +4 -0
- package/dist/icons/GitPullRequestDraftIcon.mjs +23 -0
- package/dist/icons/GitPullRequestIcon.d.mts +4 -0
- package/dist/icons/GitPullRequestIcon.mjs +23 -0
- package/dist/icons/InboxIcon.d.mts +4 -0
- package/dist/icons/InboxIcon.mjs +11 -0
- package/dist/icons/InfoIcon.d.mts +4 -0
- package/dist/icons/InfoIcon.mjs +15 -0
- package/dist/icons/LoaderCircleIcon.d.mts +4 -0
- package/dist/icons/LoaderCircleIcon.mjs +11 -0
- package/dist/icons/RefreshCwIcon.d.mts +4 -0
- package/dist/icons/RefreshCwIcon.mjs +15 -0
- package/dist/icons/SearchIcon.d.mts +4 -0
- package/dist/icons/SearchIcon.mjs +15 -0
- package/dist/icons/SettingsIcon.d.mts +4 -0
- package/dist/icons/SettingsIcon.mjs +15 -0
- package/dist/icons/XIcon.d.mts +4 -0
- package/dist/icons/XIcon.mjs +11 -0
- package/dist/icons/index.d.mts +25 -0
- package/dist/icons/index.mjs +45 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +25 -0
- package/dist/popup/client.d.mts +9 -0
- package/dist/popup/client.mjs +31 -0
- package/dist/styles/atoms.css +2 -0
- package/dist/styles/atoms.css.d.mts +219 -0
- package/dist/styles/atoms.css.mjs +2669 -0
- package/dist/styles/button-fills.mjs +17 -0
- package/dist/styles/highlight.mjs +8 -0
- package/dist/styles/hover-tint.mjs +4 -0
- package/dist/styles/join.mjs +4 -0
- package/dist/styles/layers.css +2 -0
- package/dist/styles/part-props.d.mts +8 -0
- package/dist/styles/split-style-props.mjs +9 -0
- package/dist/testing/index.d.mts +4 -0
- package/dist/testing/index.mjs +3 -0
- package/dist/testing/motion.d.mts +22 -0
- package/dist/testing/motion.mjs +49 -0
- package/dist/theme/FontCheck.mjs +11 -0
- package/dist/theme/ThemeProvider.d.mts +15 -0
- package/dist/theme/ThemeProvider.mjs +23 -0
- package/dist/theme/define-theme.d.mts +9 -0
- package/dist/theme/define-theme.mjs +84 -0
- package/dist/theme/use-theme-container.mjs +15 -0
- package/dist/tokens/contract.d.mts +110 -0
- package/dist/tokens/contract.mjs +17 -0
- package/dist/tokens/index.d.mts +2 -0
- package/dist/tokens/index.mjs +1 -0
- package/dist/tokens/roles.d.mts +6 -0
- package/dist/tokens/roles.mjs +24 -0
- package/dist/tokens/scales.d.mts +100 -0
- package/dist/tokens/scales.mjs +103 -0
- package/dist/types.d.mts +7 -0
- package/package.json +159 -3
- package/README.md +0 -3
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Menu
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a toolbar's overflow. Built on Base UI's Menu, which owns the keyboard and focus: arrow keys, Home and End move through the items, typing jumps to a matching item, Esc and an outside click close it, and focus returns to the trigger. The parts add the look: a raised surface with a border and shadow, a highlighted item, a zoom-and-fade that is a plain fade under reduced motion.
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- Several actions on one object that would crowd the layout as buttons.
|
|
10
|
+
- Actions that need no input of their own; each item runs one thing.
|
|
11
|
+
|
|
12
|
+
## When not to use
|
|
13
|
+
|
|
14
|
+
- Picking a value for a form field: use a select (v0.2 forms) or radio buttons; for several values from a long list, use `MultiSelect`.
|
|
15
|
+
- Navigation between pages: use links; a menu of links hides where they go.
|
|
16
|
+
- One or two actions: show them as buttons.
|
|
17
|
+
- Content a user reads, or anything interactive inside the list (fields, toggles): use a popover or a dialog.
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
- Compose the parts: `Menu` > `Menu.Trigger` + `Menu.Popup` > `Menu.Item`, `Menu.Separator`, `Menu.Group`. `Menu` holds the open state and renders no element, so it takes no `className` or `style`; every other part does, plus style props.
|
|
22
|
+
- `Menu.Trigger` is a bare `Clickable`: give it its look with style props (or your own class), and an accessible name. An icon-only trigger needs `aria-label`.
|
|
23
|
+
- For a trigger that reads as a button ("Actions", "New"), pass Button's `variant`, `size`, `iconStart` or `iconEnd` (a `ChevronDownIcon` for "Actions ▾"): it renders a `Button`, with its fill, border and height, and keeps the trigger's behavior and `aria-disabled`. While the menu is open it shows Button's open look, a held tint and an inset ring (ADR 0063); a bare trigger has none, so style it yourself off `aria-expanded`. Style props still override the defaults.
|
|
24
|
+
- `Menu.Item` runs its `onClick` on click, Enter and Space, then the menu closes. Pass `closeOnClick={false}` to keep it open.
|
|
25
|
+
- `variant="danger"` marks a destructive item. It still runs on one click, so open a confirm dialog from it (Cancel focused first) before anything irreversible happens.
|
|
26
|
+
- `disabled` on an item renders `aria-disabled`: it stays announced and reachable with the arrow keys, and does nothing. On the trigger it keeps the button focusable and stops it opening.
|
|
27
|
+
- `Menu.Group` takes a `label` that is shown and names the group. Separate groups with `Menu.Separator`.
|
|
28
|
+
- The popup portals into the trigger's nearest `ThemeProvider`, so it keeps the brand and mode. That provider must not clip or stack its content: wrap the app root, and keep a nested provider off elements with `overflow: hidden`, a `transform` or their own `z-index`, or the popup is clipped or drawn under siblings. It sits on `zIndex` `popover`. A menu opened only through `open`, with no `Menu.Trigger`, has no provider to find and portals to `<body>`.
|
|
29
|
+
- Item text is wrapped in `Text` for you; an `Icon` beside it is spaced by the item's `gap`.
|
|
30
|
+
- `side` (`bottom` by default) and `align` (`start`) place the popup against the trigger; it flips when it would overflow the viewport.
|
|
31
|
+
- Never add key handlers or `tabIndex` to the parts; Base UI handles the keyboard.
|
|
32
|
+
- The module has no hooks, so a server component can render the parts; `onClick` and other functions must come from a client component.
|
|
33
|
+
|
|
34
|
+
## Do
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<Menu>
|
|
38
|
+
<Menu.Trigger aria-label="Actions for Invoice 42" paddingInline="8" borderRadius="sm">
|
|
39
|
+
<MoreIcon />
|
|
40
|
+
</Menu.Trigger>
|
|
41
|
+
<Menu.Popup>
|
|
42
|
+
<Menu.Item onClick={edit}>Edit</Menu.Item>
|
|
43
|
+
<Menu.Item onClick={duplicate}>Duplicate</Menu.Item>
|
|
44
|
+
<Menu.Separator />
|
|
45
|
+
<Menu.Item variant="danger" onClick={() => setConfirming(true)}>
|
|
46
|
+
Delete
|
|
47
|
+
</Menu.Item>
|
|
48
|
+
</Menu.Popup>
|
|
49
|
+
</Menu>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<Menu.Popup>
|
|
54
|
+
<Menu.Group label="Sort by">
|
|
55
|
+
<Menu.Item onClick={() => sort("newest")}>Newest</Menu.Item>
|
|
56
|
+
<Menu.Item onClick={() => sort("oldest")}>Oldest</Menu.Item>
|
|
57
|
+
</Menu.Group>
|
|
58
|
+
</Menu.Popup>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<Menu>
|
|
63
|
+
<Menu.Trigger variant="secondary" size="sm">
|
|
64
|
+
Actions
|
|
65
|
+
</Menu.Trigger>
|
|
66
|
+
<Menu.Popup>
|
|
67
|
+
<Menu.Item onClick={archive}>Archive</Menu.Item>
|
|
68
|
+
</Menu.Popup>
|
|
69
|
+
</Menu>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Don't
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// Deletes on one click with no way back: confirm it in a dialog first.
|
|
76
|
+
<Menu.Item variant="danger" onClick={deleteForever}>
|
|
77
|
+
Delete
|
|
78
|
+
</Menu.Item>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// Base UI already moves focus with the arrow keys; this fights it.
|
|
83
|
+
<Menu.Item onKeyDown={(event) => event.key === "ArrowDown" && next()}>Edit</Menu.Item>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// A link list is navigation, not a menu of actions.
|
|
88
|
+
<Menu.Item onClick={() => navigate("/settings")}>Settings</Menu.Item>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Accessibility
|
|
92
|
+
|
|
93
|
+
- The trigger has `aria-haspopup="menu"` and `aria-expanded`; the popup is `role="menu"` labelled by the trigger, items are `role="menuitem"`, groups are `role="group"` named by their label.
|
|
94
|
+
- Opening from the keyboard focuses the first item; from the pointer, the popup. Focus stays in the menu until it closes, then returns to the trigger.
|
|
95
|
+
- The highlighted item gets a fill (`bgMuted` in light mode; in dark mode `bgRaised` with 10% `fg` mixed in, since `bgMuted` sits too close to the popup there), and the shared focus ring when reached from the keyboard. Under forced colors it is `Highlight` with `HighlightText`, disabled items are `GrayText`, and the popup keeps its border.
|
|
96
|
+
- The danger item's `danger` color passes 4.5:1 on the popup and on the highlight fill in every preset (`Menu.test.tsx` checks it, with `fg` and `fgMuted`); the word ("Delete") carries the meaning, not the color.
|
|
97
|
+
|
|
98
|
+
## Props
|
|
99
|
+
|
|
100
|
+
Generated from the TypeScript types.
|
|
101
|
+
|
|
102
|
+
### Menu
|
|
103
|
+
|
|
104
|
+
| Prop | Type | Required | Description |
|
|
105
|
+
| --- | --- | --- | --- |
|
|
106
|
+
| `children` | `ReactNode` | no | |
|
|
107
|
+
| `defaultOpen` | `boolean` | no | Whether the menu is initially open. To render a controlled menu, use the `open` prop instead. |
|
|
108
|
+
| `disabled` | `boolean` | no | Whether the component should ignore user interaction. |
|
|
109
|
+
| `loopFocus` | `boolean` | no | Whether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys. |
|
|
110
|
+
| `modal` | `boolean` | no | Determines if the menu enters a modal state when open. - `true`: user interaction is limited to the menu: document page scroll is locked and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. On touch devices, a `true` modal blocks outside taps but leaves the page scrollable unless the popup spans nearly the full viewport width, matching native iOS behavior. Nested menus ignore this prop, and menus opened by hover are never modal. |
|
|
111
|
+
| `onOpenChange` | `((open: boolean, eventDetails: MenuRootChangeEventDetails) => void)` | no | Event handler called when the menu is opened or closed. |
|
|
112
|
+
| `open` | `boolean` | no | Whether the menu is currently open. |
|
|
113
|
+
|
|
114
|
+
### Menu.Group
|
|
115
|
+
|
|
116
|
+
| Prop | Type | Required | Description |
|
|
117
|
+
| --- | --- | --- | --- |
|
|
118
|
+
| `children` | `ReactNode` | no | The content of the component. |
|
|
119
|
+
| `className` | `string` | no | |
|
|
120
|
+
| `label` | `ReactNode` | yes | |
|
|
121
|
+
| `style` | `CSSProperties` | no | |
|
|
122
|
+
|
|
123
|
+
Also accepts the style props (see `manifest.json` `styleProps`).
|
|
124
|
+
|
|
125
|
+
### Menu.Item
|
|
126
|
+
|
|
127
|
+
| Prop | Type | Required | Description |
|
|
128
|
+
| --- | --- | --- | --- |
|
|
129
|
+
| `className` | `string` | no | |
|
|
130
|
+
| `closeOnClick` | `boolean` | no | Whether to close the menu when the item is clicked. |
|
|
131
|
+
| `disabled` | `boolean` | no | Whether the component should ignore user interaction. |
|
|
132
|
+
| `id` | `string` | no | |
|
|
133
|
+
| `label` | `string` | no | Overrides the text used for keyboard text navigation and filtering. Falls back to the rendered text when not provided. |
|
|
134
|
+
| `nativeButton` | `boolean` | no | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `true` if the rendered element is a native button. |
|
|
135
|
+
| `onClick` | `((event: BaseUIEvent<MouseEvent<HTMLDivElement, MouseEvent>>) => void)` | no | The click handler for the menu item. |
|
|
136
|
+
| `style` | `CSSProperties` | no | |
|
|
137
|
+
| `variant` | `"danger" \| "neutral"` | no | |
|
|
138
|
+
|
|
139
|
+
Also accepts the style props (see `manifest.json` `styleProps`).
|
|
140
|
+
|
|
141
|
+
### Menu.Popup
|
|
142
|
+
|
|
143
|
+
| Prop | Type | Required | Description |
|
|
144
|
+
| --- | --- | --- | --- |
|
|
145
|
+
| `align` | `"center" \| "end" \| "start"` | no | How to align the popup relative to the specified side. Submenus and menubars default to `'start'`. |
|
|
146
|
+
| `children` | `ReactNode` | no | |
|
|
147
|
+
| `className` | `string` | no | |
|
|
148
|
+
| `finalFocus` | `boolean \| RefObject<HTMLElement \| null> \| ((closeType: InteractionType) => void \| boolean \| HTMLElement \| null)` | no | Determines the element to focus when the menu is closed. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (trigger or previously focused element). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing. |
|
|
149
|
+
| `id` | `string` | no | |
|
|
150
|
+
| `side` | `"bottom" \| "inline-end" \| "inline-start" \| "left" \| "right" \| "top"` | no | Which side of the anchor element to align the popup against. May automatically change to avoid collisions. Submenus and vertical menubars default to `'inline-end'`. |
|
|
151
|
+
| `style` | `CSSProperties` | no | |
|
|
152
|
+
|
|
153
|
+
Also accepts the style props (see `manifest.json` `styleProps`).
|
|
154
|
+
|
|
155
|
+
### Menu.Separator
|
|
156
|
+
|
|
157
|
+
| Prop | Type | Required | Description |
|
|
158
|
+
| --- | --- | --- | --- |
|
|
159
|
+
| `className` | `string` | no | |
|
|
160
|
+
| `orientation` | `"horizontal" \| "vertical"` | no | The orientation of the separator. |
|
|
161
|
+
| `style` | `CSSProperties` | no | |
|
|
162
|
+
|
|
163
|
+
Also accepts the style props (see `manifest.json` `styleProps`).
|
|
164
|
+
|
|
165
|
+
### Menu.Trigger
|
|
166
|
+
|
|
167
|
+
| Prop | Type | Required | Description |
|
|
168
|
+
| --- | --- | --- | --- |
|
|
169
|
+
| `children` | `ReactNode` | no | |
|
|
170
|
+
| `closeDelay` | `number` | no | How long to wait before closing the menu that was opened on hover. Specified in milliseconds. Requires the `openOnHover` prop. |
|
|
171
|
+
| `delay` | `number` | no | How long to wait before the menu may be opened on hover. Specified in milliseconds. Requires the `openOnHover` prop. |
|
|
172
|
+
| `disabled` | `boolean` | no | Whether the component should ignore user interaction. |
|
|
173
|
+
| `handle` | `MenuHandle<unknown>` | no | A handle to associate the trigger with a menu. |
|
|
174
|
+
| `iconEnd` | `ComponentType` | no | |
|
|
175
|
+
| `iconStart` | `ComponentType` | no | |
|
|
176
|
+
| `openOnHover` | `boolean` | no | Whether the menu should also open when the trigger is hovered. |
|
|
177
|
+
| `payload` | `unknown` | no | A payload to pass to the menu when it is opened. |
|
|
178
|
+
| `size` | `"lg" \| "med" \| "sm"` | no | |
|
|
179
|
+
| `variant` | `"primary" \| "secondary"` | no | |
|
|
180
|
+
|
|
181
|
+
Also accepts the style props (see `manifest.json` `styleProps`).
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# MultiSelect
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
A form field for picking any number of values from a list too long to show at once. A trigger styled as Button's secondary look opens a listbox of options; each selected option shows a check mark, and the trigger summarises the selection as the joined labels, or a count ("3 selected") once the labels would not fit. Built on Base UI's `Select` in multiple mode (ADR 0003), which owns the keyboard and focus: arrow keys, Home and End move through the options, typing jumps to a match, Esc and an outside click close it, and focus returns to the trigger. It composes `Field` for its label, description and error (ADR 0026).
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- Choosing several values from a list of roughly more than five to seven options in a form or a filter ("Regions", "Labels", "Assignees").
|
|
10
|
+
- A field where the list is too long to lay out as checkboxes but every option is a plain label.
|
|
11
|
+
|
|
12
|
+
## When not to use
|
|
13
|
+
|
|
14
|
+
- A few options, all visible on the page: use `ChipGroup multiple`.
|
|
15
|
+
- A short list shown inline, each with its own label: use a `Checkbox` group in a `Field`.
|
|
16
|
+
- Actions: use `Menu`; a multi-select picks values and runs nothing.
|
|
17
|
+
- One choice out of several: use a radio group or a single select.
|
|
18
|
+
- Search, filtering or async options: wait for Select and Combobox (v0.2, ADR 0026).
|
|
19
|
+
- Users who need to see every selected value at once: use `Chip` or `ChipGroup`, or show the selection beside the field.
|
|
20
|
+
|
|
21
|
+
## Rules
|
|
22
|
+
|
|
23
|
+
- `label` is required and names the field. `description` and `error` render below the trigger through `Field`; a non-empty `error` marks the field invalid and joins the trigger's `aria-describedby`.
|
|
24
|
+
- `options` is a list of `{ value, label, disabled? }`. `label` is plain text: it is shown in the list and in the trigger summary.
|
|
25
|
+
- `value` with `onValueChange` controls it; `defaultValue` leaves it uncontrolled. Both are arrays of option values, and `onValueChange` gets the new array.
|
|
26
|
+
- `name` makes each selected value submit as its own `name=value` pair, as a `<select multiple>` does; read them with `formData.getAll(name)`. Nothing selected submits nothing.
|
|
27
|
+
- `placeholder` (`"Select"`) shows while nothing is selected. `countLabel` (`"{count} selected"`) is the summary once the joined labels do not fit; `{count}` is replaced with the number selected. Translate both. One selected label that does not fit is truncated, not replaced by a count.
|
|
28
|
+
- `disabled` takes the trigger out of the tab order and mutes the label. `required` sets `aria-required` on the trigger; report a missing value through `error`.
|
|
29
|
+
- The trigger and the open listbox are both named by `label`.
|
|
30
|
+
- Style props, `className` and `style` go to the `Field` around the label, trigger and messages, not to the trigger or the popup.
|
|
31
|
+
- The popup portals into the trigger's nearest `ThemeProvider`, so it keeps the brand and mode. That provider must not clip or stack its content: wrap the app root, and keep a nested provider off elements with `overflow: hidden`, a `transform` or their own `z-index`. It sits on `zIndex` `popover`, below the trigger by default, and flips when it would overflow the viewport.
|
|
32
|
+
- Never add key handlers or `tabIndex`; Base UI handles the keyboard.
|
|
33
|
+
- The module has no hooks, so a server component can render it; `onValueChange` must come from a client component.
|
|
34
|
+
|
|
35
|
+
## Do
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<MultiSelect
|
|
39
|
+
label="Regions"
|
|
40
|
+
name="regions"
|
|
41
|
+
options={[
|
|
42
|
+
{ value: "eu-west", label: "Europe West" },
|
|
43
|
+
{ value: "us-east", label: "US East" },
|
|
44
|
+
{ value: "ap-south", label: "Asia Pacific South" },
|
|
45
|
+
]}
|
|
46
|
+
defaultValue={["eu-west"]}
|
|
47
|
+
/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<MultiSelect
|
|
52
|
+
label="Labels"
|
|
53
|
+
description="Filters apply as you pick."
|
|
54
|
+
options={labels}
|
|
55
|
+
value={selected}
|
|
56
|
+
onValueChange={setSelected}
|
|
57
|
+
placeholder="All labels"
|
|
58
|
+
countLabel="{count} labels"
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<MultiSelect
|
|
64
|
+
label="Assignees"
|
|
65
|
+
name="assignees"
|
|
66
|
+
options={people}
|
|
67
|
+
required
|
|
68
|
+
error={missing ? "Pick at least one assignee." : undefined}
|
|
69
|
+
/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Don't
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// Three options fit on screen: a checkbox group shows every choice without a click.
|
|
76
|
+
<MultiSelect label="Notify me by" options={[email, sms, push]} />
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
// Picking one value is not a multi-select; the trigger would read "1 selected" for no reason.
|
|
81
|
+
<MultiSelect label="Country" options={countries} onValueChange={([first]) => setCountry(first)} />
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// A menu runs actions; MultiSelect keeps a selection.
|
|
86
|
+
<MultiSelect label="Actions" options={[{ value: "delete", label: "Delete" }]} />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Accessibility
|
|
90
|
+
|
|
91
|
+
- The trigger is a button with `aria-haspopup="listbox"` and `aria-expanded`, labelled by the `Field` label and described by the description and error. The popup is `role="listbox"` with `aria-multiselectable="true"`, options are `role="option"` with `aria-selected`.
|
|
92
|
+
- Opening from the keyboard focuses an option; focus stays in the list until it closes, then returns to the trigger. Choosing an option toggles it and leaves the list open, so several can be picked in one visit.
|
|
93
|
+
- Selection is a check mark on the option as well as its highlight, so it is not color alone. The check's space is always in the row, so the text never shifts.
|
|
94
|
+
- The trigger summary is text: the joined labels, or the count. When the count replaces the labels, the labels are hidden from assistive technology so the count is read once.
|
|
95
|
+
- The shared focus ring shows on the trigger and, from the keyboard, on the highlighted option. Under forced colors the highlighted option is `Highlight` with `HighlightText`, and the popup keeps its border.
|
|
96
|
+
|
|
97
|
+
## Props
|
|
98
|
+
|
|
99
|
+
Generated from the TypeScript types.
|
|
100
|
+
|
|
101
|
+
### MultiSelect
|
|
102
|
+
|
|
103
|
+
| Prop | Type | Required | Description |
|
|
104
|
+
| --- | --- | --- | --- |
|
|
105
|
+
| `className` | `string` | no | |
|
|
106
|
+
| `countLabel` | `string` | no | |
|
|
107
|
+
| `defaultValue` | `readonly string[]` | no | |
|
|
108
|
+
| `description` | `ReactNode` | no | |
|
|
109
|
+
| `disabled` | `boolean` | no | |
|
|
110
|
+
| `error` | `ReactNode` | no | |
|
|
111
|
+
| `label` | `ReactNode` | yes | |
|
|
112
|
+
| `name` | `string` | no | |
|
|
113
|
+
| `onValueChange` | `((value: string[]) => void)` | no | |
|
|
114
|
+
| `options` | `readonly Option[]` | yes | |
|
|
115
|
+
| `placeholder` | `string` | no | |
|
|
116
|
+
| `required` | `boolean` | no | |
|
|
117
|
+
| `style` | `CSSProperties` | no | |
|
|
118
|
+
| `value` | `readonly string[]` | no | |
|
|
119
|
+
|
|
120
|
+
Also accepts the style props (see `manifest.json` `styleProps`).
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# SearchField
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
A form field for a search query: an `<input type="search">` (role `searchbox`) with a search icon at its inline start and, while it holds a value, a clear button at its inline end. Escape empties a field that has a value, and the clear button empties it and puts focus back in the input, so a keyboard user never loses their place (ADR 0087). Built on Base UI's `Input` (ADR 0003); it composes `Field` for its label, description and error (ADR 0026). Three sizes at Button's heights (`sm`, `med`, `lg`: 32, 40 and 48 px).
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- A query that searches or filters a list, a table or a page: "Search issues", "Filter accounts".
|
|
10
|
+
- A toolbar or page header search, with `hideLabel` when the design shows no label. Like every `Field` control it reserves a line below the input for its error, so align it to a Button by the input's top, not its center.
|
|
11
|
+
|
|
12
|
+
## When not to use
|
|
13
|
+
|
|
14
|
+
- Any other single-line text: use a text field (planned); a search field announces itself as a search.
|
|
15
|
+
- Picking from a known list as the user types: wait for Combobox (v0.2, ADR 0026).
|
|
16
|
+
- Picking several values from a list: use `MultiSelect` or `ChipGroup`.
|
|
17
|
+
|
|
18
|
+
## Rules
|
|
19
|
+
|
|
20
|
+
- `label` is required and names the input, even when the design shows none: pass `hideLabel` to keep it for assistive technology and hide it from sight. Never pass an empty label, and never let `placeholder` stand in for one.
|
|
21
|
+
- `value` with `onValueChange` controls it; `defaultValue` leaves it uncontrolled. `onValueChange` gets the new string on each keystroke, on Escape and on clear. Debounce a slow search in the consumer.
|
|
22
|
+
- Escape on a field with a value empties it and goes no further, so a surrounding `Dialog` stays open; a second Escape on the empty field reaches the Dialog. Escape during IME composition belongs to the IME.
|
|
23
|
+
- The clear button shows only while the field holds a value and is not disabled. Its name is `clearLabel` (`"Clear search"`); translate it. The input always reserves its room, so the first keystroke moves nothing.
|
|
24
|
+
- `description` and `error` render below the input through `Field`; a non-empty `error` marks the field invalid (`aria-invalid`, a danger border) and joins the input's `aria-describedby`.
|
|
25
|
+
- `disabled` uses the native `disabled`: the input leaves the tab order and does not submit, with a dashed border. `required` sets the native `required`.
|
|
26
|
+
- `name` submits the query with its form. An uncontrolled field does not follow a native form reset; control it if the form resets.
|
|
27
|
+
- Style props, `className` and `style` go to the `Field` around the label, input and messages, not to the input.
|
|
28
|
+
- `inputProps` reach the input: `ref`, `id`, `autoFocus`, `autoComplete`, `spellCheck`, `enterKeyHint`, `maxLength`, `onKeyDown` and `aria-*` such as `aria-controls` (ADR 0093). `label` owns the name, Field owns `aria-invalid`, `aria-describedby` and `aria-errormessage`, and `disabled` and `required` own their state, so those, `aria-readonly` and `aria-hidden` are not among them.
|
|
29
|
+
- A consumer `onKeyDown` runs before the field's own. Escape still clears unless that handler called `event.preventDefault()`. A prevented Escape also propagates, so a surrounding Dialog closes on it.
|
|
30
|
+
- A consumer `ref` gets the input, and the clear button still returns focus to it. A consumer `id` takes over from Field's generated one at hydration, and the label follows it.
|
|
31
|
+
- Wrapping the field in a search landmark (`<search>` or `role="search"`, with the form around it) is the page's job, not the field's.
|
|
32
|
+
- The module has no hooks, so a server component can render it; `onValueChange`, and an `inputProps` `ref` or `onKeyDown`, must come from a client component.
|
|
33
|
+
|
|
34
|
+
## Do
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<SearchField label="Search issues" hideLabel placeholder="Title or number" name="q" />
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<SearchField
|
|
42
|
+
label="Filter accounts"
|
|
43
|
+
description="Matches the name or the last four digits."
|
|
44
|
+
value={query}
|
|
45
|
+
onValueChange={setQuery}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<search>
|
|
51
|
+
<form action="/search">
|
|
52
|
+
<SearchField label="Search the docs" hideLabel name="q" size="lg" />
|
|
53
|
+
</form>
|
|
54
|
+
</search>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// A "/" shortcut focuses the field; the results list is the input's `aria-controls`.
|
|
59
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
60
|
+
<SearchField
|
|
61
|
+
label="Search issues"
|
|
62
|
+
hideLabel
|
|
63
|
+
value={query}
|
|
64
|
+
onValueChange={setQuery}
|
|
65
|
+
inputProps={{ ref, autoComplete: "off", "aria-controls": "issue-results" }}
|
|
66
|
+
/>;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Don't
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// Another name for the input: `label` names it, visible or not.
|
|
73
|
+
<SearchField label="Search issues" inputProps={{ "aria-label": "Find" }} />
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// The placeholder is not a name: it disappears on the first keystroke.
|
|
78
|
+
<SearchField label="" placeholder="Search" />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// A search field for plain text input: it is announced as a search.
|
|
83
|
+
<SearchField label="Your name" />
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// Another clear button: the field has its own.
|
|
88
|
+
<Inline gap="8">
|
|
89
|
+
<SearchField label="Search" value={q} onValueChange={setQ} />
|
|
90
|
+
<IconButton label="Clear" icon={XIcon} onClick={() => setQ("")} />
|
|
91
|
+
</Inline>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Accessibility
|
|
95
|
+
|
|
96
|
+
- The input is a `searchbox` named by the label, visible or not; the search icon is hidden from assistive technology.
|
|
97
|
+
- The clear button is a named button in the tab order after the input. Activating it, by pointer or keyboard, empties the field and moves focus to the input, never to `body`.
|
|
98
|
+
- Escape clears without closing a surrounding Dialog; on an empty field it passes through.
|
|
99
|
+
- Showing and hiding the clear button shifts nothing: the input's inline-end padding always holds it.
|
|
100
|
+
- Invalid adds a danger border and the error message with its icon, never color alone; disabled adds a dashed border. Every size is at least 24 px tall, and the clear button meets the 24 by 24 CSS px target size. Under forced colors the border, text and disabled state use system colors.
|
|
101
|
+
|
|
102
|
+
## Props
|
|
103
|
+
|
|
104
|
+
Generated from the TypeScript types.
|
|
105
|
+
|
|
106
|
+
### SearchField
|
|
107
|
+
|
|
108
|
+
| Prop | Type | Required | Description |
|
|
109
|
+
| --- | --- | --- | --- |
|
|
110
|
+
| `className` | `string` | no | |
|
|
111
|
+
| `clearLabel` | `string` | no | |
|
|
112
|
+
| `defaultValue` | `string` | no | |
|
|
113
|
+
| `description` | `ReactNode` | no | |
|
|
114
|
+
| `disabled` | `boolean` | no | |
|
|
115
|
+
| `error` | `ReactNode` | no | |
|
|
116
|
+
| `hideLabel` | `boolean` | no | |
|
|
117
|
+
| `inputProps` | `SearchFieldInputProps` | no | |
|
|
118
|
+
| `label` | `ReactNode` | yes | |
|
|
119
|
+
| `name` | `string` | no | |
|
|
120
|
+
| `onValueChange` | `((value: string) => void)` | no | |
|
|
121
|
+
| `placeholder` | `string` | no | |
|
|
122
|
+
| `required` | `boolean` | no | |
|
|
123
|
+
| `size` | `"lg" \| "med" \| "sm"` | no | |
|
|
124
|
+
| `style` | `CSSProperties` | no | |
|
|
125
|
+
| `value` | `string` | no | |
|
|
126
|
+
|
|
127
|
+
Also accepts the style props (see `manifest.json` `styleProps`).
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Stack
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
A vertical column of children with even space between them. `Stack` is a `Box` with `display="flex"` and `flexDirection="column"` as default style props, spaced by a required `gap`. Children render as they are: no wrapper per child.
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- Vertical rhythm inside a panel, card, form or page section: `<Stack gap="16">`.
|
|
10
|
+
- A list whose items sit apart by a fixed step: `<Stack as="ul" gap="8">`.
|
|
11
|
+
- A column that becomes a row at a breakpoint: `flexDirection={{ base: "column", med: "row" }}`.
|
|
12
|
+
|
|
13
|
+
## When not to use
|
|
14
|
+
|
|
15
|
+
- A row of items that wraps (buttons, tags, metadata): use `Inline`.
|
|
16
|
+
- A two-dimensional layout: use `Box` with `display="grid"`.
|
|
17
|
+
- One child, or spacing only on one side: use `Box` with padding or margin.
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
- `gap` is required and takes spacing tokens only: px keys (`"16"`) or a responsive object (`{ base: "8", med: "16" }`). No raw lengths, no `md`.
|
|
22
|
+
- Space comes from `gap`, never from margins on the children.
|
|
23
|
+
- It takes every `Box` prop, and the layout defaults are style props, so `display`, `flexDirection` and `alignItems` override them.
|
|
24
|
+
- Children are not wrapped, so each child is a flex item: it stretches to the column's width unless `alignItems` says otherwise.
|
|
25
|
+
- The consumer's `className` comes last and `style` is passed through; `ref` is a plain prop typed for the `as` element.
|
|
26
|
+
|
|
27
|
+
## Do
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<Stack gap="24">
|
|
31
|
+
<Heading>Settings</Heading>
|
|
32
|
+
<Text>Choose how the app looks.</Text>
|
|
33
|
+
</Stack>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<Stack as="ul" gap="8" aria-label="Recent files">
|
|
38
|
+
<li>…</li>
|
|
39
|
+
</Stack>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Don't
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// Margins on children instead of gap; and `gap` is required.
|
|
46
|
+
<Stack>
|
|
47
|
+
<Box marginBlockEnd="16">…</Box>
|
|
48
|
+
</Stack>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// A wrapper only to space children adds a needless element; Stack spaces them directly.
|
|
53
|
+
<Stack gap="16">
|
|
54
|
+
<div>
|
|
55
|
+
<Text>Name</Text>
|
|
56
|
+
</div>
|
|
57
|
+
<div>
|
|
58
|
+
<Text>Email</Text>
|
|
59
|
+
</div>
|
|
60
|
+
</Stack>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Accessibility
|
|
64
|
+
|
|
65
|
+
- Stack adds no role or ARIA. Pick the element with `as`: `ul` with `li` children for a list, `section` with a label for a region.
|
|
66
|
+
- Visual order is source order; do not use `flexDirection="column-reverse"` to reorder meaningful content, since reading and focus order follow the source.
|
|
67
|
+
|
|
68
|
+
## Props
|
|
69
|
+
|
|
70
|
+
Generated from the TypeScript types.
|
|
71
|
+
|
|
72
|
+
### Stack
|
|
73
|
+
|
|
74
|
+
| Prop | Type | Required | Description |
|
|
75
|
+
| --- | --- | --- | --- |
|
|
76
|
+
| `as` | `ElementType` | no | |
|
|
77
|
+
| `gap` | `"0" \| "1" \| "2" \| "4" \| "6" \| "8" \| "12" \| "16" \| "20" \| "24" \| "32" \| "40" \| "48" \| "64" \| "80" \| "96"` (responsive) | yes | |
|
|
78
|
+
|
|
79
|
+
Also accepts every `<div>` attribute (change the element with `as`) and the style props (see `manifest.json` `styleProps`).
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Tabs
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Switches between views that share one place on the page. The parts are `Tabs` (the root, a flex column; a row when vertical), `Tabs.List` (the `tablist`), `Tabs.Tab` (a `tab` button on `Clickable`) and `Tabs.Panel` (the `tabpanel`). Built on Base UI `Tabs` (ADR 0003, 0046): it owns the selection, one tab stop with arrow-key roving, `aria-selected`, and the ids that tie each tab to its panel. The selected tab shows an accent bar that fades and grows in (ADR 0049).
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- Peer views of one thing (a message's Details, Activity and Files), where one shows at a time and switching keeps the page.
|
|
10
|
+
- Filtering one list into a few fixed buckets (Inbox, Archive, Sent).
|
|
11
|
+
|
|
12
|
+
## When not to use
|
|
13
|
+
|
|
14
|
+
- Moving between pages or routes: use links (`Clickable as="a"`) in a `nav`. Tabs do not change the URL and the panels are not separate pages.
|
|
15
|
+
- A sequence the user must go through in order: use a stepper.
|
|
16
|
+
- Two or three options that set a value in a form: use a radio group.
|
|
17
|
+
- Content that should be readable at once, or compared side by side: show it all, or use sections with headings.
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
- Name the list: `aria-label` or `aria-labelledby` on `Tabs.List`.
|
|
22
|
+
- Every `Tabs.Tab` has a `value`, and the `Tabs.Panel` with the same `value` is its panel. Values are any type; strings read best.
|
|
23
|
+
- Uncontrolled: `defaultValue` (Base UI's default is `0`, so set it). Controlled: `value` with `onValueChange`. `onValueChange` is a function, so a server component cannot pass it; control the tabs from a client component.
|
|
24
|
+
- Arrow keys move focus (Up and Down when `orientation="vertical"`), looping at the ends; Home and End jump; Enter or Space selects. Set `activateOnFocus` on the list to select on arrow keys too, only when panels render without a wait. Tab moves from the list into the panel, which is focusable.
|
|
25
|
+
- `disabled` on a tab renders `aria-disabled`: the tab stays focusable and announced but cannot be selected. Set `value` or `defaultValue` to an enabled tab; on the server Base UI cannot skip a disabled first tab.
|
|
26
|
+
- Hidden panels unmount. Set `keepMounted` on a panel to keep its state (a form, a scroll position) while hidden; it renders with `hidden`.
|
|
27
|
+
- Tab labels are text: the tab wraps its children in `Text` (`sm`, the `label` font, `medium`); pass `size`, `font`, `weight` or `leading` on the tab to change it. Do not put a `Text` inside. Labels are short and never truncated.
|
|
28
|
+
- Every part takes style props. The root's `display`, `flexDirection`, `flexWrap` (`wrap` when vertical, so the panel drops below the list when there is no room; `nowrap` when horizontal) and `gap` (`"16"`), the panel's `flexGrow` (`1`), the list's `display`, `flexWrap` (`wrap`, so tabs wrap rather than overlap at 320px or 200% text), `gap` (`"4"`) and `position` (`relative`, which the indicator needs), and the tab's `paddingInline` (`"12"`), `justifyContent` (`start`), `minBlockSize` (`"med"`, 40px), `position` and `color` (`fgMuted`) are defaults you can replace. `color` sets the resting tab; a selected or hovered tab is always `fg`. A vertical list is a column with its hairline on the inline end; that follows `orientation`, not style props. When horizontal tabs wrap onto a second row, a selected tab on the first row has its bar between the rows; keep tab sets short enough for one row where you can. Keep padding off the list's block end (inline end when vertical): the indicator sits on the hairline at the tab's edge. Never set a tab below `"xs"` (24px). `className` is a string and `style` an object (not Base UI's state functions), and the consumer's come last.
|
|
29
|
+
- The module has no hooks: a server component can render `Tabs` and its parts (the Base UI parts inside are client components). `render` is not accepted on the parts.
|
|
30
|
+
- Style off the ARIA state (`[aria-selected='true']`, `[aria-disabled='true']`, `[aria-orientation='vertical']`), never a class.
|
|
31
|
+
|
|
32
|
+
## Do
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<Tabs defaultValue="details">
|
|
36
|
+
<Tabs.List aria-label="Message">
|
|
37
|
+
<Tabs.Tab value="details">Details</Tabs.Tab>
|
|
38
|
+
<Tabs.Tab value="activity">Activity</Tabs.Tab>
|
|
39
|
+
</Tabs.List>
|
|
40
|
+
<Tabs.Panel value="details">
|
|
41
|
+
<Details />
|
|
42
|
+
</Tabs.Panel>
|
|
43
|
+
<Tabs.Panel value="activity">
|
|
44
|
+
<Activity />
|
|
45
|
+
</Tabs.Panel>
|
|
46
|
+
</Tabs>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// Controlled, from a client component.
|
|
51
|
+
<Tabs value={tab} onValueChange={setTab}>
|
|
52
|
+
…
|
|
53
|
+
</Tabs>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Don't
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// An unnamed tablist.
|
|
60
|
+
<Tabs.List>…</Tabs.List>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// Tabs as site navigation: each "tab" is a page. Use links in a nav.
|
|
65
|
+
<Tabs.Tab value="/settings" onClick={() => navigate("/settings")}>
|
|
66
|
+
Settings
|
|
67
|
+
</Tabs.Tab>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// Text is already there; this nests a second one.
|
|
72
|
+
<Tabs.Tab value="inbox">
|
|
73
|
+
<Text>Inbox</Text>
|
|
74
|
+
</Tabs.Tab>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Accessibility
|
|
78
|
+
|
|
79
|
+
- Follows the WAI-ARIA tabs pattern: `tablist`, `tab` with `aria-selected` and `aria-controls`, `tabpanel` with `aria-labelledby`. Before hydration the tabs have `tabindex="-1"` and no `aria-controls`; Base UI fills them in on the client.
|
|
80
|
+
- The selected tab is shown by a 2px accent bar (3:1 on `bg` in both modes, `Highlight` under forced colors) and by its text turning `fg`, never by color alone.
|
|
81
|
+
- The bar fades and grows in; reduced motion leaves the fade (ADR 0019, 0049).
|
|
82
|
+
- Tabs and the panel take the library's focus ring. Tabs are at least 24px tall (WCAG 2.2 target size).
|
|
83
|
+
- A disabled tab is reduced to half opacity (`GrayText` under forced colors) and stays discoverable; say why it is disabled nearby when it is not obvious.
|
|
84
|
+
|
|
85
|
+
## Props
|
|
86
|
+
|
|
87
|
+
Generated from the TypeScript types.
|
|
88
|
+
|
|
89
|
+
### Tabs
|
|
90
|
+
|
|
91
|
+
| Prop | Type | Required | Description |
|
|
92
|
+
| --- | --- | --- | --- |
|
|
93
|
+
| `defaultValue` | `any` | no | The default value. Use when the component is not controlled. When the value is `null`, no Tab will be active. |
|
|
94
|
+
| `onValueChange` | `((value: any, eventDetails: TabsRootChangeEventDetails) => void)` | no | Callback invoked when new value is being set. The event `reason` is `'none'` for user-initiated changes, such as a click or keyboard navigation; `'initial'` for the first automatic selection or fallback in uncontrolled roots when `defaultValue` is omitted or `undefined`, including when the implicit initial value is disabled or missing; `'disabled'` for automatic fallback when the selected tab becomes disabled in uncontrolled roots; or `'missing'` for automatic fallback when the selected tab is removed, or when an explicit `defaultValue` never matches a mounted tab in uncontrolled roots. For automatic changes, the selected value can be `null` when no enabled Tab is available as a fallback. Automatic changes cannot be canceled; calling `eventDetails.cancel()` for `'initial'`, `'disabled'`, or `'missing'` has no effect. |
|
|
95
|
+
| `orientation` | `"horizontal" \| "vertical"` | no | The component orientation (layout flow direction). |
|
|
96
|
+
| `value` | `any` | no | The value of the currently active `Tab`. Use when the component is controlled. When the value is `null`, no Tab will be active. |
|
|
97
|
+
|
|
98
|
+
Also accepts the style props (see `manifest.json` `styleProps`).
|
|
99
|
+
|
|
100
|
+
### Tabs.List
|
|
101
|
+
|
|
102
|
+
| Prop | Type | Required | Description |
|
|
103
|
+
| --- | --- | --- | --- |
|
|
104
|
+
| `activateOnFocus` | `boolean` | no | Whether to automatically change the active tab on arrow key focus. Otherwise, tabs will be activated using <kbd>Enter</kbd> or <kbd>Space</kbd> key press. |
|
|
105
|
+
| `loopFocus` | `boolean` | no | Whether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys. |
|
|
106
|
+
|
|
107
|
+
Also accepts the style props (see `manifest.json` `styleProps`).
|
|
108
|
+
|
|
109
|
+
### Tabs.Panel
|
|
110
|
+
|
|
111
|
+
| Prop | Type | Required | Description |
|
|
112
|
+
| --- | --- | --- | --- |
|
|
113
|
+
| `keepMounted` | `boolean` | no | Whether to keep the HTML element in the DOM while the panel is hidden. |
|
|
114
|
+
| `value` | `any` | yes | The value of the TabPanel. It will be shown when the Tab with the corresponding value is active. |
|
|
115
|
+
|
|
116
|
+
Also accepts the style props (see `manifest.json` `styleProps`).
|
|
117
|
+
|
|
118
|
+
### Tabs.Tab
|
|
119
|
+
|
|
120
|
+
| Prop | Type | Required | Description |
|
|
121
|
+
| --- | --- | --- | --- |
|
|
122
|
+
| `disabled` | `boolean` | no | Whether the Tab is disabled. If a first Tab on a `<Tabs.List>` is disabled, it won't initially be selected. Instead, the next enabled Tab will be selected. However, it does not work like this during server-side rendering, as it is not known during pre-rendering which Tabs are disabled. To work around it, ensure that `defaultValue` or `value` on `<Tabs.Root>` is set to an enabled Tab's value. |
|
|
123
|
+
| `font` | `"body" \| "display" \| "heading" \| "label" \| "mono"` | no | |
|
|
124
|
+
| `leading` | `"lg" \| "med" \| "sm"` | no | |
|
|
125
|
+
| `nativeButton` | `boolean` | no | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
|
|
126
|
+
| `size` | `"2xl" \| "3xl" \| "4xl" \| "lg" \| "med" \| "sm" \| "xl" \| "xs"` | no | |
|
|
127
|
+
| `value` | `any` | yes | The value of the Tab. |
|
|
128
|
+
| `weight` | `"bold" \| "medium" \| "regular" \| "semibold"` | no | |
|
|
129
|
+
|
|
130
|
+
Also accepts the style props (see `manifest.json` `styleProps`).
|