@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.
Files changed (59) hide show
  1. package/README.md +38 -242
  2. package/dist/components/Input.d.ts +4 -1
  3. package/dist/components/Input.js +2 -0
  4. package/dist/components/InputDate.css +1 -0
  5. package/dist/components/InputDate.d.ts +1 -0
  6. package/dist/components/InputDate.js +5 -5
  7. package/dist/components/Menu.d.ts +5 -1
  8. package/dist/components/Menu.js +3 -2
  9. package/dist/components/Select.d.ts +1 -0
  10. package/dist/components/Select.js +9 -13
  11. package/dist/components/SelectFaceted.d.ts +1 -0
  12. package/dist/components/SelectFaceted.js +29 -23
  13. package/dist/components/select-parts.css +1 -0
  14. package/dist/elements/a-input.css +1 -1
  15. package/dist/elements/a-menu.js +12 -0
  16. package/dist/elements/a-slider.css +1 -1
  17. package/dist/elements/a-tabs.css +1 -1
  18. package/dist/elements/a-tabs.d.ts +3 -0
  19. package/dist/elements/a-tabs.js +40 -1
  20. package/dist/general_types.d.ts +2 -0
  21. package/dist/jsx-runtime.d.ts +9 -0
  22. package/docs/accessibility.md +34 -0
  23. package/docs/changelog.md +551 -0
  24. package/docs/colors.md +42 -0
  25. package/docs/comparison.md +28 -0
  26. package/docs/components/banner.md +267 -0
  27. package/docs/components/button.md +603 -0
  28. package/docs/components/card.md +322 -0
  29. package/docs/components/checkbox.md +297 -0
  30. package/docs/components/dialog.md +314 -0
  31. package/docs/components/expander.md +529 -0
  32. package/docs/components/icon.md +155 -0
  33. package/docs/components/input-autocomplete.md +101 -0
  34. package/docs/components/input-date.md +406 -0
  35. package/docs/components/input-time.md +174 -0
  36. package/docs/components/input.md +610 -0
  37. package/docs/components/loader.md +139 -0
  38. package/docs/components/menu.md +447 -0
  39. package/docs/components/progress.md +182 -0
  40. package/docs/components/radio.md +369 -0
  41. package/docs/components/select-faceted.md +456 -0
  42. package/docs/components/select.md +931 -0
  43. package/docs/components/slider.md +142 -0
  44. package/docs/components/switch.md +198 -0
  45. package/docs/components/tabs.md +471 -0
  46. package/docs/components/tag.md +185 -0
  47. package/docs/components/text.md +148 -0
  48. package/docs/components/title.md +146 -0
  49. package/docs/components/toaster.md +313 -0
  50. package/docs/components/tooltip.md +177 -0
  51. package/docs/credits.md +22 -0
  52. package/docs/index.md +52 -0
  53. package/docs/install-config.md +240 -0
  54. package/docs/normalization.md +105 -0
  55. package/docs/overview.md +10 -0
  56. package/docs/packages/stickers.md +187 -0
  57. package/docs/packages/table.md +153 -0
  58. package/docs/theming.md +13 -0
  59. 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).