@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,610 @@
|
|
|
1
|
+
# Input
|
|
2
|
+
|
|
3
|
+
A text field. The `Input` wrapper renders an `<a-input>` web component whose
|
|
4
|
+
real `<input>` / `<textarea>` lives in **shadow DOM** — so the field is
|
|
5
|
+
self-contained (you never pass a control in) yet fully native underneath:
|
|
6
|
+
focus, IME, autofill, and form submission all work, and the control is reachable
|
|
7
|
+
for styling through `::part(input)`.
|
|
8
|
+
|
|
9
|
+
## Sizes
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<Input size="small" label="Small" placeholder="24px tall" />
|
|
13
|
+
<Input size="medium" label="Medium" placeholder="28px tall" /> {/* default */}
|
|
14
|
+
<Input size="large" label="Large" placeholder="32px tall" />
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Three sizes (`medium` is the default). The field **height**, the **type scale**
|
|
18
|
+
(font / line-height), and the **icon** all track the size. Each step matches
|
|
19
|
+
Anta's text scale, so a field lines up with same-size `Text` / `Button`.
|
|
20
|
+
|
|
21
|
+
| size | height | font / line | icon |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| `small` | 24px | 13 / 16 | 14px |
|
|
24
|
+
| `medium` | 28px | 15 / 20 | 16px |
|
|
25
|
+
| `large` | 32px | 17 / 22 | 18px |
|
|
26
|
+
|
|
27
|
+
## Label & hint
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<Input label="Display name" defaultValue="Ada Lovelace" />
|
|
31
|
+
<Input label="API key" hint="Find this in Settings → Developers." placeholder="sk-…" />
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`label` sits above the field; `hint` is neutral helper text below it
|
|
35
|
+
(`--text-3`, no icon). Both accept a **string or a React node**, so you can
|
|
36
|
+
embed links, `<code>`, or a `<Text>` for full control.
|
|
37
|
+
|
|
38
|
+
The label is wired as the control's accessible name automatically — the element
|
|
39
|
+
mirrors the label text into `aria-label`, since a native `<label for>` can't
|
|
40
|
+
cross the shadow boundary. The `hint` (or error) is mirrored the same way into
|
|
41
|
+
`aria-description`, so a screen reader announces the message too.
|
|
42
|
+
|
|
43
|
+
## Tooltip
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
{/* Drop a <Tooltip> in as a child — it attaches to the field and shows on hover,
|
|
47
|
+
exactly like a tooltip on any other element. */}
|
|
48
|
+
<Input label="API key" placeholder="sk-…">
|
|
49
|
+
<Tooltip>Find this in Settings → Developers.</Tooltip>
|
|
50
|
+
</Input>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Attach a tooltip by dropping an Anta `<Tooltip>` in as a **child** — no slot or
|
|
54
|
+
prop needed. It anchors to the field and shows on hover, consistent with how
|
|
55
|
+
tooltips attach to a `<Button>` or any other element. (Children with no `slot`
|
|
56
|
+
are projected into a default slot; use `leading` / `trailing` for *in-field*
|
|
57
|
+
content.)
|
|
58
|
+
|
|
59
|
+
## Status
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
{/* No status = neutral helper text */}
|
|
63
|
+
<Input label="Display name" defaultValue="Ada Lovelace" hint="This is your public name." />
|
|
64
|
+
<Input label="Workspace" defaultValue="acme" status="info" hint="Lowercase letters and dashes only." />
|
|
65
|
+
<Input label="Username" defaultValue="ada" status="success" hint="Username is available." />
|
|
66
|
+
<Input label="Password" type="password" defaultValue="hunter2" status="warning" hint="Weak — add more characters." />
|
|
67
|
+
<Input label="Email" defaultValue="not-an-email" status="critical" hint="Enter a valid email address." />
|
|
68
|
+
<Input label="Plan" defaultValue="Pro" status="brand" hint="You're on the Pro plan." />
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`status` tints the border and the `hint`, and prefixes a glyph — `critical`,
|
|
72
|
+
`warning`, `success`, `info`, or `brand`. Each pulls its tone's role tokens, so
|
|
73
|
+
dark mode is automatic. The message always rides in `hint`; `status` only
|
|
74
|
+
recolors it.
|
|
75
|
+
|
|
76
|
+
It reuses the tone palette but isn't `tone`: **`status` is a validation *state*** (it
|
|
77
|
+
also carries the glyph and, for `critical`, validity), whereas `tone` — elsewhere in
|
|
78
|
+
Anta — is decorative colour. On `Input` the named tones live on `status`; `tone` is
|
|
79
|
+
reserved for a one-off custom border colour.
|
|
80
|
+
|
|
81
|
+
Only `status="critical"` carries validity — it sets `aria-invalid`, flips the
|
|
82
|
+
`:state(invalid)` hook, and reports invalid to a surrounding `<form>` through
|
|
83
|
+
`ElementInternals`. The other tones are advisory and stay valid.
|
|
84
|
+
|
|
85
|
+
Each status has a default glyph (`critical` → `warning-diamond`, `warning` →
|
|
86
|
+
`warning-triangle`, `success` → `circle-check`, `info` → `info`, `brand` →
|
|
87
|
+
`circle-small-solid`). Override it with **`statusIcon`** (any shape), or drop it
|
|
88
|
+
with `statusIcon={false}`.
|
|
89
|
+
|
|
90
|
+
## Leading, trailing & clear
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
{/* clearable — the clear button shows once there's a value */}
|
|
94
|
+
{/* dimActions — adornments rest quiet, brighten when the field is hovered/focused */}
|
|
95
|
+
<Input label="Search" placeholder="Search…" clearable dimActions defaultValue="design tokens" />
|
|
96
|
+
|
|
97
|
+
{/* type="search" is a shorthand for the leading search icon + clearable below */}
|
|
98
|
+
<Input label="Search" type="search" placeholder="Search…" />
|
|
99
|
+
|
|
100
|
+
{/* leading / trailing take any node — see the live playground above */}
|
|
101
|
+
<Input label="Search" leading={<Icon shape="search" />} clearable dimActions />
|
|
102
|
+
<Input
|
|
103
|
+
label="Date"
|
|
104
|
+
leading={<Icon shape="calendar" />}
|
|
105
|
+
trailing={<Button priority="quaternary" size="small" icon="eye" aria-label="Pick" />}
|
|
106
|
+
dimActions
|
|
107
|
+
/>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`leading` and `trailing` paste any node at the field edges (like the expander's
|
|
111
|
+
`actions`) — an icon, a button, a unit label. A leading item is inset from the
|
|
112
|
+
left by the same `pad-x` as the text, so it lines up on the field's rhythm; the
|
|
113
|
+
playground at the top of this page shows a live leading icon alongside the clear
|
|
114
|
+
button.
|
|
115
|
+
|
|
116
|
+
**`type="search"`** is a shorthand for that search pattern: it defaults a leading
|
|
117
|
+
search icon and `clearable` (override either — pass your own `leading`, or
|
|
118
|
+
`clearable={false}`). It's wrapper-only — the native `search` type never reaches the
|
|
119
|
+
DOM input, so the browser's own clear/search chrome never appears; Anta owns it.
|
|
120
|
+
|
|
121
|
+
**`dimActions`** rests the adornments quiet (60% opacity) and brightens them to
|
|
122
|
+
full when the field is hovered or focused — a "quiet until engaged" look for
|
|
123
|
+
trailing actions. It dims the whole slot, so it covers icons and buttons alike.
|
|
124
|
+
|
|
125
|
+
`clearable` adds a clear button at the right edge, shown once the field has a
|
|
126
|
+
value. It's a real `<a-button>` in the `clear` slot — full button styling,
|
|
127
|
+
**keyboard-focusable** — and it works **without framework hydration**: a-button's
|
|
128
|
+
global handler fires a `clearrequest` trigger on click and Enter/Space, which the
|
|
129
|
+
element turns into the lifecycle below.
|
|
130
|
+
|
|
131
|
+
Two hooks bracket the clear. **`onClearClick`** fires first and is **cancelable** —
|
|
132
|
+
`e.preventDefault()` keeps the value (e.g. a "discard unsaved text?" confirm). If
|
|
133
|
+
it isn't prevented, the field clears: it empties, fires `input` / `change` (a
|
|
134
|
+
controlled `onChange` runs with the empty value), refocuses, then fires
|
|
135
|
+
**`onClearInput`**. On a raw `<a-input>`, bind both lowercase (`onclearclick` /
|
|
136
|
+
`onclearinput`) — that form binds in React and Preact alike.
|
|
137
|
+
|
|
138
|
+
Note: there's no `type="search"` — that type makes browsers inject their own
|
|
139
|
+
clear button and magnifier (and style them inconsistently). Anta owns every
|
|
140
|
+
in-field control, and the element resets the native search/number/reveal
|
|
141
|
+
decorations across browsers.
|
|
142
|
+
|
|
143
|
+
### Password reveal
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
// Reveal toggle = flip `type` between password and text. Native masking when
|
|
147
|
+
// hidden, plain text when shown; password managers keep working.
|
|
148
|
+
const [reveal, setReveal] = useState(false)
|
|
149
|
+
|
|
150
|
+
<Input
|
|
151
|
+
label="Password"
|
|
152
|
+
type={reveal ? 'text' : 'password'}
|
|
153
|
+
defaultValue="hunter2"
|
|
154
|
+
dimActions
|
|
155
|
+
trailing={
|
|
156
|
+
<Button
|
|
157
|
+
priority="tertiary"
|
|
158
|
+
icon={reveal ? 'eye-closed' : 'eye'}
|
|
159
|
+
aria-label={reveal ? 'Hide password' : 'Show password'}
|
|
160
|
+
onClick={() => setReveal(v => !v)}
|
|
161
|
+
/>
|
|
162
|
+
}
|
|
163
|
+
/>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
A `trailing` eye `<Button>` flips the field's `type` between `password` and
|
|
167
|
+
`text` — native disc masking when hidden, plain text when revealed. Because it
|
|
168
|
+
stays a real `type="password"` when hidden, password managers and autofill keep
|
|
169
|
+
working, and the uncontrolled value is preserved across the toggle.
|
|
170
|
+
|
|
171
|
+
The mask glyph stays the browser default — restyling it means dropping
|
|
172
|
+
`type="password"`, which loses password-manager support and isn't masked in
|
|
173
|
+
Firefox. So Anta keeps the native field.
|
|
174
|
+
|
|
175
|
+
## Multiline
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
{/* Grows with content, capped at 6 rows, then scrolls */}
|
|
179
|
+
<Input multiline maxRows={6} label="Bio (autogrows)" placeholder="Tell us…" />
|
|
180
|
+
|
|
181
|
+
{/* Constant height of 3 rows */}
|
|
182
|
+
<Input multiline rows={3} label="Notes (fixed 3 rows)" placeholder="…" />
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`multiline` renders a `<textarea>` under the same API. With **no `rows`** it
|
|
186
|
+
**autogrows** with its content via CSS `field-sizing: content` — capped by
|
|
187
|
+
`maxRows` if you set one (omit for unbounded growth), then it scrolls. Pass a
|
|
188
|
+
fixed **`rows`** count for a constant-height box instead.
|
|
189
|
+
|
|
190
|
+
Autogrow uses CSS `field-sizing` where supported (Chromium, Safari ≥ 26.2) and a
|
|
191
|
+
built-in JS resize everywhere else (Firefox, older Safari), so it grows in every
|
|
192
|
+
browser.
|
|
193
|
+
|
|
194
|
+
## Controlled & uncontrolled
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// Uncontrolled — the element owns the value
|
|
198
|
+
<Input label="Name" defaultValue="Ada" onChange={(e) => log(e.target.value)} />
|
|
199
|
+
|
|
200
|
+
// Controlled — you own the value
|
|
201
|
+
const [v, setV] = useState('')
|
|
202
|
+
<Input label="Name" value={v} onInput={(e) => setV(e.target.value)} />
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Like a native input: pass `defaultValue` for **uncontrolled**, or `value` +
|
|
206
|
+
`onInput` / `onChange` for **controlled**. The controlled value is reflected to
|
|
207
|
+
the shadow control only when it differs from what's there, so the caret never
|
|
208
|
+
jumps mid-edit.
|
|
209
|
+
|
|
210
|
+
Read the value off `e.target.value` in either handler — the event retargets to
|
|
211
|
+
the `<a-input>` host, whose `value` getter returns the control's value.
|
|
212
|
+
|
|
213
|
+
Note: like other custom-element form controls (and unlike React's built-in
|
|
214
|
+
`<input>`), a controlled value that you *transform* applies on the next render,
|
|
215
|
+
but one you *reject* — set state back to the same string — isn't auto-reverted,
|
|
216
|
+
since the framework skips the re-render. Constrain hard with `maxLength` /
|
|
217
|
+
`pattern` / `type`, or transform in the handler.
|
|
218
|
+
|
|
219
|
+
## Events
|
|
220
|
+
|
|
221
|
+
| handler | fires | payload |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `onInput(e)` | on **every keystroke** | read `e.target.value` |
|
|
224
|
+
| `onChange(e)` | on **commit** (blur / Enter) — native `change` semantics, *not* React's per-keystroke `onChange` | read `e.target.value` |
|
|
225
|
+
| `onValueChange(e, attrs)` | on `input` **and** `change` (and clear) | `{ value, name, empty, valid, validationMessage }` |
|
|
226
|
+
| `onClearClick(e)` | before the `clearable` × clears | ✅ `e.preventDefault()` keeps the value |
|
|
227
|
+
|
|
228
|
+
**Input has no `onStateChange`, on purpose.** That event belongs to components
|
|
229
|
+
with a *discrete* state — a small enum like `Checkbox`'s `checked · unchecked ·
|
|
230
|
+
indeterminate` or a menu's `open · closed` — where a transition can be vetoed as
|
|
231
|
+
a unit before it applies. A text field's value is **free-form**, not a discrete
|
|
232
|
+
state, so there's nothing to model as `state` and nothing to veto wholesale;
|
|
233
|
+
you control it the native way (`value` + `onInput`/`onChange`, reject by not
|
|
234
|
+
updating). Reach for `onValueChange` as the one unified "the value changed"
|
|
235
|
+
handler — it's the same callback `Checkbox`, `RadioGroup`, and `Select` expose,
|
|
236
|
+
so form code reads the same across all of them. (`Select`, likewise composed and
|
|
237
|
+
value-based, also has only `onValueChange`.)
|
|
238
|
+
|
|
239
|
+
## Forms
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
<form onSubmit={handle}>
|
|
243
|
+
<Input name="email" label="Email" type="email" required clearable />
|
|
244
|
+
<Button tone="brand" type="submit" label="Sign up" />
|
|
245
|
+
<Button priority="tertiary" type="reset" label="Reset" />
|
|
246
|
+
</form>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Because `<a-input>` is form-associated, it submits with the surrounding
|
|
250
|
+
`<form>` under its `name` — `new FormData(form)` includes it, no hidden mirror
|
|
251
|
+
needed. `required` participates in native validity, and `<Button type="reset">`
|
|
252
|
+
restores the field to its `defaultValue` via `formResetCallback`.
|
|
253
|
+
|
|
254
|
+
## Types & validation
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
<Input type="text" label="Text" />
|
|
258
|
+
<Input type="email" label="Email" />
|
|
259
|
+
<Input type="password" label="Password" />
|
|
260
|
+
<Input type="tel" label="Tel" />
|
|
261
|
+
<Input type="url" label="URL" />
|
|
262
|
+
<Input type="number" label="Number" min={0} max={100} />
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`type` accepts `text` (default), `email`, `password`, `tel`, `url`, and
|
|
266
|
+
`number` — each gets the right mobile keyboard, autofill behaviour, and native
|
|
267
|
+
constraint validation. (`search` is omitted on purpose — see above.)
|
|
268
|
+
`number` also takes `min` / `max` / `step`.
|
|
269
|
+
|
|
270
|
+
For a **controlled** numeric field — one whose `value` you hold in state — prefer
|
|
271
|
+
`type="text"` with `inputMode="decimal"` (or `"numeric"`) over `type="number"`. A
|
|
272
|
+
number input runs value sanitization, so a mid-typing `"1."` or `"1.0"` reports an
|
|
273
|
+
empty `value` and can't be entered; storing the parsed `Number` and rendering it
|
|
274
|
+
back drops the decimal for the same reason. Keep the raw string in state and parse
|
|
275
|
+
to a number where you use it — `inputMode` still brings up the numeric keypad. An
|
|
276
|
+
**uncontrolled** `type="number"` (no bound `value`) has neither problem.
|
|
277
|
+
|
|
278
|
+
Every type carries its native constraints (`required`, the `type` itself,
|
|
279
|
+
`pattern`, `min`/`max`, `min`/`maxLength`). The field reports them to the
|
|
280
|
+
surrounding `<form>` automatically (via `ElementInternals`), so submission is
|
|
281
|
+
blocked while invalid — no extra wiring.
|
|
282
|
+
|
|
283
|
+
### Native validation
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
// Surface the browser's own validity messages as Anta's critical-status UI.
|
|
287
|
+
const [errors, setErrors] = useState({})
|
|
288
|
+
|
|
289
|
+
function onSubmit(e) {
|
|
290
|
+
e.preventDefault()
|
|
291
|
+
const next = {}
|
|
292
|
+
e.currentTarget.querySelectorAll('a-input').forEach((el) => {
|
|
293
|
+
if (!el.checkValidity()) next[el.name] = el.validationMessage
|
|
294
|
+
})
|
|
295
|
+
setErrors(next)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
<form noValidate onSubmit={onSubmit}>
|
|
299
|
+
<Input name="email" type="email" required hint={errors.email}
|
|
300
|
+
status={errors.email ? 'critical' : undefined}
|
|
301
|
+
onInput={() => setErrors(p => ({ ...p, email: undefined }))} />
|
|
302
|
+
<Input name="age" type="number" min="18" max="120" hint={errors.age}
|
|
303
|
+
status={errors.age ? 'critical' : undefined}
|
|
304
|
+
onInput={() => setErrors(p => ({ ...p, age: undefined }))} />
|
|
305
|
+
<Button type="submit" label="Submit" />
|
|
306
|
+
</form>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The element proxies native validity: `<a-input>` exposes `checkValidity()` and
|
|
310
|
+
`validationMessage` (the browser's localized message).
|
|
311
|
+
|
|
312
|
+
Pass `validationMessage` to **`hint`** and set **`status="critical"`** — the
|
|
313
|
+
field goes red, shows the warning glyph, and reports invalid to the form. Mark
|
|
314
|
+
the form `noValidate` so the browser's default bubbles step aside for Anta's
|
|
315
|
+
styling. Try submitting empty, or with a bad email / out-of-range age.
|
|
316
|
+
|
|
317
|
+
**Enter doesn't submit the form.** The real `<input>` lives in shadow DOM, so
|
|
318
|
+
it isn't a light-DOM member of the `<form>`, and pressing Enter won't trigger
|
|
319
|
+
native implicit submission — a deliberate choice, since auto-submitting a
|
|
320
|
+
half-filled multi-field form is usually more annoyance than help. To submit on
|
|
321
|
+
Enter, handle it yourself (`onKeyDown` → `form.requestSubmit()`), or reach for
|
|
322
|
+
Anta's upcoming `<Form>` component. Everything else — value submission under
|
|
323
|
+
`name`, `checkValidity()`, `formReset` — works natively.
|
|
324
|
+
|
|
325
|
+
## Layout
|
|
326
|
+
|
|
327
|
+
The label, field, and hint **aren't hardcoded to "stacked."** The element lays
|
|
328
|
+
them out as a single-column **grid** (hence the default label-above / hint-below
|
|
329
|
+
rhythm) and exposes each region as a shadow part (`label`, `field`, `hint`).
|
|
330
|
+
Rearrange them from your own CSS: re-template the grid on `a-input`, then place
|
|
331
|
+
the parts. No layout prop to learn — your rule wins because the element's
|
|
332
|
+
defaults live in `@layer anta`.
|
|
333
|
+
|
|
334
|
+
And because each `<a-input>` is itself a normal grid/flex item, **wrapping** is
|
|
335
|
+
just sizing: give the inputs a `min-width` (and maybe `max-width`) inside any
|
|
336
|
+
wrapping container and they reflow by available width — the field's internal
|
|
337
|
+
grid is independent of how the fields themselves flow.
|
|
338
|
+
|
|
339
|
+
The `.label-side` / `.aligned-form` / `.wrap-fields` classes below are only to
|
|
340
|
+
scope each demo — swap your own selector.
|
|
341
|
+
|
|
342
|
+
### Label on the left
|
|
343
|
+
|
|
344
|
+
Re-template the host to two columns and drop each part into one. The hint stays
|
|
345
|
+
under the field by landing in the same column.
|
|
346
|
+
|
|
347
|
+
```tsx
|
|
348
|
+
<Input className="label-side" label="Display name" hint="Shown on your profile" />
|
|
349
|
+
|
|
350
|
+
{/* .label-side is just for the demo — use your own selector */}
|
|
351
|
+
<style>{`
|
|
352
|
+
a-input.label-side {
|
|
353
|
+
grid-template-columns: max-content minmax(0, 1fr);
|
|
354
|
+
column-gap: 12px;
|
|
355
|
+
align-items: center;
|
|
356
|
+
}
|
|
357
|
+
a-input.label-side::part(label) { grid-column: 1; }
|
|
358
|
+
a-input.label-side::part(field) { grid-column: 2; }
|
|
359
|
+
a-input.label-side::part(hint) { grid-column: 2; }
|
|
360
|
+
`}</style>
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Label column
|
|
364
|
+
|
|
365
|
+
Put the fields in a grid and let each one adopt the form's tracks with
|
|
366
|
+
`grid-template-columns: subgrid` — which reaches *through* the shadow boundary,
|
|
367
|
+
so every label shares one column sized to the longest label, and every field
|
|
368
|
+
left-edge lines up. Each hint still tucks under its own field.
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
<div className="aligned-form">
|
|
372
|
+
<Input label="Email" type="email" hint="We'll never share it." />
|
|
373
|
+
<Input label="Confirm password" type="password" />
|
|
374
|
+
<Input label="PIN" />
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<style>{`
|
|
378
|
+
/* one shared label column; subgrid lets each field adopt the form's tracks */
|
|
379
|
+
.aligned-form { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 16px 12px; }
|
|
380
|
+
.aligned-form > a-input { grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center; }
|
|
381
|
+
.aligned-form > a-input::part(label) { grid-column: 1; }
|
|
382
|
+
.aligned-form > a-input::part(field),
|
|
383
|
+
.aligned-form > a-input::part(hint) { grid-column: 2; }
|
|
384
|
+
`}</style>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Hint on the right
|
|
388
|
+
|
|
389
|
+
Put the hint beside the field instead of under it — a short note, a counter, a
|
|
390
|
+
unit. Two columns (field, then hint); the label spans the top, and the hint
|
|
391
|
+
centers against the field.
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
<Input className="hint-right" label="Username" hint="3–20 characters" placeholder="ada" />
|
|
395
|
+
|
|
396
|
+
<style>{`
|
|
397
|
+
a-input.hint-right {
|
|
398
|
+
grid-template-columns: minmax(0, 1fr) max-content;
|
|
399
|
+
column-gap: 10px;
|
|
400
|
+
}
|
|
401
|
+
a-input.hint-right::part(label) { grid-column: 1 / -1; }
|
|
402
|
+
a-input.hint-right::part(field) { grid-column: 1; grid-row: 2; }
|
|
403
|
+
a-input.hint-right::part(hint) { grid-column: 2; grid-row: 2; align-self: center; padding-top: 0; }
|
|
404
|
+
`}</style>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Wrap by field width
|
|
408
|
+
|
|
409
|
+
Leave the fields stacked and let an outer grid flow them into as many columns as
|
|
410
|
+
fit — `minmax(200px, 1fr)` makes each field claim at least 200px, so the row
|
|
411
|
+
wraps to fewer columns as it narrows. Resize the preview to see it reflow.
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
<div className="wrap-fields">
|
|
415
|
+
<Input label="First name" />
|
|
416
|
+
<Input label="Last name" />
|
|
417
|
+
<Input label="Email" type="email" />
|
|
418
|
+
<Input label="Phone" type="tel" />
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
<style>{`
|
|
422
|
+
/* fields stay stacked; the grid wraps them by available width */
|
|
423
|
+
.wrap-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
|
|
424
|
+
`}</style>
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Props
|
|
428
|
+
|
|
429
|
+
| Prop | Type | Default | Description |
|
|
430
|
+
|------|------|---------|-------------|
|
|
431
|
+
| `autoComplete?` | 'off' \| 'on' \| 'name' \| 'username' \| 'email' \| 'current-password' \| 'new-password' \| 'one-time-code' \| 'tel' \| 'url' \| (string & {}) | — | Native autocomplete token. Overrides the value derived from `type`
|
|
432
|
+
(`email` / `tel` / `url`) — set it for the cases `type` can't express, e.g.
|
|
433
|
+
`username`, `current-password`, `new-password`, `one-time-code`, or `off`. |
|
|
434
|
+
| `children?` | ReactNode | — | Extra content rendered directly under the field, above the hint/error (it
|
|
435
|
+
pushes the message down). A no-box child like an Anta `<Tooltip>` takes no
|
|
436
|
+
space and just anchors to the field — consistent with how tooltips attach
|
|
437
|
+
to any other element. Use the named `leading` / `trailing` props for
|
|
438
|
+
in-field content. |
|
|
439
|
+
| `clearable?` | boolean | — | Show a clear button as the first trailing item once the field has a
|
|
440
|
+
value. |
|
|
441
|
+
| `defaultValue?` | string | — | Initial value for the uncontrolled case. |
|
|
442
|
+
| `dimActions?` | boolean | — | Dim the `leading` / `trailing` adornments at rest; they brighten to full
|
|
443
|
+
when the field is hovered or focused (a quiet-until-engaged affordance for
|
|
444
|
+
trailing actions). |
|
|
445
|
+
| `disabled?` | boolean | — | Disable the field. |
|
|
446
|
+
| `hint?` | ReactNode | — | Message below the field. Neutral helper text by default; `status` recolors
|
|
447
|
+
it and prefixes the matching glyph. |
|
|
448
|
+
| `inputMode?` | 'none' \| 'text' \| 'decimal' \| 'numeric' \| 'tel' \| 'search' \| 'email' \| 'url' | — | Virtual-keyboard hint. Overrides the value derived from `type`. |
|
|
449
|
+
| `label?` | ReactNode | — | Field label, shown above the control. A string is rendered with the
|
|
450
|
+
label type scale; pass a node for full control. Associated with the
|
|
451
|
+
control as its accessible name (the element mirrors the label text to
|
|
452
|
+
`aria-label`, since `<label for>` can't cross the shadow boundary). |
|
|
453
|
+
| `leading?` | ReactNode | — | Content pinned to the start of the field (e.g. an icon). |
|
|
454
|
+
| `max?` | number \| string | — | |
|
|
455
|
+
| `maxLength?` | number | — | Max input length. |
|
|
456
|
+
| `maxRows?` | number | — | Cap the autogrow height (in rows) of a `multiline` field with no `rows`.
|
|
457
|
+
Omit for unbounded growth. |
|
|
458
|
+
| `min?` | number \| string | — | Min / max / step — for `type="number"`. |
|
|
459
|
+
| `minLength?` | number | — | Min input length. |
|
|
460
|
+
| `multiline?` | boolean | — | Render a `<textarea>` instead of an `<input>`. Without `rows` it grows
|
|
461
|
+
with its content from one line (capped by `maxRows` if set). Autogrow uses
|
|
462
|
+
CSS `field-sizing` where supported (Chrome/Edge, Safari ≥ 26.2) and falls
|
|
463
|
+
back to a built-in JS resize elsewhere (Firefox, older Safari), so it grows
|
|
464
|
+
in every browser. |
|
|
465
|
+
| `name?` | string | — | Form field name — submitted with the form via ElementInternals. |
|
|
466
|
+
| `onBlur?` | (e) => void | — | Fires when the field loses focus. |
|
|
467
|
+
| `onChange?` | (e) => void | — | Fires on **commit** (blur / Enter) — the platform `change` semantics, **not**
|
|
468
|
+
React's per-keystroke `onChange`. This is a web component, so `onChange` keeps
|
|
469
|
+
the native meaning; reach for `onInput` (every keystroke) or `onValueChange`
|
|
470
|
+
(both) for live updates. Read `e.target.value`. |
|
|
471
|
+
| `onClearClick?` | (e) => void | — | Fires when the built-in clear button (`clearable`) is clicked, *before*
|
|
472
|
+
the field is cleared. Call `e.preventDefault()` to keep the current value
|
|
473
|
+
— the clear is cancelled and `onClearInput` won't fire. Backed by the
|
|
474
|
+
element's cancelable, bubbling `clearclick` event. |
|
|
475
|
+
| `onClearInput?` | (e) => void | — | Fires after the built-in clear button (`clearable`) has cleared the field
|
|
476
|
+
— so `onInput` / `onChange` fire too — making this useful for reacting
|
|
477
|
+
specifically to a clear. Doesn't fire if `onClearClick` cancelled the
|
|
478
|
+
clear. Backed by the element's bubbling `clearinput` event. |
|
|
479
|
+
| `onFocus?` | (e) => void | — | Fires when the field gains focus. |
|
|
480
|
+
| `onInput?` | (e) => void | — | Fires on every keystroke. Read `e.target.value`. |
|
|
481
|
+
| `onValueChange?` | (event, attrs) => void | — | Unified value-change handler — the easy path for state. Fires on `input`
|
|
482
|
+
*and* `change` (and on clear), with the native `event` plus a convenience
|
|
483
|
+
`attrs` snapshot (`value`, `name`, `empty`, `valid`, `validationMessage`) so
|
|
484
|
+
you can do `setForm(s => ({ ...s, [attrs.name]: attrs.value }))` without
|
|
485
|
+
digging into the event. Use `event.type` to tell a live edit (`input`) from
|
|
486
|
+
a commit (`change`); read `id` / `type` / `className` off `event.target`. |
|
|
487
|
+
| `pattern?` | string | — | Validation pattern (single-line). |
|
|
488
|
+
| `placeholder?` | string | — | Placeholder shown when empty. |
|
|
489
|
+
| `readOnly?` | boolean | — | Make the field read-only. |
|
|
490
|
+
| `required?` | boolean | — | Mark the field required (drives native validity). |
|
|
491
|
+
| `role?` | string | — | ARIA `role` for the field — e.g. `combobox` when the input drives a
|
|
492
|
+
suggestion `listbox` (see `InputAutocomplete`). Left unset by default. |
|
|
493
|
+
| `round?` | boolean \| number \| string | — | Fully-round the field (`border-radius: 999px`). Pass a `number` (px) or a CSS
|
|
494
|
+
length string for a custom radius. The `clearable` × button always rounds to
|
|
495
|
+
a circle to match (it isn't sized by a custom field value). |
|
|
496
|
+
| `rows?` | number | — | Fixed visible row count — a constant-height `<textarea>` (implies
|
|
497
|
+
`multiline`). |
|
|
498
|
+
| `size?` | 'small' \| 'medium' \| 'large' | medium | Size variant. small=24px, medium=28px, large=32px tall; the type scale and
|
|
499
|
+
icon track the size (small 13/16 + 14px icon · medium 15/20 + 16px ·
|
|
500
|
+
large 17/22 + 18px). |
|
|
501
|
+
| `spellCheck?` | boolean | — | Toggle native spell-checking. |
|
|
502
|
+
| `status?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' | — | Validation / feedback tone — colors the border + `hint` and prefixes a
|
|
503
|
+
glyph. Only `critical` marks the field invalid (`aria-invalid`, blocks form
|
|
504
|
+
submission, `:state(invalid)`); `success` / `warning` / `info` / `brand`
|
|
505
|
+
are advisory and stay valid. Omit (or `neutral`) for a plain field. |
|
|
506
|
+
| `statusIcon?` | (string & {}) \| false \| IconShape | — | Glyph shown before the `hint` when `status` is set. Each status has a
|
|
507
|
+
default (critical → `warning-diamond`, warning → `warning-triangle`,
|
|
508
|
+
success → `circle-check`, info → `info`, brand → `circle-small-solid`); pass a
|
|
509
|
+
shape to override, or `false` to drop it. `neutral` has no default glyph. |
|
|
510
|
+
| `step?` | number \| string | — | |
|
|
511
|
+
| `tone?` | string | — | Custom accent colour — any literal CSS colour tints the resting + hover
|
|
512
|
+
border (focus ring stays the global `--focus-ring`). For consistency with the
|
|
513
|
+
other controls' custom-tone knob; a `status` still overrides for validation. |
|
|
514
|
+
| `trailing?` | ReactNode | — | Content pinned to the end of the field (e.g. icons, buttons), after the
|
|
515
|
+
clear button when `clearable`. |
|
|
516
|
+
| `truncate?` | boolean | — | Ellipsize an overflowing single-line value. Read-only inputs already do
|
|
517
|
+
this; use `truncate` when an editable field should keep the same treatment. |
|
|
518
|
+
| `type?` | 'text' \| 'search' \| 'email' \| 'password' \| 'tel' \| 'url' \| 'number' | text | Single-line input type. Ignored when `multiline`. `search` is a
|
|
519
|
+
**wrapper-only** shorthand: it defaults a leading search icon and a clear
|
|
520
|
+
button (both overridable — pass your own `leading`, or `clearable={false}`)
|
|
521
|
+
and sets `inputmode="search"`, but the DOM input stays `type="text"`. The
|
|
522
|
+
native `search` type never reaches the element, so the browser's own
|
|
523
|
+
clear/search affordances never appear — Anta owns that chrome. |
|
|
524
|
+
| `value?` | string | — | Controlled value. Pair with `onChange` / `onInput`. |
|
|
525
|
+
|
|
526
|
+
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.
|
|
527
|
+
|
|
528
|
+
Slots hold the label, hint, and controls around the native field.
|
|
529
|
+
|
|
530
|
+
```html
|
|
531
|
+
<a-input name="email" placeholder="you@example.com">
|
|
532
|
+
<span slot="label">Email</span>
|
|
533
|
+
<span slot="hint">We only use it for account notices.</span>
|
|
534
|
+
</a-input>
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Native HTML inputs
|
|
538
|
+
|
|
539
|
+
For a standard HTML form, add `data-anta` to a native input. Text, search,
|
|
540
|
+
email, telephone, URL, password, and number inputs keep their native editing,
|
|
541
|
+
keyboard, validation, and form behavior while using Anta's field chrome. Search
|
|
542
|
+
keeps its browser-supplied affordances; file keeps its native chooser and selected
|
|
543
|
+
file name, with an Anta-styled chooser button. `submit` and `reset` inputs use the
|
|
544
|
+
matching Anta button chrome; set `priority="primary"` on a submit control when it
|
|
545
|
+
is the form's main action.
|
|
546
|
+
|
|
547
|
+
Use `data-anta-size="small"` or `"large"` for field sizing. Native inputs own
|
|
548
|
+
their `size` attribute, so Anta does not reuse it. `round` and a custom-color
|
|
549
|
+
`tone` work the same way as Input.
|
|
550
|
+
|
|
551
|
+
```html
|
|
552
|
+
<input data-anta data-anta-size="small" type="text" name="name" placeholder="Small text">
|
|
553
|
+
<input data-anta type="search" name="search" placeholder="Search">
|
|
554
|
+
<input data-anta type="email" name="email" placeholder="Email">
|
|
555
|
+
<input data-anta type="tel" name="phone" placeholder="Telephone">
|
|
556
|
+
<input data-anta round type="url" name="site" placeholder="Rounded URL">
|
|
557
|
+
<input data-anta type="password" name="password" value="password">
|
|
558
|
+
<input data-anta tone="rebeccapurple" type="number" name="seats" value="3" min="1">
|
|
559
|
+
<input data-anta type="file" name="attachment" accept=".pdf,.docx">
|
|
560
|
+
|
|
561
|
+
<input data-anta type="submit" value="Submit" priority="primary">
|
|
562
|
+
<input data-anta type="reset" value="Reset">
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Reach for the props first: **`status`** sets a validation tone (border + message),
|
|
566
|
+
**`tone`** a custom accent colour for the border (any CSS colour), **`size`** the
|
|
567
|
+
dimensions. The focus ring is the global [`--focus-ring`](../colors.md#focus-ring).
|
|
568
|
+
|
|
569
|
+
```tsx
|
|
570
|
+
<Input tone="#e0457b" label="Custom accent" />
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
For everything else, `<a-input>` is shadow-DOM — style its **parts** with plain
|
|
574
|
+
CSS: `::part(field | input | label | leading | trailing | clear | hint)`, plus the
|
|
575
|
+
`:state(filled)` / `:state(invalid)` state hooks and `:focus-within::part(field)`
|
|
576
|
+
for the focused state. The clear button is a real `<a-button>` in the `clear` slot,
|
|
577
|
+
so style it directly. `light-dark()` works too (Anta sets `color-scheme` from its
|
|
578
|
+
theme toggle).
|
|
579
|
+
|
|
580
|
+
```css
|
|
581
|
+
a-input::part(field) { border-radius: 999px; }
|
|
582
|
+
a-input::part(input) { font-variant-numeric: tabular-nums; }
|
|
583
|
+
a-input::part(label) { text-transform: uppercase; }
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
The border is drawn as an **inset `box-shadow`**, not a real `border`, so its width
|
|
587
|
+
(`0.5px` → `1px` when a `status` is set) and placement never change the box size —
|
|
588
|
+
the field height stays locked to the matching Button. Drop the leading `inset` to
|
|
589
|
+
draw the border as an outset ring instead. You can also re-point an `--input-*`
|
|
590
|
+
colour token on one instance (`style={{ '--input-border': 'var(--border-1)' }}`)
|
|
591
|
+
or a wrapping selector; the resolver lives in `@layer anta`, so any un-layered rule
|
|
592
|
+
of yours wins.
|
|
593
|
+
|
|
594
|
+
The `.fancy` class is just for the demo — a borderless pill that fills dark-grey
|
|
595
|
+
and darkens on focus in light mode, and the inverse in dark:
|
|
596
|
+
|
|
597
|
+
```css
|
|
598
|
+
/* a borderless pill — the resting border is a box-shadow, so clear it. Dark-grey
|
|
599
|
+
fill that darkens on focus, white text; the inverse in dark mode via light-dark()
|
|
600
|
+
(Anta drives color-scheme from its theme toggle). */
|
|
601
|
+
a-input.fancy::part(field) {
|
|
602
|
+
border-radius: 999px;
|
|
603
|
+
box-shadow: none; /* remove the box-shadow border */
|
|
604
|
+
background: light-dark(#3a3a3a, #ececec);
|
|
605
|
+
padding-block: 10px;
|
|
606
|
+
padding-inline: 20px; /* roomier */
|
|
607
|
+
}
|
|
608
|
+
a-input.fancy::part(input) { color: light-dark(#ffffff, #1a1a1a); }
|
|
609
|
+
a-input.fancy:focus-within::part(field) { background: light-dark(#161616, #ffffff); }
|
|
610
|
+
```
|