actual-css 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -1
- package/README.md +24 -9
- package/dist/actual-themes.min.css +1 -0
- package/dist/actual.css +1532 -1114
- package/dist/actual.js +1 -3
- package/dist/actual.min.css +1 -1
- package/dist/optional.css +1048 -0
- package/dist/optional.min.css +1 -0
- package/docs/design-notes/browser-support.md +94 -14
- package/docs/design-notes/cascade-layer.md +7 -6
- package/docs/design-notes/enhancement-contract.md +11 -14
- package/docs/design-notes/forced-colors.md +105 -0
- package/docs/design-notes/naming.md +2 -1
- package/docs/design-notes/platform-alignment.md +492 -0
- package/docs/design-notes/references.md +8 -0
- package/docs/design-notes/widget-primitives.md +37 -1
- package/docs/navigation.json +135 -0
- package/docs/pages/components/accordion.md +80 -0
- package/docs/pages/components/alert.md +203 -0
- package/docs/pages/components/aura.md +35 -0
- package/docs/pages/components/avatar.md +95 -0
- package/docs/pages/components/badge.md +102 -0
- package/docs/pages/components/breadcrumb.md +19 -0
- package/docs/pages/components/button.md +210 -0
- package/docs/pages/components/card.md +195 -0
- package/docs/pages/components/chat.md +50 -0
- package/docs/pages/components/dialog.md +367 -0
- package/docs/pages/components/drawer.md +113 -0
- package/docs/pages/components/fab.md +118 -0
- package/docs/pages/components/key.md +33 -0
- package/docs/pages/components/meter.md +23 -0
- package/docs/pages/components/navbar.md +92 -0
- package/docs/pages/components/pagination.md +55 -0
- package/docs/pages/components/progress.md +29 -0
- package/docs/pages/components/skeleton.md +38 -0
- package/docs/pages/components/spinner.md +93 -0
- package/docs/pages/components/table.md +102 -0
- package/docs/pages/enhancements/flyout.md +391 -0
- package/docs/pages/enhancements/overview.md +36 -0
- package/docs/pages/enhancements/scrollspy.md +71 -0
- package/docs/pages/enhancements/status-bar.md +72 -0
- package/docs/pages/enhancements/tabs.md +80 -0
- package/docs/pages/enhancements/tooltip.md +130 -0
- package/docs/pages/examples/overview.md +43 -0
- package/docs/pages/forms/choice-cards.md +102 -0
- package/docs/pages/forms/controls.md +32 -0
- package/docs/pages/forms/detached-actions.md +59 -0
- package/docs/pages/forms/floating-field.md +88 -0
- package/docs/pages/forms/form-actions.md +56 -0
- package/docs/pages/forms/input-policy.md +91 -0
- package/docs/pages/forms/joined-controls.md +37 -0
- package/docs/pages/forms/labels.md +42 -0
- package/docs/pages/forms/otp.md +50 -0
- package/docs/pages/forms/overview.md +188 -0
- package/docs/pages/forms/password.md +40 -0
- package/docs/pages/forms/range.md +17 -0
- package/docs/pages/forms/states-and-sizes.md +321 -0
- package/docs/pages/forms/sticky-actions.md +187 -0
- package/docs/pages/forms/switch.md +31 -0
- package/docs/pages/forms/validation.md +241 -0
- package/docs/{mental-model.md → pages/foundations/philosophy.md} +33 -18
- package/docs/pages/foundations/theming.md +99 -0
- package/docs/pages/foundations/tokens.md +421 -0
- package/docs/{typography.md → pages/foundations/typography.md} +27 -9
- package/docs/pages/guides/bootstrap.md +438 -0
- package/docs/pages/guides/introduction.md +157 -0
- package/docs/pages/guides/optional-components.md +33 -0
- package/docs/pages/guides/progressive-enhancement.md +686 -0
- package/docs/pages/guides/tailwind.md +455 -0
- package/docs/pages/layout/app-shell.md +25 -0
- package/docs/pages/layout/center.md +29 -0
- package/docs/pages/layout/cluster.md +59 -0
- package/docs/pages/layout/frame.md +30 -0
- package/docs/pages/layout/grid.md +142 -0
- package/docs/pages/layout/list.md +41 -0
- package/docs/pages/layout/media.md +28 -0
- package/docs/pages/layout/overview.md +37 -0
- package/docs/pages/layout/scroll-snap.md +54 -0
- package/docs/pages/layout/sidebar-layout.md +27 -0
- package/docs/pages/layout/stack.md +55 -0
- package/docs/pages/layout/switcher.md +36 -0
- package/docs/pages/layout/topbar.md +23 -0
- package/docs/pages/patterns/actions.md +52 -0
- package/docs/pages/patterns/code-block.md +36 -0
- package/docs/pages/patterns/exclusive-accordion.md +16 -0
- package/docs/pages/patterns/hierarchical-nav.md +26 -0
- package/docs/pages/patterns/icon-input.md +25 -0
- package/docs/pages/patterns/intent-nesting.md +3 -0
- package/docs/pages/patterns/navigation-list.md +235 -0
- package/docs/pages/patterns/overview.md +58 -0
- package/docs/pages/patterns/structured-lists.md +47 -0
- package/docs/pages/utilities/accessibility.md +34 -0
- package/docs/pages/utilities/flex.md +105 -0
- package/docs/pages/utilities/links.md +18 -0
- package/docs/pages/utilities/list.md +32 -0
- package/docs/pages/utilities/overflow.md +43 -0
- package/docs/pages/utilities/overview.md +18 -0
- package/docs/pages/utilities/shape.md +25 -0
- package/docs/pages/utilities/sizing-wrapping.md +115 -0
- package/docs/pages/utilities/spacing.md +79 -0
- package/docs/pages/utilities/text.md +43 -0
- package/llms.txt +4 -4
- package/package.json +21 -12
- package/src/css/actual.css +16 -2
- package/src/css/base.css +3 -5
- package/src/css/components/accordion.css +18 -30
- package/src/css/components/actions.css +4 -1
- package/src/css/components/alert.css +139 -102
- package/src/css/components/avatar.css +16 -10
- package/src/css/components/badge.css +69 -42
- package/src/css/components/breadcrumb.css +7 -15
- package/src/css/components/busy.css +6 -0
- package/src/css/components/button.css +84 -34
- package/src/css/components/card.css +26 -38
- package/src/css/components/drawer.css +30 -13
- package/src/css/components/flyout.css +39 -109
- package/src/css/components/index.css +1 -0
- package/src/css/components/join.css +14 -2
- package/src/css/components/key.css +2 -0
- package/src/css/components/menu.css +125 -0
- package/src/css/components/meter.css +0 -23
- package/src/css/components/modal.css +162 -7
- package/src/css/components/navbar.css +30 -23
- package/src/css/components/overline.css +8 -9
- package/src/css/components/pagination.css +1 -1
- package/src/css/components/progress.css +8 -26
- package/src/css/components/skeleton.css +7 -0
- package/src/css/components/spinner.css +6 -7
- package/src/css/components/status-bar.css +15 -24
- package/src/css/components/surface.css +2 -8
- package/src/css/components/tab.css +19 -48
- package/src/css/components/table.css +7 -5
- package/src/css/components/tooltip.css +14 -7
- package/src/css/focus.css +14 -38
- package/src/css/forms/choice-card.css +13 -14
- package/src/css/forms/choice.css +21 -27
- package/src/css/forms/control.css +18 -17
- package/src/css/forms/custom-select.css +9 -11
- package/src/css/forms/form-actions.css +14 -3
- package/src/css/forms/form.css +7 -4
- package/src/css/forms/index.css +4 -4
- package/src/css/forms/input-icon.css +30 -12
- package/src/css/forms/native.css +50 -11
- package/src/css/forms/range.css +17 -9
- package/src/css/forms/select.css +8 -8
- package/src/css/forms/switch.css +14 -12
- package/src/css/forms/validation.css +32 -5
- package/src/css/grid.css +32 -18
- package/src/css/intents.css +2 -2
- package/src/css/layout.css +45 -2
- package/src/css/optional/aura.css +93 -0
- package/src/css/optional/chat.css +105 -0
- package/src/css/optional/fab.css +104 -0
- package/src/css/optional/floating-field.css +87 -0
- package/src/css/optional/index.css +5 -0
- package/src/css/optional/layout-extra.css +7 -4
- package/src/css/optional/otp.css +115 -0
- package/src/css/optional/scroll-snap.css +6 -0
- package/src/css/optional/scroller.css +13 -7
- package/src/css/optional/typography-fluid.css +5 -0
- package/src/css/optional/utilities-extra.css +17 -2
- package/src/css/prose.css +18 -8
- package/src/css/reset.css +0 -10
- package/src/css/theme.css +21 -5
- package/src/css/themes/cyberpunk.css +1 -1
- package/src/css/themes/dim.css +3 -3
- package/src/css/themes/edge.css +8 -10
- package/src/css/themes/index.css +8 -4
- package/src/css/themes/{dark.css → indigo.css} +5 -4
- package/src/css/themes/neon.css +5 -5
- package/src/css/themes/square.css +0 -3
- package/src/css/tokens.css +74 -20
- package/src/css/utilities.css +8 -10
- package/src/css/variants.css +51 -66
- package/src/js/command.js +1 -3
- package/src/js/context-menu.js +67 -35
- package/src/js/dialog.js +68 -79
- package/src/js/dismiss.js +15 -0
- package/src/js/enhance.js +54 -23
- package/src/js/enhancement-loader.js +301 -0
- package/src/js/events.js +3 -0
- package/src/js/filter.js +3 -3
- package/src/js/floating.js +101 -131
- package/src/js/flyout.js +104 -87
- package/src/js/focus-group.js +116 -0
- package/src/js/focus.js +6 -1
- package/src/js/index.js +4 -2
- package/src/js/input.js +1 -1
- package/src/js/keys.js +27 -0
- package/src/js/mask.js +7 -6
- package/src/js/menu.js +87 -34
- package/src/js/parse-config.js +47 -0
- package/src/js/password.js +9 -1
- package/src/js/scrollspy.js +12 -4
- package/src/js/selectors.js +0 -7
- package/src/js/status.js +3 -1
- package/src/js/surface.js +187 -136
- package/src/js/tab.js +36 -53
- package/src/js/tooltip.js +70 -35
- package/src/js/validation.js +34 -8
- package/dist/actual.js.map +0 -31
- package/docs/README.md +0 -28
- package/docs/components.md +0 -860
- package/docs/design-notes/previous-work.md +0 -5
- package/docs/forms.md +0 -1265
- package/docs/javascript.md +0 -496
- package/docs/layout.md +0 -480
- package/docs/patterns.md +0 -504
- package/docs/reserved.md +0 -7
- package/docs/tokens.md +0 -455
- package/docs/ui.md +0 -929
- package/docs/utilities.md +0 -384
- package/src/css/dialog-fallback.css +0 -56
- package/src/js/dialog-fallback.js +0 -163
package/docs/forms.md
DELETED
|
@@ -1,1265 +0,0 @@
|
|
|
1
|
-
# Forms
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
> Cohesive set of form elements that share focus styles, helpers, and validation patterns.
|
|
6
|
-
|
|
7
|
-
- Form controls are styled with explicit classes, not by parent scope.
|
|
8
|
-
- Control classes: `.input`, `.textarea`, `.select`, `.check`, `.radio`, `.switch`, `.range`, `.file`.
|
|
9
|
-
- Reusable layout classes: `.field`, `.field-label`, `.field-help`, `.field-error`, `.field-group`, `.choice`, `.form-actions`, `.form-actions.sticky`.
|
|
10
|
-
- No floating labels shipped — the blocks demo shows a userland recipe for teams that want them.
|
|
11
|
-
- Proper focus style that preserves keyboard navigation.
|
|
12
|
-
- `.field-group` and `.choice` provide layout without depending on parent scope.
|
|
13
|
-
- Customizable select is progressive enhancement only — the native select remains the baseline.
|
|
14
|
-
|
|
15
|
-
```html
|
|
16
|
-
<form novalidate>
|
|
17
|
-
<div class="stack">
|
|
18
|
-
|
|
19
|
-
<label class="field">
|
|
20
|
-
<span class="field-label">Full name <span class="required-mark" aria-hidden="true">*</span></span>
|
|
21
|
-
<input class="input" type="text" autocomplete="name" required placeholder="Jane Doe" />
|
|
22
|
-
</label>
|
|
23
|
-
|
|
24
|
-
<label class="field">
|
|
25
|
-
<span class="field-label">Email address <span class="required-mark" aria-hidden="true">*</span></span>
|
|
26
|
-
<input class="input" type="email"
|
|
27
|
-
autocomplete="email"
|
|
28
|
-
required
|
|
29
|
-
aria-invalid="true"
|
|
30
|
-
aria-describedby="email-error"
|
|
31
|
-
value="jane@" />
|
|
32
|
-
<span class="field-error" id="email-error" role="alert">
|
|
33
|
-
Enter a valid email address
|
|
34
|
-
</span>
|
|
35
|
-
</label>
|
|
36
|
-
|
|
37
|
-
<label class="field">
|
|
38
|
-
<span class="field-label">Website</span>
|
|
39
|
-
<input class="input" type="url"
|
|
40
|
-
autocomplete="url"
|
|
41
|
-
aria-describedby="website-help"
|
|
42
|
-
placeholder="https://example.com" />
|
|
43
|
-
<span class="field-help" id="website-help">Your public portfolio or personal site</span>
|
|
44
|
-
</label>
|
|
45
|
-
|
|
46
|
-
<label class="field">
|
|
47
|
-
<span class="field-label">Bio</span>
|
|
48
|
-
<textarea class="textarea" aria-describedby="bio-help"
|
|
49
|
-
maxlength="200"
|
|
50
|
-
placeholder="Tell us a bit about yourself…"></textarea>
|
|
51
|
-
<span class="field-help" id="bio-help">Max 200 characters. Shown on your public profile.</span>
|
|
52
|
-
</label>
|
|
53
|
-
|
|
54
|
-
<label class="field">
|
|
55
|
-
<span class="field-label">Language</span>
|
|
56
|
-
<select class="select" autocomplete="language">
|
|
57
|
-
<option value="">Choose a language</option>
|
|
58
|
-
<optgroup label="Available languages">
|
|
59
|
-
<option value="en" selected>English</option>
|
|
60
|
-
<option value="fr">Français</option>
|
|
61
|
-
<option value="nl">Nederlands</option>
|
|
62
|
-
</optgroup>
|
|
63
|
-
<optgroup label="Not yet available">
|
|
64
|
-
<option value="de" disabled>Deutsch</option>
|
|
65
|
-
</optgroup>
|
|
66
|
-
</select>
|
|
67
|
-
</label>
|
|
68
|
-
|
|
69
|
-
<fieldset class="field-group">
|
|
70
|
-
<legend class="field-label">Availability</legend>
|
|
71
|
-
<div class="stack">
|
|
72
|
-
<label class="choice">
|
|
73
|
-
<input class="radio" type="radio" name="availability" value="available" checked />
|
|
74
|
-
<span>Available for work</span>
|
|
75
|
-
</label>
|
|
76
|
-
<label class="choice">
|
|
77
|
-
<input class="radio" type="radio" name="availability" value="open" />
|
|
78
|
-
<span>Open to opportunities</span>
|
|
79
|
-
</label>
|
|
80
|
-
<label class="choice">
|
|
81
|
-
<input class="radio" type="radio" name="availability" value="unavailable" />
|
|
82
|
-
<span>Not available</span>
|
|
83
|
-
</label>
|
|
84
|
-
</div>
|
|
85
|
-
</fieldset>
|
|
86
|
-
|
|
87
|
-
<label class="field">
|
|
88
|
-
<span class="field-label">Experience level</span>
|
|
89
|
-
<input class="range" type="range"
|
|
90
|
-
min="0"
|
|
91
|
-
max="10"
|
|
92
|
-
value="5"
|
|
93
|
-
aria-valuetext="5 years"
|
|
94
|
-
aria-describedby="range-help" />
|
|
95
|
-
<span class="field-help" id="range-help">Years of professional experience</span>
|
|
96
|
-
</label>
|
|
97
|
-
|
|
98
|
-
<label class="choice">
|
|
99
|
-
<input class="switch" type="checkbox" role="switch" checked />
|
|
100
|
-
<span>
|
|
101
|
-
<span class="field-label">Email notifications</span>
|
|
102
|
-
<span class="field-help">Receive updates about your account activity</span>
|
|
103
|
-
</span>
|
|
104
|
-
</label>
|
|
105
|
-
|
|
106
|
-
<label class="choice">
|
|
107
|
-
<input class="check" type="checkbox" />
|
|
108
|
-
<span>
|
|
109
|
-
I agree to the <a href="#">terms of service</a>
|
|
110
|
-
<span class="field-help">Required to create an account</span>
|
|
111
|
-
</span>
|
|
112
|
-
</label>
|
|
113
|
-
|
|
114
|
-
<label class="field">
|
|
115
|
-
<span class="field-label">Profile picture</span>
|
|
116
|
-
<input class="file" type="file" accept="image/*" aria-describedby="file-help" />
|
|
117
|
-
<span class="field-help" id="file-help">JPG, PNG or WebP — max 2 MB</span>
|
|
118
|
-
</label>
|
|
119
|
-
|
|
120
|
-
<label class="field">
|
|
121
|
-
<span class="field-label">Username</span>
|
|
122
|
-
<input class="input" type="text"
|
|
123
|
-
value="janedoe"
|
|
124
|
-
disabled
|
|
125
|
-
aria-describedby="username-help" />
|
|
126
|
-
<span class="field-help" id="username-help">Contact support to change your username</span>
|
|
127
|
-
</label>
|
|
128
|
-
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
<div class="form-actions">
|
|
132
|
-
<button type="button" class="btn outline">Cancel</button>
|
|
133
|
-
<button type="submit" class="btn primary">Save changes</button>
|
|
134
|
-
</div>
|
|
135
|
-
</form>
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Notes
|
|
139
|
-
|
|
140
|
-
- `required` is enough on native inputs. `aria-required="true"` is redundant and not used in these examples.
|
|
141
|
-
- `required` is the semantic source of truth. Add `<span class="required-mark" aria-hidden="true">*</span>` inside a label only when a visual required marker is wanted; the framework does not add one automatically.
|
|
142
|
-
- `.field` is the canonical field wrapper. It works on `<label>` and `<div>`.
|
|
143
|
-
- `.field-label` is element-agnostic. Use it on `<span>` inside a wrapped label, on `<label for="…">` in a detached layout, or on `<legend>` inside a fieldset.
|
|
144
|
-
- `.field-group` styles a `<fieldset class="field-group">` as a grouped field container.
|
|
145
|
-
- Use native `<optgroup>` and `disabled` options in `.select` controls when applicable; the customizable picker preserves both as a progressive enhancement.
|
|
146
|
-
- `.choice` is the choice-label API. Use `.check` or `.radio` on the nested control. The control goes first, the label group second, so multi-line labels align the control with the first line.
|
|
147
|
-
- For a switch, use `class="switch"` and `role="switch"`.
|
|
148
|
-
- Core range controls stay native and are enhanced by `accent-color`. An optional richer range skin may come later if real projects need it.
|
|
149
|
-
- `.form-actions` carries a default top margin. Override it with `--form-actions-margin-block-start`. It is class-only and may live inside or outside `<form>`. See Detached Actions below.
|
|
150
|
-
- `.join` visually joins adjacent controls into a single unit. Use `.join-addon` for static prefix/suffix content. The container still carries `role="group"` for accessibility.
|
|
151
|
-
|
|
152
|
-
### Optional automatic required marks
|
|
153
|
-
|
|
154
|
-
If a form consistently marks every required field, opt into automatic visual markers in application CSS. Scope the rule to that form so other forms stay explicit, and retain a per-field opt-out:
|
|
155
|
-
|
|
156
|
-
```css
|
|
157
|
-
form[data-required-marks="auto"]
|
|
158
|
-
.field:not([data-required-mark="none"]):has(:required)
|
|
159
|
-
> .field-label::after {
|
|
160
|
-
content: " *";
|
|
161
|
-
color: var(--danger);
|
|
162
|
-
}
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
`required` remains the accessible source of truth; this recipe adds only a visual cue. Use `data-required-mark="none"` on a `.field` when a required control should not receive the generated marker.
|
|
166
|
-
|
|
167
|
-
## States And Sizes
|
|
168
|
-
|
|
169
|
-
> Read-only, disabled, and shared size modifiers for form controls.
|
|
170
|
-
|
|
171
|
-
Use `readonly` for text-like values that can still be focused, selected, and submitted. Use `disabled` for unavailable controls that should not receive focus or submit a value.
|
|
172
|
-
|
|
173
|
-
The `.sm` and `.lg` modifiers set the shared control tokens used by inputs, selects, textareas, buttons, and switches. Put the modifier on the control itself, or on a `.field` wrapper when the whole field should share that size.
|
|
174
|
-
|
|
175
|
-
```html
|
|
176
|
-
<form novalidate>
|
|
177
|
-
<fieldset class="field-group">
|
|
178
|
-
<legend class="field-label">Text control states</legend>
|
|
179
|
-
<div class="stack container-query">
|
|
180
|
-
<div class="grid-3 items-start">
|
|
181
|
-
<label class="field">
|
|
182
|
-
<span class="field-label">Editable</span>
|
|
183
|
-
<input class="input" type="text" value="Ocean Beach Clinic" />
|
|
184
|
-
</label>
|
|
185
|
-
|
|
186
|
-
<label class="field">
|
|
187
|
-
<span class="field-label">Read-only</span>
|
|
188
|
-
<input class="input" type="text" value="INV-2048" readonly />
|
|
189
|
-
<span class="field-help">Focusable and submitted with the form.</span>
|
|
190
|
-
</label>
|
|
191
|
-
|
|
192
|
-
<label class="field">
|
|
193
|
-
<span class="field-label">Disabled</span>
|
|
194
|
-
<input class="input" type="text" value="Locked by billing" disabled />
|
|
195
|
-
<span class="field-help">Unavailable and not submitted.</span>
|
|
196
|
-
</label>
|
|
197
|
-
</div>
|
|
198
|
-
|
|
199
|
-
<div class="grid-3 items-start">
|
|
200
|
-
<label class="field">
|
|
201
|
-
<span class="field-label">Editable notes</span>
|
|
202
|
-
<textarea class="textarea">Patient asked for an invoice copy.</textarea>
|
|
203
|
-
</label>
|
|
204
|
-
|
|
205
|
-
<label class="field">
|
|
206
|
-
<span class="field-label">Read-only notes</span>
|
|
207
|
-
<textarea class="textarea" readonly>Insurance details imported from the patient record.</textarea>
|
|
208
|
-
</label>
|
|
209
|
-
|
|
210
|
-
<label class="field">
|
|
211
|
-
<span class="field-label">Disabled notes</span>
|
|
212
|
-
<textarea class="textarea" disabled>Notes are disabled until the transaction is selected.</textarea>
|
|
213
|
-
</label>
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
</fieldset>
|
|
217
|
-
|
|
218
|
-
<fieldset class="field-group">
|
|
219
|
-
<legend class="field-label">Choice and select states</legend>
|
|
220
|
-
<div class="stack container-query">
|
|
221
|
-
<div class="grid-3 items-start">
|
|
222
|
-
<label class="field">
|
|
223
|
-
<span class="field-label">Editable select</span>
|
|
224
|
-
<select class="select">
|
|
225
|
-
<option>Card</option>
|
|
226
|
-
<option>Bank transfer</option>
|
|
227
|
-
<option>Cash</option>
|
|
228
|
-
</select>
|
|
229
|
-
</label>
|
|
230
|
-
|
|
231
|
-
<label class="field">
|
|
232
|
-
<span class="field-label">Invalid select</span>
|
|
233
|
-
<select class="select" aria-invalid="true">
|
|
234
|
-
<option>Choose a method</option>
|
|
235
|
-
<option>Card</option>
|
|
236
|
-
<option>Bank transfer</option>
|
|
237
|
-
</select>
|
|
238
|
-
<span class="field-error">Choose a payment method.</span>
|
|
239
|
-
</label>
|
|
240
|
-
|
|
241
|
-
<label class="field">
|
|
242
|
-
<span class="field-label">Disabled select</span>
|
|
243
|
-
<select class="select" disabled>
|
|
244
|
-
<option>Card</option>
|
|
245
|
-
<option>Bank transfer</option>
|
|
246
|
-
<option>Cash</option>
|
|
247
|
-
</select>
|
|
248
|
-
</label>
|
|
249
|
-
</div>
|
|
250
|
-
|
|
251
|
-
<div class="grid-5 items-start">
|
|
252
|
-
<label class="choice">
|
|
253
|
-
<input class="check" type="checkbox" />
|
|
254
|
-
<span>
|
|
255
|
-
<span class="field-label">Unchecked checkbox</span>
|
|
256
|
-
<span class="field-help">Available and not selected.</span>
|
|
257
|
-
</span>
|
|
258
|
-
</label>
|
|
259
|
-
|
|
260
|
-
<label class="choice">
|
|
261
|
-
<input class="check" type="checkbox" checked />
|
|
262
|
-
<span>
|
|
263
|
-
<span class="field-label">Checked checkbox</span>
|
|
264
|
-
<span class="field-help">Available and selected.</span>
|
|
265
|
-
</span>
|
|
266
|
-
</label>
|
|
267
|
-
|
|
268
|
-
<label class="choice">
|
|
269
|
-
<input class="check" type="checkbox" checked disabled />
|
|
270
|
-
<span>
|
|
271
|
-
<span class="field-label">Disabled checked</span>
|
|
272
|
-
<span class="field-help">Selected but unavailable.</span>
|
|
273
|
-
</span>
|
|
274
|
-
</label>
|
|
275
|
-
|
|
276
|
-
<label class="choice">
|
|
277
|
-
<input class="check" type="checkbox" disabled />
|
|
278
|
-
<span>
|
|
279
|
-
<span class="field-label">Disabled unchecked</span>
|
|
280
|
-
<span class="field-help">Unavailable and not selected.</span>
|
|
281
|
-
</span>
|
|
282
|
-
</label>
|
|
283
|
-
|
|
284
|
-
<label class="choice">
|
|
285
|
-
<input class="check" type="checkbox" id="indet-demo" />
|
|
286
|
-
<span>
|
|
287
|
-
<span class="field-label">Indeterminate checkbox</span>
|
|
288
|
-
<span class="field-help">Partially selected — set via <code>element.indeterminate = true</code>. Try it: <button class="btn text sm" type="button" onclick="document.getElementById('indet-demo').indeterminate = !document.getElementById('indet-demo').indeterminate">toggle</button></span>
|
|
289
|
-
</span>
|
|
290
|
-
</label>
|
|
291
|
-
</div>
|
|
292
|
-
|
|
293
|
-
<div class="grid-4 items-start">
|
|
294
|
-
<label class="choice">
|
|
295
|
-
<input class="radio" type="radio" name="demo-radio-state" />
|
|
296
|
-
<span>
|
|
297
|
-
<span class="field-label">Unselected radio</span>
|
|
298
|
-
<span class="field-help">Available and not selected.</span>
|
|
299
|
-
</span>
|
|
300
|
-
</label>
|
|
301
|
-
|
|
302
|
-
<label class="choice">
|
|
303
|
-
<input class="radio" type="radio" name="demo-radio-state" checked />
|
|
304
|
-
<span>
|
|
305
|
-
<span class="field-label">Selected radio</span>
|
|
306
|
-
<span class="field-help">Available and selected.</span>
|
|
307
|
-
</span>
|
|
308
|
-
</label>
|
|
309
|
-
|
|
310
|
-
<label class="choice">
|
|
311
|
-
<input class="radio" type="radio" name="demo-radio-disabled" checked disabled />
|
|
312
|
-
<span>
|
|
313
|
-
<span class="field-label">Disabled selected</span>
|
|
314
|
-
<span class="field-help">Selected but unavailable.</span>
|
|
315
|
-
</span>
|
|
316
|
-
</label>
|
|
317
|
-
|
|
318
|
-
<label class="choice">
|
|
319
|
-
<input class="radio" type="radio" name="demo-radio-disabled" disabled />
|
|
320
|
-
<span>
|
|
321
|
-
<span class="field-label">Disabled unselected</span>
|
|
322
|
-
<span class="field-help">Unavailable and not selected.</span>
|
|
323
|
-
</span>
|
|
324
|
-
</label>
|
|
325
|
-
</div>
|
|
326
|
-
|
|
327
|
-
<div class="grid-4 items-start">
|
|
328
|
-
<label class="choice">
|
|
329
|
-
<input class="switch" type="checkbox" role="switch" />
|
|
330
|
-
<span>
|
|
331
|
-
<span class="field-label">Switch off</span>
|
|
332
|
-
<span class="field-help">Available and inactive.</span>
|
|
333
|
-
</span>
|
|
334
|
-
</label>
|
|
335
|
-
|
|
336
|
-
<label class="choice">
|
|
337
|
-
<input class="switch" type="checkbox" role="switch" checked />
|
|
338
|
-
<span>
|
|
339
|
-
<span class="field-label">Switch on</span>
|
|
340
|
-
<span class="field-help">Available and enabled.</span>
|
|
341
|
-
</span>
|
|
342
|
-
</label>
|
|
343
|
-
|
|
344
|
-
<label class="choice">
|
|
345
|
-
<input class="switch" type="checkbox" role="switch" checked disabled />
|
|
346
|
-
<span>
|
|
347
|
-
<span class="field-label">Disabled on</span>
|
|
348
|
-
<span class="field-help">On but unavailable.</span>
|
|
349
|
-
</span>
|
|
350
|
-
</label>
|
|
351
|
-
|
|
352
|
-
<label class="choice">
|
|
353
|
-
<input class="switch" type="checkbox" role="switch" disabled />
|
|
354
|
-
<span>
|
|
355
|
-
<span class="field-label">Disabled off</span>
|
|
356
|
-
<span class="field-help">Off and unavailable.</span>
|
|
357
|
-
</span>
|
|
358
|
-
</label>
|
|
359
|
-
</div>
|
|
360
|
-
</div>
|
|
361
|
-
</fieldset>
|
|
362
|
-
|
|
363
|
-
<fieldset class="field-group">
|
|
364
|
-
<legend class="field-label">Control sizes</legend>
|
|
365
|
-
<div class="stack container-query">
|
|
366
|
-
<div class="grid-3 items-start">
|
|
367
|
-
<label class="field sm">
|
|
368
|
-
<span class="field-label">Small field</span>
|
|
369
|
-
<input class="input" type="text" value="sm" />
|
|
370
|
-
</label>
|
|
371
|
-
|
|
372
|
-
<label class="field">
|
|
373
|
-
<span class="field-label">Default field</span>
|
|
374
|
-
<input class="input" type="text" value="default" />
|
|
375
|
-
</label>
|
|
376
|
-
|
|
377
|
-
<label class="field lg">
|
|
378
|
-
<span class="field-label">Large field</span>
|
|
379
|
-
<input class="input" type="text" value="lg" />
|
|
380
|
-
</label>
|
|
381
|
-
</div>
|
|
382
|
-
|
|
383
|
-
<div class="grid-3 items-start">
|
|
384
|
-
<label class="field sm">
|
|
385
|
-
<span class="field-label">Small select</span>
|
|
386
|
-
<select class="select">
|
|
387
|
-
<option>Compact</option>
|
|
388
|
-
</select>
|
|
389
|
-
</label>
|
|
390
|
-
|
|
391
|
-
<label class="field">
|
|
392
|
-
<span class="field-label">Default select</span>
|
|
393
|
-
<select class="select">
|
|
394
|
-
<option>Default</option>
|
|
395
|
-
</select>
|
|
396
|
-
</label>
|
|
397
|
-
|
|
398
|
-
<label class="field lg">
|
|
399
|
-
<span class="field-label">Large select</span>
|
|
400
|
-
<select class="select">
|
|
401
|
-
<option>Comfortable</option>
|
|
402
|
-
</select>
|
|
403
|
-
</label>
|
|
404
|
-
</div>
|
|
405
|
-
|
|
406
|
-
<div class="grid-3 items-start">
|
|
407
|
-
<label class="field sm">
|
|
408
|
-
<span class="field-label">Small textarea</span>
|
|
409
|
-
<textarea class="textarea" rows="2">Short note</textarea>
|
|
410
|
-
</label>
|
|
411
|
-
|
|
412
|
-
<label class="field">
|
|
413
|
-
<span class="field-label">Default textarea</span>
|
|
414
|
-
<textarea class="textarea" rows="2">Default note</textarea>
|
|
415
|
-
</label>
|
|
416
|
-
|
|
417
|
-
<label class="field lg">
|
|
418
|
-
<span class="field-label">Large textarea</span>
|
|
419
|
-
<textarea class="textarea" rows="2">Comfortable note</textarea>
|
|
420
|
-
</label>
|
|
421
|
-
</div>
|
|
422
|
-
|
|
423
|
-
<div class="grid-3 items-start">
|
|
424
|
-
<button class="btn outline sm" type="button">Small</button>
|
|
425
|
-
<button class="btn outline" type="button">Default</button>
|
|
426
|
-
<button class="btn outline lg" type="button">Large</button>
|
|
427
|
-
</div>
|
|
428
|
-
|
|
429
|
-
<div class="grid-4 items-start">
|
|
430
|
-
<label class="choice sm">
|
|
431
|
-
<input class="check" type="checkbox" checked />
|
|
432
|
-
<span>Small checkbox</span>
|
|
433
|
-
</label>
|
|
434
|
-
<label class="choice">
|
|
435
|
-
<input class="check" type="checkbox" checked />
|
|
436
|
-
<span>Default checkbox</span>
|
|
437
|
-
</label>
|
|
438
|
-
<label class="choice lg">
|
|
439
|
-
<input class="check" type="checkbox" checked />
|
|
440
|
-
<span>Large checkbox</span>
|
|
441
|
-
</label>
|
|
442
|
-
<label class="choice">
|
|
443
|
-
<input class="check" type="checkbox" id="indet-size" />
|
|
444
|
-
<span>Indeterminate <button class="btn text sm" type="button" onclick="document.getElementById('indet-size').indeterminate = !document.getElementById('indet-size').indeterminate">toggle</button></span>
|
|
445
|
-
</label>
|
|
446
|
-
</div>
|
|
447
|
-
|
|
448
|
-
<div class="grid-3 items-start">
|
|
449
|
-
<label class="choice sm">
|
|
450
|
-
<input class="radio" type="radio" name="demo-radio-size" checked />
|
|
451
|
-
<span>Small radio</span>
|
|
452
|
-
</label>
|
|
453
|
-
<label class="choice">
|
|
454
|
-
<input class="radio" type="radio" name="demo-radio-size" />
|
|
455
|
-
<span>Default radio</span>
|
|
456
|
-
</label>
|
|
457
|
-
<label class="choice lg">
|
|
458
|
-
<input class="radio" type="radio" name="demo-radio-size" />
|
|
459
|
-
<span>Large radio</span>
|
|
460
|
-
</label>
|
|
461
|
-
</div>
|
|
462
|
-
|
|
463
|
-
<div class="grid-3 items-start">
|
|
464
|
-
<label class="choice sm">
|
|
465
|
-
<input class="switch" type="checkbox" role="switch" checked />
|
|
466
|
-
<span>Small switch</span>
|
|
467
|
-
</label>
|
|
468
|
-
<label class="choice">
|
|
469
|
-
<input class="switch" type="checkbox" role="switch" checked />
|
|
470
|
-
<span>Default switch</span>
|
|
471
|
-
</label>
|
|
472
|
-
<label class="choice lg">
|
|
473
|
-
<input class="switch" type="checkbox" role="switch" checked />
|
|
474
|
-
<span>Large switch</span>
|
|
475
|
-
</label>
|
|
476
|
-
</div>
|
|
477
|
-
</div>
|
|
478
|
-
</fieldset>
|
|
479
|
-
</form>
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
## Labels
|
|
483
|
-
|
|
484
|
-
> The field wrapper is `.field`. Choose between a wrapped `<label>` and a detached `for`/`id` pair based on the layout.
|
|
485
|
-
|
|
486
|
-
Use a **wrapped label** for simple hand-authored forms. The whole label area becomes the click target, there is no `id` noise, and the markup is harder to break.
|
|
487
|
-
|
|
488
|
-
```html
|
|
489
|
-
<label class="field">
|
|
490
|
-
<span class="field-label">Full name</span>
|
|
491
|
-
<input class="input" type="text" name="name" autocomplete="name" />
|
|
492
|
-
</label>
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
Use **`for`/`id`** when the layout needs the label and control in different grid areas, when the markup is generated by a backend or form library, or when a single label is paired with multiple controls. `.field-label` is element-agnostic, so it works on a detached `<label>`.
|
|
496
|
-
|
|
497
|
-
```html
|
|
498
|
-
<div class="field">
|
|
499
|
-
<label class="field-label" for="profile-email">
|
|
500
|
-
Email address <span class="required-mark" aria-hidden="true">*</span>
|
|
501
|
-
</label>
|
|
502
|
-
<input class="input"
|
|
503
|
-
id="profile-email"
|
|
504
|
-
name="email"
|
|
505
|
-
type="email"
|
|
506
|
-
autocomplete="email"
|
|
507
|
-
required
|
|
508
|
-
aria-describedby="profile-email-help" />
|
|
509
|
-
<span class="field-help" id="profile-email-help">
|
|
510
|
-
We only use this for account notifications.
|
|
511
|
-
</span>
|
|
512
|
-
</div>
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
The mixed shape `<label for="x">…<input id="x"></label>` is allowed by the spec but rarely useful. Prefer either pure wrapped or pure `for`/`id`.
|
|
516
|
-
|
|
517
|
-
## Joined Controls
|
|
518
|
-
|
|
519
|
-
> Use `.join` when controls need to be visually joined into a single unit.
|
|
520
|
-
|
|
521
|
-
Add `.join` to the wrapper that groups the controls. Use `.join-addon` for static prefix/suffix content such as currency symbols, units, or protocol text. Action buttons inside the group should use `.btn`. The wrapper should still carry `role="group"` with a label for accessibility.
|
|
522
|
-
|
|
523
|
-
When composing source files manually, import `components/join.css` after the controls it groups. `.join` writes child border-radius longhands directly so joined corners win at equal specificity.
|
|
524
|
-
|
|
525
|
-
```html
|
|
526
|
-
<div class="field">
|
|
527
|
-
<label class="field-label" id="amount-label" for="amount">Amount</label>
|
|
528
|
-
<div class="join" role="group" aria-labelledby="amount-label">
|
|
529
|
-
<span class="join-addon">$</span>
|
|
530
|
-
<input class="input" id="amount" name="amount" inputmode="decimal" />
|
|
531
|
-
<button class="btn outline" type="button">Clear</button>
|
|
532
|
-
</div>
|
|
533
|
-
<span class="field-help">Enter the invoice total before tax.</span>
|
|
534
|
-
</div>
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
`.join` handles border-radius and border collapsing between adjacent children. It works with any direct child — `.input`, `.btn`, `.select`, or `.join-addon`.
|
|
538
|
-
When an `.input`, `.textarea`, or `.select` receives keyboard-relevant focus,
|
|
539
|
-
the focus ring surrounds the complete joined field. Attached buttons keep their
|
|
540
|
-
own focus indicator when reached directly, so the actionable segment remains
|
|
541
|
-
identifiable.
|
|
542
|
-
|
|
543
|
-
## Input Policy
|
|
544
|
-
|
|
545
|
-
> Native input intent first. Actual's runtime filters values only when explicitly asked, and fixed-shape masks stay opt-in through `data-mask`.
|
|
546
|
-
|
|
547
|
-
`inputmode` changes the virtual keyboard but does not constrain the value. Actual
|
|
548
|
-
does not filter plain `inputmode` fields by default because numeric entry often
|
|
549
|
-
needs temporary, signed, grouped, or locale-specific values while the user types.
|
|
550
|
-
|
|
551
|
-
When the JavaScript runtime is loaded, Actual enhances `input[data-filter]`.
|
|
552
|
-
Textareas are left alone.
|
|
553
|
-
|
|
554
|
-
Add an explicit `data-filter` value when the field should enforce one of
|
|
555
|
-
Actual's small built-in filters. Unsupported filter names are ignored. An
|
|
556
|
-
empty `data-filter` never filters — `inputmode` is not read as a fallback,
|
|
557
|
-
even when it is `numeric` or `decimal`. `data-filter="numeric"` /
|
|
558
|
-
`data-filter="decimal"` are intentionally destructive (they rewrite the
|
|
559
|
-
value as the user types), unlike `inputmode`, which only hints the virtual
|
|
560
|
-
keyboard.
|
|
561
|
-
|
|
562
|
-
```html
|
|
563
|
-
<label class="field">
|
|
564
|
-
<span class="field-label">Quantity</span>
|
|
565
|
-
<input class="input" inputmode="numeric" data-filter="numeric" placeholder="42" />
|
|
566
|
-
</label>
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
```html
|
|
570
|
-
<label class="field">
|
|
571
|
-
<span class="field-label">Amount</span>
|
|
572
|
-
<input class="input" inputmode="decimal" data-filter="decimal" placeholder="12.34" />
|
|
573
|
-
</label>
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
### Built-in filters
|
|
577
|
-
|
|
578
|
-
The built-in filters are intentionally narrow. They are input helpers, not
|
|
579
|
-
domain validation.
|
|
580
|
-
|
|
581
|
-
- `numeric` — Keeps ASCII digits only: `0` through `9`.
|
|
582
|
-
- `decimal` — Converts `,` to `.`, keeps ASCII digits, and keeps the first `.` separator.
|
|
583
|
-
- `lower` — Converts the value with `toLocaleLowerCase()`.
|
|
584
|
-
- `upper` — Converts the value with `toLocaleUpperCase()`.
|
|
585
|
-
- `letters` — Keeps Unicode letters only. Digits, punctuation, symbols, and spaces are removed.
|
|
586
|
-
- `slug` — Normalizes accents, lowercases text, turns runs of non-letter and non-digit characters into `-`, collapses repeated `-`, and trims edge separators.
|
|
587
|
-
|
|
588
|
-
During direct typing, `slug` may keep a trailing `-` until the next character so
|
|
589
|
-
words do not merge while the user is still entering text.
|
|
590
|
-
|
|
591
|
-
Filters can be piped with `|` when a field needs more than one transform. They
|
|
592
|
-
run from left to right.
|
|
593
|
-
|
|
594
|
-
```html
|
|
595
|
-
<label class="field">
|
|
596
|
-
<span class="field-label">Slug</span>
|
|
597
|
-
<input class="input" data-filter="lower|slug" autocomplete="off" placeholder="release-notes" />
|
|
598
|
-
</label>
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
```html
|
|
602
|
-
<label class="field">
|
|
603
|
-
<span class="field-label">Code</span>
|
|
604
|
-
<input class="input" data-filter="upper|letters" autocomplete="off" placeholder="ABC" />
|
|
605
|
-
</label>
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
For signed numbers, locale formatting, currency rules, time ranges, or
|
|
609
|
-
app-specific character policies, use application code. Those rules are domain
|
|
610
|
-
policy, not a framework default. See [JavaScript](javascript.md) for custom
|
|
611
|
-
filter patterns built on `enhance()`.
|
|
612
|
-
|
|
613
|
-
### Pattern mask
|
|
614
|
-
|
|
615
|
-
Use `data-mask` when the input has a fixed shape. Tokens are `9` for a digit,
|
|
616
|
-
`a` for a letter, and `*` for any character.
|
|
617
|
-
|
|
618
|
-
```html
|
|
619
|
-
<label class="field">
|
|
620
|
-
<span class="field-label">Reference</span>
|
|
621
|
-
<input class="input" data-mask="aaa-999" autocomplete="off" placeholder="abc-123" />
|
|
622
|
-
</label>
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
```html
|
|
626
|
-
<label class="field">
|
|
627
|
-
<span class="field-label">Date</span>
|
|
628
|
-
<input class="input" data-mask="9999-99-99" inputmode="numeric" autocomplete="off" placeholder="yyyy-mm-dd" />
|
|
629
|
-
</label>
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
Masks enforce shape, not domain validity. Use application validation when a date
|
|
633
|
-
must reject impossible values such as `2026-13-40`.
|
|
634
|
-
|
|
635
|
-
## Password Toggle
|
|
636
|
-
|
|
637
|
-
> A password field reveals its value through a linked button. The markup is a
|
|
638
|
-
> plain command invoker; the behavior ships with the JS runtime
|
|
639
|
-
> (`actual-css/js` or `actual-css/js/password`).
|
|
640
|
-
|
|
641
|
-
Point a `button` at the input with `commandfor` and
|
|
642
|
-
`command="--password-toggle"`. Use `.join` when the button should sit attached
|
|
643
|
-
to the field.
|
|
644
|
-
|
|
645
|
-
```html
|
|
646
|
-
<div class="field">
|
|
647
|
-
<label class="field-label" for="pw">Password</label>
|
|
648
|
-
<div class="join">
|
|
649
|
-
<input class="input" type="password" id="pw" name="password"
|
|
650
|
-
autocomplete="current-password" required />
|
|
651
|
-
<button class="btn outline" type="button" commandfor="pw"
|
|
652
|
-
command="--password-toggle" aria-controls="pw"
|
|
653
|
-
aria-label="Show password" aria-pressed="false"><i class="ti ti-eye" aria-hidden="true"></i></button>
|
|
654
|
-
</div>
|
|
655
|
-
</div>
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
Start with `aria-pressed="false"`; this makes the toggle semantics available
|
|
659
|
-
before JavaScript runs. The runtime flips the input between `type="password"`
|
|
660
|
-
and `type="text"` and mirrors the state on every linked trigger, which
|
|
661
|
-
`.btn[aria-pressed="true"]` already styles. Keep the trigger label stable
|
|
662
|
-
("Show password") — `aria-pressed` announces the state, so a label that flips
|
|
663
|
-
to "Hide" reads as a double negation to screen readers. Style an icon swap off
|
|
664
|
-
the attribute instead:
|
|
665
|
-
|
|
666
|
-
```css
|
|
667
|
-
[command="--password-toggle"][aria-pressed="true"] .icon-show { display: none; }
|
|
668
|
-
[command="--password-toggle"]:not([aria-pressed="true"]) .icon-hide { display: none; }
|
|
669
|
-
```
|
|
670
|
-
|
|
671
|
-
A revealed value does not survive navigation: a non-canceled form submit and
|
|
672
|
-
`pagehide` both revert the input to hidden, so back navigation cannot land on
|
|
673
|
-
a readable password. A canceled submit (failed validation, ajax) keeps the
|
|
674
|
-
value revealed.
|
|
675
|
-
|
|
676
|
-
## Detached Actions
|
|
677
|
-
|
|
678
|
-
> `.form-actions` is class-only. It may live inside or outside the `<form>`. Use the native `form` attribute to connect a detached submit button to its form.
|
|
679
|
-
|
|
680
|
-
The simplest case stays inside the form.
|
|
681
|
-
|
|
682
|
-
```html
|
|
683
|
-
<form novalidate>
|
|
684
|
-
<div class="stack">
|
|
685
|
-
<!-- fields -->
|
|
686
|
-
</div>
|
|
687
|
-
|
|
688
|
-
<div class="form-actions">
|
|
689
|
-
<button type="button" class="btn outline">Cancel</button>
|
|
690
|
-
<button type="submit" class="btn primary">Save changes</button>
|
|
691
|
-
</div>
|
|
692
|
-
</form>
|
|
693
|
-
```
|
|
694
|
-
|
|
695
|
-
For sticky page footers, dialog footers, card footers, and split layouts, detach the actions and connect the submit button with `form="<id>"`.
|
|
696
|
-
|
|
697
|
-
```html
|
|
698
|
-
<form id="profile-form" novalidate>
|
|
699
|
-
<div class="stack">
|
|
700
|
-
<!-- long form fields -->
|
|
701
|
-
</div>
|
|
702
|
-
</form>
|
|
703
|
-
|
|
704
|
-
<footer class="form-actions">
|
|
705
|
-
<button type="button" class="btn outline">Cancel</button>
|
|
706
|
-
<button type="submit" class="btn primary" form="profile-form">
|
|
707
|
-
Save changes
|
|
708
|
-
</button>
|
|
709
|
-
</footer>
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
For multiple submit intents, pair the `form` attribute with `formaction`. The form has one action; each submit button can override it.
|
|
713
|
-
|
|
714
|
-
```html
|
|
715
|
-
<form id="article-form"
|
|
716
|
-
action="/articles/publish"
|
|
717
|
-
method="post">
|
|
718
|
-
<!-- fields -->
|
|
719
|
-
</form>
|
|
720
|
-
|
|
721
|
-
<div class="form-actions">
|
|
722
|
-
<button type="submit"
|
|
723
|
-
class="btn outline"
|
|
724
|
-
form="article-form"
|
|
725
|
-
formaction="/articles/draft">
|
|
726
|
-
Save draft
|
|
727
|
-
</button>
|
|
728
|
-
<button type="submit"
|
|
729
|
-
class="btn primary"
|
|
730
|
-
form="article-form">
|
|
731
|
-
Publish
|
|
732
|
-
</button>
|
|
733
|
-
</div>
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
## Sticky Actions
|
|
737
|
-
|
|
738
|
-
> Use `.form-actions sticky` when long forms need reachable submit actions while scrolling.
|
|
739
|
-
|
|
740
|
-
### Inside the form
|
|
741
|
-
|
|
742
|
-
The `<form>` is the bounding parent. The sticky stays pinned to the viewport bottom while the form is taller than the viewport, then releases when the form's bottom edge reaches it.
|
|
743
|
-
|
|
744
|
-
Scroll the page to see the actions stay reachable.
|
|
745
|
-
|
|
746
|
-
```html
|
|
747
|
-
<form novalidate>
|
|
748
|
-
<div class="stack">
|
|
749
|
-
<label class="field">
|
|
750
|
-
<span class="field-label">Full name <span class="required-mark" aria-hidden="true">*</span></span>
|
|
751
|
-
<input class="input" type="text" autocomplete="name" required placeholder="Jane Doe" />
|
|
752
|
-
</label>
|
|
753
|
-
|
|
754
|
-
<label class="field">
|
|
755
|
-
<span class="field-label">Email <span class="required-mark" aria-hidden="true">*</span></span>
|
|
756
|
-
<input class="input" type="email" autocomplete="email" required />
|
|
757
|
-
</label>
|
|
758
|
-
|
|
759
|
-
<label class="field">
|
|
760
|
-
<span class="field-label">Phone</span>
|
|
761
|
-
<input class="input" type="tel" autocomplete="tel" />
|
|
762
|
-
</label>
|
|
763
|
-
|
|
764
|
-
<label class="field">
|
|
765
|
-
<span class="field-label">Bio</span>
|
|
766
|
-
<textarea class="textarea" rows="4" placeholder="Tell us about yourself…"></textarea>
|
|
767
|
-
</label>
|
|
768
|
-
|
|
769
|
-
<label class="field">
|
|
770
|
-
<span class="field-label">Website</span>
|
|
771
|
-
<input class="input" type="url" autocomplete="url" placeholder="https://example.com" />
|
|
772
|
-
</label>
|
|
773
|
-
|
|
774
|
-
<label class="field">
|
|
775
|
-
<span class="field-label">Language</span>
|
|
776
|
-
<select class="select">
|
|
777
|
-
<option>English</option>
|
|
778
|
-
<option>Français</option>
|
|
779
|
-
<option>Deutsch</option>
|
|
780
|
-
</select>
|
|
781
|
-
</label>
|
|
782
|
-
|
|
783
|
-
<label class="field">
|
|
784
|
-
<span class="field-label">Experience level</span>
|
|
785
|
-
<input class="range" type="range" min="0" max="10" value="5" />
|
|
786
|
-
</label>
|
|
787
|
-
|
|
788
|
-
<label class="field">
|
|
789
|
-
<span class="field-label">Profile picture</span>
|
|
790
|
-
<input class="file" type="file" accept="image/*" />
|
|
791
|
-
</label>
|
|
792
|
-
|
|
793
|
-
<fieldset class="field-group">
|
|
794
|
-
<legend class="field-label">Theme</legend>
|
|
795
|
-
<div class="stack">
|
|
796
|
-
<label class="choice">
|
|
797
|
-
<input class="radio" type="radio" name="theme" value="light" checked />
|
|
798
|
-
<span>Light</span>
|
|
799
|
-
</label>
|
|
800
|
-
<label class="choice">
|
|
801
|
-
<input class="radio" type="radio" name="theme" value="dark" />
|
|
802
|
-
<span>Dark</span>
|
|
803
|
-
</label>
|
|
804
|
-
<label class="choice">
|
|
805
|
-
<input class="radio" type="radio" name="theme" value="auto" />
|
|
806
|
-
<span>System</span>
|
|
807
|
-
</label>
|
|
808
|
-
</div>
|
|
809
|
-
</fieldset>
|
|
810
|
-
|
|
811
|
-
<label class="choice">
|
|
812
|
-
<input class="check" type="checkbox" />
|
|
813
|
-
<span>Email notifications</span>
|
|
814
|
-
</label>
|
|
815
|
-
</div>
|
|
816
|
-
|
|
817
|
-
<div class="form-actions sticky">
|
|
818
|
-
<button type="button" class="btn outline">Cancel</button>
|
|
819
|
-
<button type="submit" class="btn primary">Save changes</button>
|
|
820
|
-
</div>
|
|
821
|
-
</form>
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
### Detached from the form
|
|
825
|
-
|
|
826
|
-
For a sticky page footer, dialog footer, or card footer, detach the actions and connect the submit button with the `form` attribute. The bounding parent becomes the wrapper that holds both the form and the actions.
|
|
827
|
-
|
|
828
|
-
Scroll the page to see the detached footer stay reachable.
|
|
829
|
-
|
|
830
|
-
```html
|
|
831
|
-
<form id="account-form" novalidate>
|
|
832
|
-
<div class="stack">
|
|
833
|
-
<label class="field">
|
|
834
|
-
<span class="field-label">Full name <span class="required-mark" aria-hidden="true">*</span></span>
|
|
835
|
-
<input class="input" type="text" autocomplete="name" required placeholder="Jane Doe" />
|
|
836
|
-
</label>
|
|
837
|
-
|
|
838
|
-
<label class="field">
|
|
839
|
-
<span class="field-label">Email <span class="required-mark" aria-hidden="true">*</span></span>
|
|
840
|
-
<input class="input" type="email" autocomplete="email" required />
|
|
841
|
-
</label>
|
|
842
|
-
|
|
843
|
-
<label class="field">
|
|
844
|
-
<span class="field-label">Phone</span>
|
|
845
|
-
<input class="input" type="tel" autocomplete="tel" />
|
|
846
|
-
</label>
|
|
847
|
-
|
|
848
|
-
<label class="field">
|
|
849
|
-
<span class="field-label">Bio</span>
|
|
850
|
-
<textarea class="textarea" rows="4" placeholder="Tell us about yourself…"></textarea>
|
|
851
|
-
</label>
|
|
852
|
-
|
|
853
|
-
<label class="field">
|
|
854
|
-
<span class="field-label">Website</span>
|
|
855
|
-
<input class="input" type="url" autocomplete="url" placeholder="https://example.com" />
|
|
856
|
-
</label>
|
|
857
|
-
|
|
858
|
-
<label class="field">
|
|
859
|
-
<span class="field-label">Language</span>
|
|
860
|
-
<select class="select">
|
|
861
|
-
<option>English</option>
|
|
862
|
-
<option>Français</option>
|
|
863
|
-
<option>Deutsch</option>
|
|
864
|
-
</select>
|
|
865
|
-
</label>
|
|
866
|
-
|
|
867
|
-
<label class="field">
|
|
868
|
-
<span class="field-label">Experience level</span>
|
|
869
|
-
<input class="range" type="range" min="0" max="10" value="5" />
|
|
870
|
-
</label>
|
|
871
|
-
|
|
872
|
-
<label class="field">
|
|
873
|
-
<span class="field-label">Profile picture</span>
|
|
874
|
-
<input class="file" type="file" accept="image/*" />
|
|
875
|
-
</label>
|
|
876
|
-
|
|
877
|
-
<fieldset class="field-group">
|
|
878
|
-
<legend class="field-label">Theme</legend>
|
|
879
|
-
<div class="stack">
|
|
880
|
-
<label class="choice">
|
|
881
|
-
<input class="radio" type="radio" name="theme-detached" value="light" checked />
|
|
882
|
-
<span>Light</span>
|
|
883
|
-
</label>
|
|
884
|
-
<label class="choice">
|
|
885
|
-
<input class="radio" type="radio" name="theme-detached" value="dark" />
|
|
886
|
-
<span>Dark</span>
|
|
887
|
-
</label>
|
|
888
|
-
<label class="choice">
|
|
889
|
-
<input class="radio" type="radio" name="theme-detached" value="auto" />
|
|
890
|
-
<span>System</span>
|
|
891
|
-
</label>
|
|
892
|
-
</div>
|
|
893
|
-
</fieldset>
|
|
894
|
-
|
|
895
|
-
<label class="choice">
|
|
896
|
-
<input class="check" type="checkbox" />
|
|
897
|
-
<span>Email notifications</span>
|
|
898
|
-
</label>
|
|
899
|
-
</div>
|
|
900
|
-
</form>
|
|
901
|
-
|
|
902
|
-
<footer class="form-actions sticky">
|
|
903
|
-
<button type="button" class="btn outline">Cancel</button>
|
|
904
|
-
<button type="submit" class="btn primary" form="account-form">
|
|
905
|
-
Save changes
|
|
906
|
-
</button>
|
|
907
|
-
</footer>
|
|
908
|
-
```
|
|
909
|
-
|
|
910
|
-
Knobs:
|
|
911
|
-
|
|
912
|
-
- `--z-sticky` — layering token, defaults to `10`.
|
|
913
|
-
- `--form-actions-sticky-padding` — inner padding, defaults to `--space-3`.
|
|
914
|
-
- `--form-actions-sticky-inline-offset` — full-bleed escape hatch for sticky actions inside padded containers. Set this on the page, not the form.
|
|
915
|
-
|
|
916
|
-
```css
|
|
917
|
-
.settings-page {
|
|
918
|
-
--form-actions-sticky-inline-offset: var(--space-4);
|
|
919
|
-
}
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
A sticky footer can cover the last field on short pages. That is a page-layout concern, not a framework concern. Add bottom padding to the page or scroll container if needed.
|
|
923
|
-
|
|
924
|
-
## Choice Cards
|
|
925
|
-
|
|
926
|
-
> Card-style radio and checkbox labels where the whole card acts as a clickable target.
|
|
927
|
-
|
|
928
|
-
- Use `.choice-card` on a `<label>` wrapping a native `<input type="radio">` or `<input type="checkbox">`. The input is visually hidden; in browsers without `:has()` it stays visible so selection state never disappears.
|
|
929
|
-
- Selected items show a primary border, subtle background tint, and a checkmark badge at the top-right corner — round for radio, rounded square for checkbox.
|
|
930
|
-
- Pair with grid or flex utilities for group layout. The badge overhangs the top-right corner, so avoid `overflow: hidden` on the direct container.
|
|
931
|
-
- Supports disabled, checked, focus-visible, and invalid states.
|
|
932
|
-
|
|
933
|
-
```html
|
|
934
|
-
<div class="grid-3">
|
|
935
|
-
<label class="choice-card">
|
|
936
|
-
<input type="radio" name="cpu" value="1" checked />
|
|
937
|
-
<strong>8-core CPU</strong>
|
|
938
|
-
<span class="muted">32 GB RAM</span>
|
|
939
|
-
</label>
|
|
940
|
-
<label class="choice-card">
|
|
941
|
-
<input type="radio" name="cpu" value="2" />
|
|
942
|
-
<strong>6-core CPU</strong>
|
|
943
|
-
<span class="muted">24 GB RAM</span>
|
|
944
|
-
</label>
|
|
945
|
-
<label class="choice-card">
|
|
946
|
-
<input type="radio" name="cpu" value="3" />
|
|
947
|
-
<strong>4-core CPU</strong>
|
|
948
|
-
<span class="muted">16 GB RAM</span>
|
|
949
|
-
</label>
|
|
950
|
-
</div>
|
|
951
|
-
```
|
|
952
|
-
|
|
953
|
-
```html
|
|
954
|
-
<fieldset class="field-group">
|
|
955
|
-
<legend class="field-label">Features</legend>
|
|
956
|
-
<div class="grid-3">
|
|
957
|
-
<label class="choice-card">
|
|
958
|
-
<input type="checkbox" name="features" value="wifi" checked />
|
|
959
|
-
<strong>WiFi</strong>
|
|
960
|
-
<span class="muted">Dual-band 2.4/5 GHz</span>
|
|
961
|
-
</label>
|
|
962
|
-
<label class="choice-card">
|
|
963
|
-
<input type="checkbox" name="features" value="bluetooth" />
|
|
964
|
-
<strong>Bluetooth</strong>
|
|
965
|
-
<span class="muted">5.3, low energy</span>
|
|
966
|
-
</label>
|
|
967
|
-
<label class="choice-card">
|
|
968
|
-
<input type="checkbox" name="features" value="nfc" disabled />
|
|
969
|
-
<strong>NFC</strong>
|
|
970
|
-
<span class="muted">Coming soon</span>
|
|
971
|
-
</label>
|
|
972
|
-
</div>
|
|
973
|
-
</fieldset>
|
|
974
|
-
```
|
|
975
|
-
|
|
976
|
-
Validation follows the shared pattern: `aria-invalid="true"` on the input (set manually or by the `.needs-validation` enhancer) turns the card border to danger, and a `.field-error` inside a `.field` wrapper is shown.
|
|
977
|
-
|
|
978
|
-
```html
|
|
979
|
-
<fieldset class="field field-group">
|
|
980
|
-
<legend class="field-label">Plan</legend>
|
|
981
|
-
<div class="grid-3">
|
|
982
|
-
<label class="choice-card">
|
|
983
|
-
<input type="radio" name="plan" value="starter"
|
|
984
|
-
aria-invalid="true" aria-describedby="plan-error" />
|
|
985
|
-
<strong>Starter</strong>
|
|
986
|
-
<span class="muted">1 project</span>
|
|
987
|
-
</label>
|
|
988
|
-
<label class="choice-card">
|
|
989
|
-
<input type="radio" name="plan" value="pro"
|
|
990
|
-
aria-invalid="true" aria-describedby="plan-error" />
|
|
991
|
-
<strong>Pro</strong>
|
|
992
|
-
<span class="muted">Unlimited projects</span>
|
|
993
|
-
</label>
|
|
994
|
-
<label class="choice-card">
|
|
995
|
-
<input type="radio" name="plan" value="team"
|
|
996
|
-
aria-invalid="true" aria-describedby="plan-error" />
|
|
997
|
-
<strong>Team</strong>
|
|
998
|
-
<span class="muted">Unlimited + SSO</span>
|
|
999
|
-
</label>
|
|
1000
|
-
</div>
|
|
1001
|
-
<span class="field-error" id="plan-error" role="alert">
|
|
1002
|
-
Choose a plan to continue.
|
|
1003
|
-
</span>
|
|
1004
|
-
</fieldset>
|
|
1005
|
-
```
|
|
1006
|
-
|
|
1007
|
-
Sizes follow the shared `.sm` and `.lg` modifiers. Intent classes (`.primary`, `.secondary`) are supported on the `.choice-card` element.
|
|
1008
|
-
|
|
1009
|
-
## Switches
|
|
1010
|
-
|
|
1011
|
-
> Toggle controls that share a native checkbox at the markup level, with a switch visual.
|
|
1012
|
-
|
|
1013
|
-
- The API is `class="switch"`. Use `role="switch"` for ARIA correctness.
|
|
1014
|
-
- The visual state tracks the native `checked` attribute. No JavaScript is required.
|
|
1015
|
-
- For strict ARIA correctness, keep `aria-checked` in sync. This is an enhancement, not the baseline.
|
|
1016
|
-
|
|
1017
|
-
```html
|
|
1018
|
-
<!-- Default: native state, no JS required. -->
|
|
1019
|
-
<input class="switch" type="checkbox" role="switch" checked />
|
|
1020
|
-
```
|
|
1021
|
-
|
|
1022
|
-
```html{.js}
|
|
1023
|
-
<!-- Enhancement: keep aria-checked in sync. -->
|
|
1024
|
-
<input class="switch"
|
|
1025
|
-
type="checkbox"
|
|
1026
|
-
role="switch"
|
|
1027
|
-
aria-checked="true"
|
|
1028
|
-
checked
|
|
1029
|
-
onchange="this.setAttribute('aria-checked', this.checked)" />
|
|
1030
|
-
```
|
|
1031
|
-
|
|
1032
|
-
## Range
|
|
1033
|
-
|
|
1034
|
-
> Native range inputs with theme-aware accent color.
|
|
1035
|
-
|
|
1036
|
-
Core: native range, enhanced by `accent-color`. Optional richer skin may come later.
|
|
1037
|
-
|
|
1038
|
-
```html
|
|
1039
|
-
<label class="field">
|
|
1040
|
-
<span class="field-label">Volume</span>
|
|
1041
|
-
<input class="range" type="range" min="0" max="100" value="50" />
|
|
1042
|
-
</label>
|
|
1043
|
-
```
|
|
1044
|
-
|
|
1045
|
-
## Validation
|
|
1046
|
-
|
|
1047
|
-
> Inline error feedback that respects assistive technology and form state.
|
|
1048
|
-
|
|
1049
|
-
- `.field-error` is the canonical error message. It is wired to `aria-invalid="true"` via the input, so the visual border and the assistive-tech announcement move together.
|
|
1050
|
-
- `.field-help` is the canonical helper text. It carries supporting copy before and after validation.
|
|
1051
|
-
- Set `aria-invalid="true"` on the input. The `.field-error` element is linked via `aria-describedby`.
|
|
1052
|
-
- `role="alert"` on `.field-error` is only required when the message is inserted dynamically. Static messages do not need it.
|
|
1053
|
-
|
|
1054
|
-
```html
|
|
1055
|
-
<form novalidate>
|
|
1056
|
-
<div class="stack">
|
|
1057
|
-
<label class="field">
|
|
1058
|
-
<span class="field-label">Email</span>
|
|
1059
|
-
<input class="input" type="email"
|
|
1060
|
-
value="jane@"
|
|
1061
|
-
aria-invalid="true"
|
|
1062
|
-
aria-describedby="email-error" />
|
|
1063
|
-
<span class="field-error" id="email-error" role="alert">
|
|
1064
|
-
Enter a valid email address
|
|
1065
|
-
</span>
|
|
1066
|
-
</label>
|
|
1067
|
-
|
|
1068
|
-
<label class="field">
|
|
1069
|
-
<span class="field-label">Username</span>
|
|
1070
|
-
<input class="input" type="text" value="janedoe" aria-describedby="username-help" />
|
|
1071
|
-
<span class="field-help" id="username-help">
|
|
1072
|
-
Username is available.
|
|
1073
|
-
</span>
|
|
1074
|
-
</label>
|
|
1075
|
-
</div>
|
|
1076
|
-
</form>
|
|
1077
|
-
```
|
|
1078
|
-
|
|
1079
|
-
### Optional JavaScript enhancer
|
|
1080
|
-
|
|
1081
|
-
Native validation first; the enhancer only adds state and focus behavior.
|
|
1082
|
-
|
|
1083
|
-
Actual CSS ships validation *styles*. Invalid fields are marked with `aria-invalid="true"` (or the manual `.field.danger` wrapper class). The default forms bundle imports `forms/validation.css`; custom builds may omit that file when validation styling is app-owned. A small enhancer (`actual-css/js/validation`), included in the default runtime (`actual-css/js`) and also importable on its own, prevents premature error display, marks invalid fields on blur and submit, focuses the first invalid field on submit, and supports a few custom rules. It is not a validation framework — server and AJAX validation stay in app code.
|
|
1084
|
-
|
|
1085
|
-
Opt in with the `.needs-validation` class. Importing the module registers the behavior; there is no init call.
|
|
1086
|
-
|
|
1087
|
-
```html
|
|
1088
|
-
<form class="needs-validation" data-enhance="validation" data-validation-message="Please check the highlighted fields.">
|
|
1089
|
-
<div class="stack">
|
|
1090
|
-
<label class="field">
|
|
1091
|
-
<span class="field-label">Password</span>
|
|
1092
|
-
<input class="input" type="password" name="password" id="password" required
|
|
1093
|
-
minlength="8" maxlength="128"
|
|
1094
|
-
aria-describedby="password-error" />
|
|
1095
|
-
<span class="field-error" id="password-error">Enter a password.</span>
|
|
1096
|
-
</label>
|
|
1097
|
-
|
|
1098
|
-
<label class="field">
|
|
1099
|
-
<span class="field-label">Confirm password</span>
|
|
1100
|
-
<input class="input" type="password" name="confirm" required
|
|
1101
|
-
data-validation-rules="same #password"
|
|
1102
|
-
aria-describedby="confirm-error" />
|
|
1103
|
-
<span class="field-error" id="confirm-error" role="alert">Passwords must match.</span>
|
|
1104
|
-
</label>
|
|
1105
|
-
</div>
|
|
1106
|
-
|
|
1107
|
-
<div class="form-actions">
|
|
1108
|
-
<button class="btn primary" type="submit">Submit</button>
|
|
1109
|
-
<button class="btn neutral outline" type="reset">Reset</button>
|
|
1110
|
-
</div>
|
|
1111
|
-
</form>
|
|
1112
|
-
```
|
|
1113
|
-
|
|
1114
|
-
On blur, the enhancer sets `aria-invalid="true"` on the invalid field so visual and assistive-tech state move together. On submit, it adds `.was-validated` to the form and marks every invalid field. When the form is invalid it prevents submission, focuses the first invalid field, and dispatches a bubbling `actual:invalid` event with `{ form, firstInvalid, message }`. On reset, it clears all validation state — `.was-validated`, `aria-invalid`, `.field.danger`, and custom-validity messages — which native reset alone would leave behind.
|
|
1115
|
-
|
|
1116
|
-
The status bar (`actual-css/js/status`) auto-wires to that event: import it and add one status element, and the form's `data-validation-message` appears automatically on invalid submit — no manual listener.
|
|
1117
|
-
|
|
1118
|
-
```html
|
|
1119
|
-
<form class="needs-validation" data-enhance="validation" data-validation-message="Please check the highlighted fields.">
|
|
1120
|
-
<div class="stack">
|
|
1121
|
-
<label class="field">
|
|
1122
|
-
<span class="field-label">Email</span>
|
|
1123
|
-
<input class="input" type="email" name="email" required
|
|
1124
|
-
aria-describedby="demo-email-error" />
|
|
1125
|
-
<span class="field-error" id="demo-email-error" role="alert">
|
|
1126
|
-
Enter a valid email.
|
|
1127
|
-
</span>
|
|
1128
|
-
</label>
|
|
1129
|
-
|
|
1130
|
-
<label class="field">
|
|
1131
|
-
<span class="field-label">Quantity</span>
|
|
1132
|
-
<input class="input" name="quantity" inputmode="numeric" required
|
|
1133
|
-
data-validation-rules="digits"
|
|
1134
|
-
aria-describedby="demo-quantity-error" />
|
|
1135
|
-
<span class="field-error" id="demo-quantity-error" role="alert">
|
|
1136
|
-
Use digits only.
|
|
1137
|
-
</span>
|
|
1138
|
-
</label>
|
|
1139
|
-
|
|
1140
|
-
<div class="field">
|
|
1141
|
-
<label class="choice">
|
|
1142
|
-
<input class="check" type="checkbox" name="terms" required
|
|
1143
|
-
aria-describedby="demo-terms-error" />
|
|
1144
|
-
<span>
|
|
1145
|
-
<span class="field-label">Accept the terms</span>
|
|
1146
|
-
</span>
|
|
1147
|
-
</label>
|
|
1148
|
-
<span class="field-error" id="demo-terms-error" role="alert">
|
|
1149
|
-
Required before continuing.
|
|
1150
|
-
</span>
|
|
1151
|
-
</div>
|
|
1152
|
-
|
|
1153
|
-
<div class="field">
|
|
1154
|
-
<label class="choice">
|
|
1155
|
-
<input class="switch" type="checkbox" role="switch" name="confirm" required
|
|
1156
|
-
aria-describedby="demo-confirm-error" />
|
|
1157
|
-
<span>
|
|
1158
|
-
<span class="field-label">Confirm setup</span>
|
|
1159
|
-
</span>
|
|
1160
|
-
</label>
|
|
1161
|
-
<span class="field-error" id="demo-confirm-error" role="alert">
|
|
1162
|
-
Switch this on to submit.
|
|
1163
|
-
</span>
|
|
1164
|
-
</div>
|
|
1165
|
-
<div class="form-actions">
|
|
1166
|
-
<button class="btn primary" type="submit">Submit</button>
|
|
1167
|
-
<button class="btn neutral outline" type="reset">Reset</button>
|
|
1168
|
-
</div>
|
|
1169
|
-
</div>
|
|
1170
|
-
</form>
|
|
1171
|
-
|
|
1172
|
-
<div class="status-bar" data-status role="status" aria-live="polite" aria-atomic="true"></div>
|
|
1173
|
-
```
|
|
1174
|
-
|
|
1175
|
-
If you prefer to handle the summary yourself, listen for `actual:invalid` instead of importing the status module:
|
|
1176
|
-
|
|
1177
|
-
```js
|
|
1178
|
-
document.addEventListener("actual:invalid", (event) => {
|
|
1179
|
-
const { firstInvalid, message } = event.detail;
|
|
1180
|
-
// e.g. scroll to firstInvalid, or route message to your own surface
|
|
1181
|
-
});
|
|
1182
|
-
```
|
|
1183
|
-
|
|
1184
|
-
#### Built-in custom rules
|
|
1185
|
-
|
|
1186
|
-
Custom rules live in `data-validation-rules` as a comma-separated list; each rule may take space-separated options. The enhancer registers a few defaults:
|
|
1187
|
-
|
|
1188
|
-
- `same <selector>` — Value must equal the value of the element matched by `<selector>` (scoped to the field's form).
|
|
1189
|
-
- `number` — Value is empty or a valid `Number`.
|
|
1190
|
-
- `digits` — Value is empty or ASCII digits only.
|
|
1191
|
-
- `alnum` — Value is empty or letters and digits only.
|
|
1192
|
-
- `date` — Value is a valid date in `yyyy-mm-dd`, `dd/mm/yyyy`, `mm/dd/yyyy`, `dd.mm.yyyy`, `mm.dd.yyyy`, `dd-mm-yyyy`, or `mm-dd-yyyy` shape; rejects non-existent dates like `2026-02-29`. ISO remains the recommended wire format because servers can parse it without guessing.
|
|
1193
|
-
|
|
1194
|
-
Add your own with `FormValidator.registerRule(name, (value, el, ...opts) => boolean)`. The built-in rules treat empty values as valid so optional fields stay optional; custom rules should do the same when that behavior is wanted.
|
|
1195
|
-
|
|
1196
|
-
The `date` rule pairs naturally with `data-mask` — the mask structures input, the rule validates meaning. Same rule, different formats:
|
|
1197
|
-
|
|
1198
|
-
```html
|
|
1199
|
-
<form class="stack needs-validation" data-enhance="validation" novalidate>
|
|
1200
|
-
<label class="field">
|
|
1201
|
-
<span class="field-label">Date (ISO)</span>
|
|
1202
|
-
<input class="input" name="date-iso"
|
|
1203
|
-
data-mask="9999-99-99"
|
|
1204
|
-
data-validation-rules="date"
|
|
1205
|
-
inputmode="numeric"
|
|
1206
|
-
autocomplete="off"
|
|
1207
|
-
placeholder="yyyy-mm-dd"
|
|
1208
|
-
aria-describedby="date-iso-help date-iso-error"
|
|
1209
|
-
required />
|
|
1210
|
-
<span class="field-help" id="date-iso-help">
|
|
1211
|
-
yyyy-mm-dd format.
|
|
1212
|
-
</span>
|
|
1213
|
-
<span class="field-error" id="date-iso-error">
|
|
1214
|
-
Enter a valid date.
|
|
1215
|
-
</span>
|
|
1216
|
-
</label>
|
|
1217
|
-
|
|
1218
|
-
<label class="field">
|
|
1219
|
-
<span class="field-label">Date (European)</span>
|
|
1220
|
-
<input class="input" name="date-eu"
|
|
1221
|
-
data-mask="99/99/9999"
|
|
1222
|
-
data-validation-rules="date"
|
|
1223
|
-
inputmode="numeric"
|
|
1224
|
-
autocomplete="off"
|
|
1225
|
-
placeholder="dd/mm/yyyy"
|
|
1226
|
-
aria-describedby="date-eu-help date-eu-error"
|
|
1227
|
-
required />
|
|
1228
|
-
<span class="field-help" id="date-eu-help">
|
|
1229
|
-
dd/mm/yyyy format.
|
|
1230
|
-
</span>
|
|
1231
|
-
<span class="field-error" id="date-eu-error">
|
|
1232
|
-
Enter a valid date.
|
|
1233
|
-
</span>
|
|
1234
|
-
</label>
|
|
1235
|
-
|
|
1236
|
-
<div class="form-actions">
|
|
1237
|
-
<button class="btn primary" type="submit">Submit</button>
|
|
1238
|
-
<button class="btn neutral outline" type="reset">Reset</button>
|
|
1239
|
-
</div>
|
|
1240
|
-
</form>
|
|
1241
|
-
```
|
|
1242
|
-
|
|
1243
|
-
Custom rules only run when the field has a value. Empty fields skip all custom rules — `required` is the sole gatekeeper for empty values. A custom date rule for a stricter app-specific format could look like:
|
|
1244
|
-
|
|
1245
|
-
```js
|
|
1246
|
-
FormValidator.registerRule("iso-date", (value) => {
|
|
1247
|
-
return /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(value));
|
|
1248
|
-
});
|
|
1249
|
-
```
|
|
1250
|
-
|
|
1251
|
-
#### Server and AJAX validation
|
|
1252
|
-
|
|
1253
|
-
The enhancer never makes network calls. After a valid client submit, your own `form[data-ajax]` flow (see [JavaScript](javascript.md)) performs the request and maps server errors back onto fields through the public API:
|
|
1254
|
-
|
|
1255
|
-
```js
|
|
1256
|
-
import { FormValidator } from "actual-css/js/validation";
|
|
1257
|
-
|
|
1258
|
-
// server responded with field errors
|
|
1259
|
-
FormValidator.setErrors(form, { email: "Already taken" });
|
|
1260
|
-
|
|
1261
|
-
// clear a single field once fixed
|
|
1262
|
-
FormValidator.clearFieldError(form.elements.email);
|
|
1263
|
-
```
|
|
1264
|
-
|
|
1265
|
-
`setErrors` resolves each name to a field, sets `aria-invalid`, the field's `.field-error` text, and `setCustomValidity` so the next submit stays blocked until corrected.
|