@antadesign/anta 0.3.18 → 0.3.19
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/README.md +38 -242
- package/dist/components/Input.d.ts +4 -1
- package/dist/components/Input.js +2 -0
- package/dist/components/InputDate.css +1 -0
- package/dist/components/InputDate.d.ts +1 -0
- package/dist/components/InputDate.js +5 -5
- package/dist/components/Menu.d.ts +5 -1
- package/dist/components/Menu.js +3 -2
- package/dist/components/Select.d.ts +1 -0
- package/dist/components/Select.js +9 -13
- package/dist/components/SelectFaceted.d.ts +1 -0
- package/dist/components/SelectFaceted.js +29 -23
- package/dist/components/select-parts.css +1 -0
- package/dist/elements/a-input.css +1 -1
- package/dist/elements/a-menu.js +12 -0
- package/dist/elements/a-slider.css +1 -1
- package/dist/elements/a-tabs.css +1 -1
- package/dist/elements/a-tabs.d.ts +3 -0
- package/dist/elements/a-tabs.js +40 -1
- package/dist/general_types.d.ts +2 -0
- package/dist/jsx-runtime.d.ts +9 -0
- package/docs/accessibility.md +34 -0
- package/docs/changelog.md +551 -0
- package/docs/colors.md +42 -0
- package/docs/comparison.md +28 -0
- package/docs/components/banner.md +267 -0
- package/docs/components/button.md +603 -0
- package/docs/components/card.md +322 -0
- package/docs/components/checkbox.md +297 -0
- package/docs/components/dialog.md +314 -0
- package/docs/components/expander.md +529 -0
- package/docs/components/icon.md +155 -0
- package/docs/components/input-autocomplete.md +101 -0
- package/docs/components/input-date.md +406 -0
- package/docs/components/input-time.md +174 -0
- package/docs/components/input.md +610 -0
- package/docs/components/loader.md +139 -0
- package/docs/components/menu.md +447 -0
- package/docs/components/progress.md +182 -0
- package/docs/components/radio.md +369 -0
- package/docs/components/select-faceted.md +456 -0
- package/docs/components/select.md +931 -0
- package/docs/components/slider.md +142 -0
- package/docs/components/switch.md +198 -0
- package/docs/components/tabs.md +471 -0
- package/docs/components/tag.md +185 -0
- package/docs/components/text.md +148 -0
- package/docs/components/title.md +146 -0
- package/docs/components/toaster.md +313 -0
- package/docs/components/tooltip.md +177 -0
- package/docs/credits.md +22 -0
- package/docs/index.md +52 -0
- package/docs/install-config.md +240 -0
- package/docs/normalization.md +105 -0
- package/docs/overview.md +10 -0
- package/docs/packages/stickers.md +187 -0
- package/docs/packages/table.md +153 -0
- package/docs/theming.md +13 -0
- package/package.json +10 -6
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
# Tabs
|
|
2
|
+
|
|
3
|
+
A tablist for switching between views. The strip is data-driven: pass an
|
|
4
|
+
`options` array (like [RadioGroup](./radio.md)), each entry a unique `value` plus a
|
|
5
|
+
`label` (or `children`) and optional `icon` / `iconTrailing`. That alone is a
|
|
6
|
+
selectable strip that emits the chosen value; add matching `<TabPanel>` children
|
|
7
|
+
(see [Panels](#panels)) to show and hide the content too.
|
|
8
|
+
|
|
9
|
+
## Priority
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<Tabs priority="primary" defaultValue="a">…</Tabs> {/* default */}
|
|
13
|
+
<Tabs priority="secondary" defaultValue="a">…</Tabs>
|
|
14
|
+
<Tabs priority="tertiary" defaultValue="a">…</Tabs>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`priority` sets the selected-tab indicator:
|
|
18
|
+
|
|
19
|
+
- **`primary`** (default): a pill on a track.
|
|
20
|
+
- **`secondary`**: a filled tab, no track.
|
|
21
|
+
- **`tertiary`**: an underline.
|
|
22
|
+
|
|
23
|
+
## Tones
|
|
24
|
+
|
|
25
|
+
Six named tones: `neutral` (default), `brand`, `info`, `success`, `warning`,
|
|
26
|
+
`critical`. Any CSS colour works for a one-off custom tone. The tone runs through the
|
|
27
|
+
strip's labels, indicator, and track at every priority. Rows are tones (the last is a
|
|
28
|
+
custom colour); columns are priorities.
|
|
29
|
+
|
|
30
|
+
**Primary**
|
|
31
|
+
|
|
32
|
+
**Secondary**
|
|
33
|
+
|
|
34
|
+
**Tertiary**
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<Tabs tone="brand" priority="secondary" defaultValue="a">…</Tabs>
|
|
38
|
+
<Tabs tone="#e0457b" priority="tertiary" defaultValue="a">…</Tabs> {/* any CSS colour */}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Per-tab tone.** A `tone` on an individual option overrides the strip's tone for that one
|
|
42
|
+
tab: it colours the label and icons in every mode, and the indicator when it's the active tab.
|
|
43
|
+
A named tone colours the sliding indicator too; a custom literal colour tones the label
|
|
44
|
+
everywhere but the indicator only under `noslide` (the single shared sliding element can't
|
|
45
|
+
read a descendant's colour).
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<Tabs
|
|
49
|
+
defaultValue="overview"
|
|
50
|
+
label="Review queue"
|
|
51
|
+
options={[
|
|
52
|
+
{ value: 'overview', label: 'Overview' },
|
|
53
|
+
{ value: 'approved', label: 'Approved', icon: 'circle-check', tone: 'success' },
|
|
54
|
+
{ value: 'flagged', label: 'Flagged', tone: 'warning' },
|
|
55
|
+
{ value: 'rejected', label: 'Rejected', tone: 'critical' },
|
|
56
|
+
]}
|
|
57
|
+
/>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Size
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<Tabs size="small" defaultValue="a">…</Tabs>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Three sizes reuse [Button](./button.md)'s type scale, so a strip lines up with
|
|
67
|
+
same-size buttons and inputs: `small`, `medium` (default), `large`. Icons scale
|
|
68
|
+
with the tab.
|
|
69
|
+
|
|
70
|
+
## Orientation
|
|
71
|
+
|
|
72
|
+
`horizontal` (default) lays the tabs in a row; `vertical` stacks them into a column.
|
|
73
|
+
The arrow keys follow the axis: `←`/`→` when horizontal, `↑`/`↓` when vertical. To
|
|
74
|
+
put panels **beside** a vertical strip, wrap `<Tabs>` in your own flex row (the strip
|
|
75
|
+
and panels are siblings; see [Panels](#panels)). Vertical works at every priority:
|
|
76
|
+
|
|
77
|
+
**Primary**
|
|
78
|
+
|
|
79
|
+
**Secondary**
|
|
80
|
+
|
|
81
|
+
**Tertiary**
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<Tabs
|
|
85
|
+
orientation="vertical"
|
|
86
|
+
priority="tertiary"
|
|
87
|
+
defaultValue="general"
|
|
88
|
+
options={[
|
|
89
|
+
{ value: 'general', label: 'General' },
|
|
90
|
+
{ value: 'members', label: 'Members' },
|
|
91
|
+
{ value: 'integrations', label: 'Integrations' },
|
|
92
|
+
]}
|
|
93
|
+
/>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Icons & content
|
|
97
|
+
|
|
98
|
+
A tab option takes a leading **`icon`** and a trailing **`iconTrailing`**; here the
|
|
99
|
+
trailing dot flags an unsaved file. For more than text, give the option **`children`**
|
|
100
|
+
instead of `label`, like a [Tag](./tag.md) counter.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
<Tabs
|
|
104
|
+
defaultValue="app"
|
|
105
|
+
label="Open files"
|
|
106
|
+
options={[
|
|
107
|
+
{ value: 'app', icon: 'braces', children: <>app.tsx <Tag size="small" value="2" /></> },
|
|
108
|
+
{ value: 'readme', label: 'README.md', icon: 'file' },
|
|
109
|
+
{ value: 'styles', icon: 'file', iconTrailing: 'circle-small-solid', children: 'styles.css' },
|
|
110
|
+
]}
|
|
111
|
+
/>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Overflow
|
|
115
|
+
|
|
116
|
+
When the tabs don't fit, their labels truncate with an ellipsis; the strip never
|
|
117
|
+
widens the page. To scroll or wrap labels instead, see [Styling](#styling).
|
|
118
|
+
|
|
119
|
+
Set an option's **`tooltip`** and a clipped tab reveals its full label on hover; tabs
|
|
120
|
+
that fit show nothing. It renders as a `truncatedOnly` [Tooltip](./tooltip.md#show-only-when-truncated)
|
|
121
|
+
anchored to the tab, so it shows only when the label ellipsizes. Hover the truncated
|
|
122
|
+
tabs above to see it.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<Tabs
|
|
126
|
+
defaultValue="overview"
|
|
127
|
+
label="Sections"
|
|
128
|
+
options={sections.map(({ value, label }) => ({
|
|
129
|
+
value,
|
|
130
|
+
label,
|
|
131
|
+
tooltip: label,
|
|
132
|
+
}))}
|
|
133
|
+
/>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Panels
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
{/* The strip and panels are flat siblings — you arrange them. A flex column gaps
|
|
140
|
+
them; the strip centres itself (a <Tabs> `style` lands on <a-tabs>). */}
|
|
141
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
142
|
+
<Tabs
|
|
143
|
+
defaultValue="account"
|
|
144
|
+
label="Settings"
|
|
145
|
+
style={{ alignSelf: 'center' }}
|
|
146
|
+
options={[
|
|
147
|
+
{ value: 'account', label: 'Account', icon: 'home' },
|
|
148
|
+
{ value: 'security', label: 'Security' },
|
|
149
|
+
{ value: 'billing', label: 'Billing' },
|
|
150
|
+
]}
|
|
151
|
+
>
|
|
152
|
+
<TabPanel value="account"><AccountForm /></TabPanel>
|
|
153
|
+
<TabPanel value="security"><SecurityForm /></TabPanel>
|
|
154
|
+
<TabPanel value="billing"><BillingForm /></TabPanel>
|
|
155
|
+
</Tabs>
|
|
156
|
+
</div>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Add a `<TabPanel value="…">` child for each option; each panel shows itself when
|
|
160
|
+
its value is active. The panel names its tab as its accessible label
|
|
161
|
+
(`aria-labelledby`, off-DOM). Panel content is arbitrary React, so it renders normally.
|
|
162
|
+
|
|
163
|
+
Panels are **optional**. With none, `Tabs` is just the strip: listen to
|
|
164
|
+
`onStateChange` / `onValueChange` and render the content yourself.
|
|
165
|
+
|
|
166
|
+
The strip renders from the `options` array; each `<TabPanel>` renders a
|
|
167
|
+
self-managing `<a-tabpanel>`. `Tabs` **never reads or toggles the panels**: there's
|
|
168
|
+
no child introspection, so it renders the same in static SSR and works with any
|
|
169
|
+
renderer. There is **no wrapper element**: the JSX becomes the strip followed by the
|
|
170
|
+
panels as flat **siblings**, and `className` / `id` / `style` / `...rest` land on the
|
|
171
|
+
strip. Arranging them is up to you (see below):
|
|
172
|
+
|
|
173
|
+
```html
|
|
174
|
+
<a-tabs role="tablist">
|
|
175
|
+
<a-tab role="tab" value="account" tabindex="0"><a-tab-label>Account</a-tab-label></a-tab>
|
|
176
|
+
…
|
|
177
|
+
</a-tabs>
|
|
178
|
+
<a-tabpanel role="tabpanel" value="account">…</a-tabpanel> <!-- active: shown -->
|
|
179
|
+
<a-tabpanel role="tabpanel" value="security">…</a-tabpanel> <!-- inactive: display:none -->
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
A horizontal strip stacks above its panels in normal flow with no extra markup; for a
|
|
183
|
+
vertical strip beside its panels, wrap `<Tabs>` in your own flex row. The panels find
|
|
184
|
+
the strip as a sibling under the same parent (`a-tabpanel` → `:scope > a-tabs`), so
|
|
185
|
+
keep them together; to split them into separate layout regions, drive a
|
|
186
|
+
controlled `value` and render the panels yourself.
|
|
187
|
+
|
|
188
|
+
Each `<a-tabpanel>` sits **beside** `<a-tabs>` (its sibling, never inside it, so a
|
|
189
|
+
panel never lands in the primary track). It reads the active value from `<a-tabs>`
|
|
190
|
+
and shows/hides **itself** via its own off-DOM `:state(active)`; nothing writes
|
|
191
|
+
`hidden` / `inert` on it. The panel↔tab link is off-DOM too
|
|
192
|
+
(`internals.ariaLabelledByElements`), so no `id` wiring is needed. No web component
|
|
193
|
+
writes the light DOM, only your JSX does, which keeps a worker-thread reactive engine
|
|
194
|
+
in sync.
|
|
195
|
+
|
|
196
|
+
A `<TabPanel>` stays mounted while inactive (its DOM, form values, and scroll
|
|
197
|
+
survive tab switches); `hideMode` picks how it hides:
|
|
198
|
+
|
|
199
|
+
- **`display`** (default): `display:none`; leaves the tab order + a11y tree.
|
|
200
|
+
- **`visibility`**: `visibility:hidden`, keeping its layout box (to avoid reflow
|
|
201
|
+
or to measure a hidden panel).
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
<TabPanel value="b" hideMode="visibility"><Chart /></TabPanel>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
To **not render** an inactive panel at all (unmount it to reset its state or defer an
|
|
208
|
+
expensive subtree, or mount it lazily on first open), drive selection with a
|
|
209
|
+
controlled `value` and render the panels yourself. `Tabs` passes panels straight
|
|
210
|
+
through, so you decide what's mounted:
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
const [tab, setTab] = useState('a')
|
|
214
|
+
|
|
215
|
+
// Unmount inactive — only the open panel exists:
|
|
216
|
+
<Tabs value={tab} onStateChange={(_e, { next }) => next && setTab(next)} options={…}>
|
|
217
|
+
{tab === 'a' && <TabPanel value="a"><Expensive /></TabPanel>}
|
|
218
|
+
{tab === 'b' && <TabPanel value="b"><Form /></TabPanel>}
|
|
219
|
+
</Tabs>
|
|
220
|
+
|
|
221
|
+
// Lazy — mount on first open, keep after (track a "seen" set in state):
|
|
222
|
+
{seen.has('b') && <TabPanel value="b"><Heavy /></TabPanel>}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The same controlled pattern places the strip and panels in **different layout
|
|
226
|
+
regions**: render `<Tabs options={…} value onStateChange>` in one place and your
|
|
227
|
+
content wherever you like, switching on the value yourself.
|
|
228
|
+
|
|
229
|
+
## Controlled vs. uncontrolled
|
|
230
|
+
|
|
231
|
+
The active tab is named by a single **value string**, the `value` of one of your
|
|
232
|
+
`options`. Whatever value is active, `Tabs` marks that tab selected and reveals the
|
|
233
|
+
`<TabPanel value="…">` that shares it (if any). You choose who owns that value:
|
|
234
|
+
|
|
235
|
+
**Uncontrolled.** Pass `defaultValue` (an option's `value`) and let `Tabs` own the
|
|
236
|
+
active tab from there:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
<Tabs
|
|
240
|
+
defaultValue="overview"
|
|
241
|
+
options={[
|
|
242
|
+
{ value: 'overview', label: 'Overview' },
|
|
243
|
+
{ value: 'activity', label: 'Activity' },
|
|
244
|
+
]}
|
|
245
|
+
/>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Controlled.** Pass `value` (an option's `value`) + `onStateChange` and own it
|
|
249
|
+
yourself. `onStateChange` fires *before* the pick applies: set `value` to the new tab
|
|
250
|
+
to accept, or do nothing to reject.
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
const [tab, setTab] = useState('overview')
|
|
254
|
+
|
|
255
|
+
<Tabs
|
|
256
|
+
value={tab}
|
|
257
|
+
onStateChange={(e, { next }) => setTab(next)}
|
|
258
|
+
options={[
|
|
259
|
+
{ value: 'overview', label: 'Overview' },
|
|
260
|
+
{ value: 'activity', label: 'Activity' },
|
|
261
|
+
]}
|
|
262
|
+
/>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Every enabled tab is in the tab order; `Tab` / `Shift`+`Tab` step through them.
|
|
266
|
+
The arrow keys also move between enabled tabs (wrapping at the ends), `Home` /
|
|
267
|
+
`End` jump to the first / last, and `Space` / `Enter` activate the focused tab;
|
|
268
|
+
arrow / `Home` / `End` navigation activates as it moves (selection follows focus).
|
|
269
|
+
Disabled tabs are skipped and dropped from the tab order.
|
|
270
|
+
|
|
271
|
+
Pass `label` for the tablist's accessible name. Each `<TabPanel>` names its tab as
|
|
272
|
+
its accessible label (`aria-labelledby`), set off-DOM so nothing writes the panel's
|
|
273
|
+
attributes.
|
|
274
|
+
|
|
275
|
+
`Tabs` reports the picked value and leaves navigation to the app, so a strip can
|
|
276
|
+
switch routes as readily as panels. Treat each `value` as a path: control `value`
|
|
277
|
+
from the current location and navigate in `onStateChange` (the pick fires *on click*;
|
|
278
|
+
`onValueChange` waits for `value` to change, which for a route happens only after
|
|
279
|
+
you've already navigated). Skip the `<TabPanel>`s, since the router renders the view.
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
<Tabs
|
|
283
|
+
value={pathname}
|
|
284
|
+
onStateChange={(_e, { next }) => next && navigation.navigate(next)}
|
|
285
|
+
options={[
|
|
286
|
+
{ value: '/settings/profile', label: 'Profile' },
|
|
287
|
+
{ value: '/settings/billing', label: 'Billing' },
|
|
288
|
+
]}
|
|
289
|
+
/>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Swap `navigation.navigate` for whatever the app uses (`router.push`, Astro's
|
|
293
|
+
`navigate`, `location.assign`). Controlling `value` from the route keeps the right
|
|
294
|
+
tab active through back/forward and deep links. To guard a switch (unsaved edits),
|
|
295
|
+
veto it in `onStateChange` with `event.preventDefault()`.
|
|
296
|
+
|
|
297
|
+
## Events
|
|
298
|
+
|
|
299
|
+
Three callbacks, in firing order:
|
|
300
|
+
|
|
301
|
+
| Callback | When | Cancelable | Payload |
|
|
302
|
+
|---|---|---|---|
|
|
303
|
+
| `onStateChange(e, { next, prev })` | **before** a pick applies | ✅ | next / prev value |
|
|
304
|
+
| `onChange(e)` | **after** selection applies | — | native `change` event |
|
|
305
|
+
| `onValueChange(e, { value })` | **after** selection applies | — | `{ value }` |
|
|
306
|
+
|
|
307
|
+
Use **`onStateChange`** when you control `value`: it fires on the user's pick before
|
|
308
|
+
anything applies, so it's your chance to set the new `value`. Applying it (or the
|
|
309
|
+
element self-applying, uncontrolled) then fires **`onChange`** / **`onValueChange`**,
|
|
310
|
+
so those suit an uncontrolled strip reacting to its own pick. **`onFocus`** /
|
|
311
|
+
**`onBlur`** report focus entering and leaving the strip (wired to `focusin` /
|
|
312
|
+
`focusout`, since focus lands on a tab).
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
// Uncontrolled: the strip owns selection, so onValueChange fires on each pick.
|
|
316
|
+
<Tabs defaultValue="a" onValueChange={(_e, { value }) => route(value)}>…</Tabs>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
| Prop | Type | Default | Description |
|
|
320
|
+
|------|------|---------|-------------|
|
|
321
|
+
| `children?` | ReactNode | — | Optional `<TabPanel value="…">` panels, one per tab value. Each is a
|
|
322
|
+
self-managing `<a-tabpanel>` that shows itself when its `value` is the active
|
|
323
|
+
tab. Omit them to use `Tabs` as a bare selectable strip. To place panels in a
|
|
324
|
+
different layout region, or to unmount an inactive panel, drive selection with
|
|
325
|
+
a controlled `value` and render the content yourself (see the docs). |
|
|
326
|
+
| `defaultValue?` | string | — | Initial active value for the uncontrolled case. After first render `Tabs`
|
|
327
|
+
owns selection itself. |
|
|
328
|
+
| `disabled?` | boolean | — | Disable the whole strip. |
|
|
329
|
+
| `label?` | string | — | Accessible name for the tablist (`aria-label`). |
|
|
330
|
+
| `noslide?` | boolean | — | Disable the sliding indicator. By default the selected-tab indicator animates
|
|
331
|
+
between tabs (a single rectangle, via CSS anchor positioning); `noslide` paints it
|
|
332
|
+
per tab so it snaps with no movement. (Browsers without anchor positioning get that
|
|
333
|
+
per-tab paint automatically — `noslide` is the explicit opt-out.) |
|
|
334
|
+
| `onBlur?` | (event) => void | — | Focus left the strip entirely — wired to `focusout`. |
|
|
335
|
+
| `onChange?` | (event) => void | — | Fired *after* the active tab changes — a native `change` event. |
|
|
336
|
+
| `onFocus?` | (event) => void | — | Focus entered the strip (any tab) — wired to `focusin` (focus lands on a tab,
|
|
337
|
+
not the tablist). |
|
|
338
|
+
| `onStateChange?` | (event, detail) => void | — | Fired whenever the active tab changes — event-first. `detail` is
|
|
339
|
+
`{ next, prev }` (values; `null` = none). Cancelable: `event.preventDefault()`
|
|
340
|
+
vetoes it (uncontrolled), or in controlled mode answer by updating `value`. |
|
|
341
|
+
| `onValueChange?` | (event, attrs) => void | — | Like `onChange`, but with a `{ value }` snapshot as the 2nd argument. |
|
|
342
|
+
| `options?` | TabOption[] | — | The tabs, as a data array (the strip's single source). Each entry is a
|
|
343
|
+
`TabOption` (`value`, `label` or `children`, `icon`, `iconTrailing`, `tone`,
|
|
344
|
+
`disabled`, `round`). |
|
|
345
|
+
| `orientation?` | 'horizontal' \| 'vertical' | 'horizontal' | Layout + arrow-key axis. Horizontal ellipsizes labels when tabs overflow (scroll
|
|
346
|
+
is opt-in via CSS); vertical stacks them. |
|
|
347
|
+
| `priority?` | 'primary' \| 'secondary' \| 'tertiary' | 'primary' | Visual priority. `primary` is the raised pill on a recessed track (the
|
|
348
|
+
segmented-control look); `secondary` keeps that sizing but drops the track, marking
|
|
349
|
+
the selected tab with a subtle active background fill; `tertiary` is a bottom-underline
|
|
350
|
+
indicator under the selected tab (no track, no rest line). `tone` colours `secondary` +
|
|
351
|
+
`tertiary`; `primary` stays neutral. |
|
|
352
|
+
| `round?` | boolean \| number \| string | — | Fully-round the tabs and the sliding indicator (and the primary track
|
|
353
|
+
well). Applies strip-wide; a single tab's `round` rounds just that tab. A
|
|
354
|
+
`number` (px) or CSS length string sets a custom radius on the top-level
|
|
355
|
+
track well only — the tab pills + indicator stay fully round. |
|
|
356
|
+
| `size?` | 'small' \| 'medium' \| 'large' | 'medium' | Size — small 24px · medium 28px · large 32px tall, matching Button's scale (the tab's
|
|
357
|
+
label leading runs a touch tighter, offset by 1px more block padding per side). |
|
|
358
|
+
| `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Tone applied to the selected indicator/label, or any literal CSS color for a
|
|
359
|
+
one-off custom tone (derived in oklch). Named tones track light/dark. |
|
|
360
|
+
| `value?` | string | — | Controlled active value — the tab `value` to mark selected (and, when a
|
|
361
|
+
`<TabPanel value="…">` shares it, the panel to reveal). When set, you own
|
|
362
|
+
selection: the strip renders exactly what this says, and a user pick only
|
|
363
|
+
*requests* a change via `onStateChange` — apply it by updating this prop.
|
|
364
|
+
Leave undefined (and use `defaultValue`) for uncontrolled. |
|
|
365
|
+
|
|
366
|
+
Use this when you are not using the React or Preact wrapper and a native HTML control does not fit: construct the equivalent Anta web component from the elements below.
|
|
367
|
+
|
|
368
|
+
Keep the tab strip and panels as siblings under one parent. Panels read the active
|
|
369
|
+
value from `<a-tabs>`.
|
|
370
|
+
|
|
371
|
+
```html
|
|
372
|
+
<div>
|
|
373
|
+
<a-tabs role="tablist" default-state="account">
|
|
374
|
+
<a-tab role="tab" value="account" tabindex="0"><a-tab-label>Account</a-tab-label></a-tab>
|
|
375
|
+
<a-tab role="tab" value="security" tabindex="-1"><a-tab-label>Security</a-tab-label></a-tab>
|
|
376
|
+
</a-tabs>
|
|
377
|
+
<a-tabpanel role="tabpanel" value="account">Account settings</a-tabpanel>
|
|
378
|
+
<a-tabpanel role="tabpanel" value="security">Security settings</a-tabpanel>
|
|
379
|
+
</div>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Reach for the props first: **`tone`**, **`size`**, **`priority`**. The focus ring is
|
|
383
|
+
the global `--focus-ring` (see [Colors](../colors.md#focus-ring)).
|
|
384
|
+
|
|
385
|
+
For anything else the strip is light-DOM: `<a-tabs>` the tablist, each `<a-tab>` a
|
|
386
|
+
tab (its text wrapped in `<a-tab-label>`), `<a-tabpanel>` a panel, and the moving
|
|
387
|
+
indicator is `<a-tabs>`'s `::before`. Target them with ordinary CSS (an un-layered rule
|
|
388
|
+
beats `@layer anta` without `!important`) and reach for real properties
|
|
389
|
+
(`border-radius`, `padding`, `background`, …), **not** the internal `--*` output tokens.
|
|
390
|
+
The demo classes below (`.square-tabs`, `.glow-tabs`, …) are just hooks; swap your own.
|
|
391
|
+
|
|
392
|
+
**Roomier track.** The strip hugs its tabs by default (0 padding), so it's exactly a
|
|
393
|
+
same-size button tall. For a gap around the selected pill (the classic "well" look), add
|
|
394
|
+
`padding` and bump `border-radius` by the same amount, so the outer corner stays concentric
|
|
395
|
+
with the tabs' 4px. The strip grows 2×the padding taller.
|
|
396
|
+
|
|
397
|
+
```css
|
|
398
|
+
a-tabs.roomy-1 { padding: 1px; border-radius: 5px; } /* 4 + 1 */
|
|
399
|
+
a-tabs.roomy-3 { padding: 3px; border-radius: 7px; } /* 4 + 3 */
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Squarer primary.** Square corners, heavier labels, and a roomier `4px` track with no
|
|
403
|
+
border or ring on the strip (`box-shadow: none` drops the default track ring) — just a 1px
|
|
404
|
+
square ring on the raised pill. The track is `<a-tabs>`, the sliding pill its `::before`:
|
|
405
|
+
|
|
406
|
+
```css
|
|
407
|
+
a-tabs.square-tabs { border-radius: 0; padding: 4px; box-shadow: none; } /* no track border / ring */
|
|
408
|
+
.square-tabs a-tab { border-radius: 0; font-weight: 600; }
|
|
409
|
+
a-tabs.square-tabs::before { border-radius: 0; box-shadow: 0 0 0 1px light-dark(#cfcfd6, #46464e); } /* 1px square pill ring */
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Tertiary glowing underline.** Recolour the sliding underline and add a radial highlight
|
|
413
|
+
rising from it. The slider is a full-cover box whose `border-bottom` is the line, so a
|
|
414
|
+
`background` gradient on it fills the tab behind the label. Because it's one element, the
|
|
415
|
+
glow **slides with the line** between tabs, with no `noslide` and no separate per-tab
|
|
416
|
+
background.
|
|
417
|
+
|
|
418
|
+
```css
|
|
419
|
+
/* The class is on the <a-tabs> element itself, so target a-tabs.glow-tabs (not a descendant).
|
|
420
|
+
The tertiary slider is a full-cover box with a border-bottom line — recolour the border and
|
|
421
|
+
fill it with a gradient; both slide together. Where CSS anchor positioning isn't supported
|
|
422
|
+
there's no slider — tertiary falls back to the per-tab ::after underline (a plain line). */
|
|
423
|
+
a-tabs.glow-tabs::before {
|
|
424
|
+
border-bottom-color: light-dark(#8067ff, #907aff); /* recolour the sliding line */
|
|
425
|
+
background: /* highlight rising from the line, slides with it */
|
|
426
|
+
radial-gradient(80% 100% at 50% 100%,
|
|
427
|
+
color-mix(in oklch, light-dark(#8067ff, #907aff) 30%, transparent), transparent 70%);
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**Scroll instead of ellipsizing.** Keep full labels and let the strip scroll
|
|
432
|
+
horizontally: pin each tab to its content width (`flex: none`) and make the strip
|
|
433
|
+
scrollable.
|
|
434
|
+
|
|
435
|
+
```css
|
|
436
|
+
.scroll-tabs a-tabs { overflow-x: auto; scrollbar-width: thin; }
|
|
437
|
+
.scroll-tabs a-tab { flex: none; }
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Equal-width tabs.** By default tabs are sized to their labels (and compress only
|
|
441
|
+
when the strip overflows). For a segmented-control look where every tab is the same
|
|
442
|
+
width, let them fill the strip from a zero basis:
|
|
443
|
+
|
|
444
|
+
```css
|
|
445
|
+
.equal-tabs a-tabs { width: 100%; }
|
|
446
|
+
.equal-tabs a-tab { flex: 1 1 0; max-width: none; }
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**Wrap labels.** Let long labels wrap so the tabs grow taller instead of truncating:
|
|
450
|
+
|
|
451
|
+
```css
|
|
452
|
+
.wrap-tabs a-tab,
|
|
453
|
+
.wrap-tabs a-tab-label { white-space: normal; text-overflow: clip; overflow: visible; text-align: center; }
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**Disable the slide.** By default the selected-tab indicator slides between tabs (a
|
|
457
|
+
single rectangle, via CSS anchor positioning). Pass `noslide` to paint it on each tab
|
|
458
|
+
instead, so the highlight snaps with no movement. It's also the automatic fallback
|
|
459
|
+
where anchor positioning isn't supported.
|
|
460
|
+
|
|
461
|
+
```tsx
|
|
462
|
+
<Tabs noslide defaultValue="a">…</Tabs>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Fully-rounded pill.** The built-in **`round`** prop rounds the track, every tab, **and**
|
|
466
|
+
the sliding pill to `999px` in one flag, no CSS needed (it drives `--tab-radius`, which feeds
|
|
467
|
+
the tabs, the moving indicator, and the primary track well):
|
|
468
|
+
|
|
469
|
+
```tsx
|
|
470
|
+
<Tabs round defaultValue="a" label="Sections">…</Tabs> {/* pill track + tabs + indicator */}
|
|
471
|
+
```
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Tag
|
|
2
|
+
|
|
3
|
+
A compact pill for status, labels, and metadata. The `Tag` JSX wrapper
|
|
4
|
+
renders an `<a-tag>` styled tag — there's no JavaScript, so tone, size,
|
|
5
|
+
and case are all plain attributes and the styling is identical whether
|
|
6
|
+
you use the wrapper or author the element by hand. Content is composed
|
|
7
|
+
from `icon`, `label`, `value`, and `iconTrailing` props, the same way
|
|
8
|
+
`Button` works.
|
|
9
|
+
|
|
10
|
+
## Priority
|
|
11
|
+
|
|
12
|
+
`priority` sets the emphasis. `secondary` (the default) is the subtle
|
|
13
|
+
alpha-tint fill; `primary` is a solid fill with white text; `tertiary` is
|
|
14
|
+
a transparent outline. It composes with every tone — named or custom —
|
|
15
|
+
and tracks dark mode.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<Tag allcaps priority="primary" tone="brand" label="Primary" />
|
|
19
|
+
<Tag allcaps tone="brand" label="Secondary" />
|
|
20
|
+
<Tag allcaps priority="tertiary" tone="brand" label="Tertiary" />
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Tones
|
|
24
|
+
|
|
25
|
+
Omit `tone` for the neutral gray tag, or pick a semantic tone. Color
|
|
26
|
+
comes from the theme tokens, so every tone tracks light and dark mode
|
|
27
|
+
automatically.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<Tag allcaps label="Neutral" />
|
|
31
|
+
<Tag allcaps tone="brand" label="Brand" />
|
|
32
|
+
<Tag allcaps tone="info" label="Info" />
|
|
33
|
+
<Tag allcaps tone="success" label="Success" />
|
|
34
|
+
<Tag allcaps tone="warning" label="Warning" />
|
|
35
|
+
<Tag allcaps tone="critical" label="Critical" />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`tone` also accepts any literal CSS color for a one-off tag. The hue is
|
|
39
|
+
kept while lightness and chroma are pinned to the named-tone curve.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<Tag allcaps tone="#ff1493" label="Magenta" />
|
|
43
|
+
<Tag allcaps tone="rebeccapurple" label="Purple" />
|
|
44
|
+
<Tag allcaps tone="#0d9488" label="Teal" />
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Every tone — named or custom — paints its fill and border as
|
|
48
|
+
semi-transparent alpha tints rather than solid colors, so a tag picks up
|
|
49
|
+
whatever surface sits behind it. Here the same tags sit on a `--bg-4`
|
|
50
|
+
panel instead of the page canvas.
|
|
51
|
+
|
|
52
|
+
## Size
|
|
53
|
+
|
|
54
|
+
Three sizes — `small` (16px), `medium` (20px, the default — emits no
|
|
55
|
+
attribute), and `large` (24px) — matching `Button`. Height is intrinsic
|
|
56
|
+
(line-height + padding rather than a fixed value), so the text is never
|
|
57
|
+
clipped.
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
<Tag allcaps tone="info" size="small" label="Small" />
|
|
61
|
+
<Tag allcaps tone="info" label="Medium" />
|
|
62
|
+
<Tag allcaps tone="info" size="large" label="Large" />
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Icon, label & value
|
|
66
|
+
|
|
67
|
+
Set `icon` / `iconTrailing` for a leading or trailing glyph (scaled to
|
|
68
|
+
the pill), and pair `label` with `value` for a two-part tag. `value` is
|
|
69
|
+
the primary text (default color and weight); the `label` sits before it
|
|
70
|
+
as a bold "key" (weight 600, same color) with no divider — the weight
|
|
71
|
+
contrast does the separating. Tabular figures are always on, so counts,
|
|
72
|
+
versions, and timers don't reflow.
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<Tag allcaps tone="success" icon="circle-check" label="Build" value="passed" />
|
|
76
|
+
<Tag allcaps tone="info" icon="hourglass" label="Running" value="20m 16s" />
|
|
77
|
+
<Tag allcaps icon="github-logo" label="Commit" value="4f90d13" />
|
|
78
|
+
<Tag allcaps tone="warning" label="Retries" value="3" iconTrailing="refresh" />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Segments
|
|
82
|
+
|
|
83
|
+
For an arbitrary multi-part tag, pass several **children** instead of
|
|
84
|
+
`label` / `value` — each segment after the first gets a hairline divider
|
|
85
|
+
(a leading `icon` stays flush). The divider-less `label` + `value`
|
|
86
|
+
pairing is the exception; everything else segments like this.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
<Tag allcaps>
|
|
90
|
+
<span>v2.1.0</span>
|
|
91
|
+
<span>stable</span>
|
|
92
|
+
<span>x64</span>
|
|
93
|
+
</Tag>
|
|
94
|
+
<Tag allcaps tone="info">
|
|
95
|
+
<span>GET</span>
|
|
96
|
+
<span>200</span>
|
|
97
|
+
<span>142 ms</span>
|
|
98
|
+
</Tag>
|
|
99
|
+
<Tag allcaps tone="critical" icon="warning-triangle">
|
|
100
|
+
<span>Exit 137</span>
|
|
101
|
+
<span>OOM</span>
|
|
102
|
+
</Tag>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Case
|
|
106
|
+
|
|
107
|
+
Tags are normal (mixed) case by default, so the text is read as written —
|
|
108
|
+
proper names, identifiers, and case-sensitive IDs keep their shape (`GitHub`,
|
|
109
|
+
not `GITHUB`). Pass `allcaps` for the uppercase treatment: at this small size
|
|
110
|
+
all-caps reads as a label at a glance and keeps a uniform, scannable shape,
|
|
111
|
+
which suits short status words where the exact case carries no meaning.
|
|
112
|
+
Uppercase also tracks wider (0.08ch vs the default 0.02ch) and steps each size
|
|
113
|
+
down 1px, since caps read larger than mixed case at the same size.
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<Tag icon="github-logo" label="GitHub" value="v1.6.9" />
|
|
117
|
+
<Tag allcaps label="Running" />
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Props
|
|
121
|
+
|
|
122
|
+
| Prop | Type | Default | Description |
|
|
123
|
+
|------|------|---------|-------------|
|
|
124
|
+
| `allcaps?` | boolean | — | Render in all-caps instead of the default normal (mixed) case
|
|
125
|
+
(uppercase tracks wider than the default body-text letter-spacing). |
|
|
126
|
+
| `icon?` | IconShape | — | Leading icon shape. Sits flush before the label, scaled to the pill. |
|
|
127
|
+
| `iconTrailing?` | IconShape | — | Trailing icon shape. Renders last, after the value. |
|
|
128
|
+
| `label?` | string | — | A short "key" shown before the value. When paired with `value` it
|
|
129
|
+
renders bold (weight 600), same color. On its own (no `value`) it's
|
|
130
|
+
treated as the tag's primary text and keeps the default styling. |
|
|
131
|
+
| `priority?` | 'primary' \| 'secondary' \| 'tertiary' | secondary | Emphasis level. `secondary` (the default) is the subtle alpha-tint
|
|
132
|
+
fill; `primary` is a solid fill with white text; `tertiary` is a
|
|
133
|
+
transparent outline. Omitting it (or passing `'secondary'`) renders
|
|
134
|
+
the default and emits no DOM attribute. |
|
|
135
|
+
| `size?` | 'small' \| 'medium' \| 'large' | medium | Size variant. `small` = 16px tall, `medium` = 20px, `large` = 24px
|
|
136
|
+
(matching `Button`). Omit the attribute or pass `'medium'` for the
|
|
137
|
+
default — both render identically and emit no DOM attribute. |
|
|
138
|
+
| `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | neutral | Semantic tone, or any literal CSS color (`'#ff1493'`, `'rebeccapurple'`)
|
|
139
|
+
for a one-off custom tone. Each tone renders the secondary tag style:
|
|
140
|
+
`--text-3-{tone}` text over an alpha tint of the tone's hue (fill + a
|
|
141
|
+
slightly stronger hairline border). A custom color is tinted the same
|
|
142
|
+
way, with the text deepened to a readable foreground. `'neutral'` (the
|
|
143
|
+
default) is the gray tag — the same as omitting `tone`. |
|
|
144
|
+
| `value?` | string | — | The tag's primary text — a status, count, version, duration, etc.
|
|
145
|
+
Rendered in the default color and weight, with no divider from the
|
|
146
|
+
label; the color + weight contrast does the separating. |
|
|
147
|
+
|
|
148
|
+
Use this when you are not using the React or Preact wrapper and a native HTML control does not fit: construct the equivalent Anta web component from the elements below.
|
|
149
|
+
|
|
150
|
+
Use `a-tag-label` for the key and `a-tag-value` for its value.
|
|
151
|
+
|
|
152
|
+
```html
|
|
153
|
+
<a-tag tone="success">
|
|
154
|
+
<a-icon shape="check" aria-hidden="true"></a-icon>
|
|
155
|
+
<a-tag-label>Build</a-tag-label>
|
|
156
|
+
<a-tag-value>Passed</a-tag-value>
|
|
157
|
+
</a-tag>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Reach for the props first: **`tone`** sets the colour (any CSS colour for a custom
|
|
161
|
+
tone — it derives the fill / border / text in oklch), **`priority`** the fill style,
|
|
162
|
+
**`size`** the dimensions.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
<Tag allcaps tone="#e0457b" priority="secondary" label="Custom" />
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`<a-tag>` is light-DOM, so for anything a tone can't give you it's plain CSS (an
|
|
169
|
+
un-layered rule beats `@layer anta` without `!important`). The `.badge` class below
|
|
170
|
+
takes a **gradient** background (impossible from `tone`, which is a solid colour),
|
|
171
|
+
**1px** corners, and taller vertical padding so it reads completely differently:
|
|
172
|
+
|
|
173
|
+
```css
|
|
174
|
+
a-tag.badge {
|
|
175
|
+
background: linear-gradient(135deg, #1f6e5f, #845ec2); /* a tone can't do gradients */
|
|
176
|
+
color: #fff;
|
|
177
|
+
border-radius: 1px; /* sharp corners */
|
|
178
|
+
padding-block: 5px; /* tighter vertical padding */
|
|
179
|
+
padding-inline: 14px;
|
|
180
|
+
}
|
|
181
|
+
a-tag.badge a-tag-label { font-weight: 600; letter-spacing: 0.15ch; }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
For a *solid* recolour, prefer `tone` / `priority` — don't reach for the resolved
|
|
185
|
+
`--tag-bg` / `--tag-border` / `--tag-text` (they're recomputed per priority and tone).
|