Previews

No matching results.

x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
<div class="tw:p-6 tw:max-w-7xl tw:mx-auto tw:space-y-8">
<!-- Introductory callout -->
<div class="callout">
<p class="tw:mb-1 tw:text-sm tw:font-mono">
<strong>PATTERN NAME:</strong> Form Field<br>
<strong>COMPOSES:</strong> .form-label (+ -required / -optional) | .form-control / .form-select / .form-check | .form-text | .invalid-feedback | .valid-feedback
</p>
<p class="tw:mb-0 tw:text-sm">
One wrapper that groups a field's label, control, helper text and feedback, and carries the
validation state <strong>once</strong><code>.form-field.is-invalid</code> tints the control
and reveals the error, instead of the caller marking every part by hand. Adds
<code>.form-field-errors</code> for the several-messages-for-one-field case the design system
previously had no shape for.
</p>
</div>
<!-- Default -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Default</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Default:</strong> label + control, no state. A visible <code><label for></code> is
always required — never a placeholder standing in for a label.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field">
<label class="form-label form-label-required" for="ff-name">Child's full name</label>
<input type="text" class="form-control" id="ff-name" aria-required="true">
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><div class="form-field"></code><br>
<code>  <label class="form-label form-label-required" for="ff-name">Child's full name</label></code><br>
<code>  <input type="text" class="form-control" id="ff-name" aria-required="true"></code><br>
<code></div></code>
</div>
</div>
<!-- With helper text -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">With helper text</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Helper text:</strong> <code>.form-text</code> below the control, referenced by the
control's <code>aria-describedby</code>. Helper text explains how to answer; it is not an error.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field">
<label class="form-label" for="ff-dob">Date of birth</label>
<input type="date" class="form-control" id="ff-dob" aria-describedby="ff-dob-help">
<p class="form-text" id="ff-dob-help">Use the date on your child's birth certificate.</p>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><input class="form-control" id="ff-dob" aria-describedby="ff-dob-help"></code><br>
<code><p class="form-text" id="ff-dob-help">Use the date on your child's birth certificate.</p></code>
</div>
</div>
<!-- Invalid — single error -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Invalid — single error</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Invalid:</strong> <code>.is-invalid</code> goes on the <code>.form-field</code> wrapper.
It tints the control's border and reveals the <code>.invalid-feedback</code> line — which stays
hidden inside a wrapper that is not marked invalid, so the error text can be server-rendered up
front. <code>aria-invalid="true"</code> and <code>aria-describedby</code> stay on the control:
those are the accessible name/description contract, not styling, and both the helper text and
the error must be referenced when both are present.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field is-invalid">
<label class="form-label form-label-required" for="ff-email">Email address</label>
<input type="email" class="form-control" id="ff-email" value="parent@"
aria-required="true" aria-invalid="true"
aria-describedby="ff-email-help ff-email-error">
<p class="form-text" id="ff-email-help">We'll send your application updates here.</p>
<p class="invalid-feedback" id="ff-email-error">Enter an email address in the format name@example.com.</p>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><div class="form-field is-invalid"></code><br>
<code>  <input class="form-control" id="ff-email" aria-invalid="true" aria-describedby="ff-email-help ff-email-error"></code><br>
<code>  <p class="form-text" id="ff-email-help"></p></code><br>
<code>  <p class="invalid-feedback" id="ff-email-error"></p></code><br>
<code></div></code>
</div>
</div>
<!-- Invalid — multiple errors -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Invalid — multiple errors</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Multi-error list:</strong> <code>.form-field-errors</code> is a real
<code><ul></code> whose <code>id</code> goes in the control's
<code>aria-describedby</code>, so a screen reader hears every message, not just the first. Ink
and type match <code>.invalid-feedback</code> exactly — one error and three errors must not look
like different components.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field is-invalid">
<label class="form-label form-label-required" for="ff-password">Create a password</label>
<input type="password" class="form-control" id="ff-password" value="abc"
aria-required="true" aria-invalid="true"
aria-describedby="ff-password-help ff-password-errors">
<p class="form-text" id="ff-password-help">You'll use this to check your application later.</p>
<ul class="form-field-errors" id="ff-password-errors">
<li>Use at least 8 characters.</li>
<li>Include one number.</li>
<li>Include one capital letter.</li>
</ul>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><ul class="form-field-errors" id="ff-password-errors"></code><br>
<code>  <li>Use at least 8 characters.</li></code><br>
<code></ul></code>
</div>
</div>
<!-- Valid -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Valid</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Valid:</strong> <code>.is-valid</code> on the wrapper tints the control green and
reveals <code>.valid-feedback</code>. Use it sparingly — confirm a field the user could
reasonably doubt (a postcode that resolved, a reference that was found), not every field they
filled in correctly.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field is-valid">
<label class="form-label" for="ff-postcode">Home postcode</label>
<input type="text" class="form-control" id="ff-postcode" value="SW1A 1AA"
aria-describedby="ff-postcode-ok">
<p class="valid-feedback" id="ff-postcode-ok">Matched: 12 Example Street, London.</p>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><div class="form-field is-valid"></code><code><p class="valid-feedback" id="…"></p></code>
</div>
</div>
<!-- Disabled -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Disabled</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Disabled:</strong> the wrapper adds nothing — <code>disabled</code> is an attribute on
the control and <code>.form-control:disabled</code> already styles it. Say <em>why</em> in the
helper text; a control that is dimmed and silent is a dead end for a first-time applicant.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field">
<label class="form-label" for="ff-school">Second choice school</label>
<select class="form-select" id="ff-school" disabled aria-describedby="ff-school-help">
<option>Choose a school…</option>
</select>
<p class="form-text" id="ff-school-help">Available once you've chosen a first preference.</p>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><select class="form-select" id="ff-school" disabled aria-describedby="ff-school-help"></code>
</div>
</div>
<!-- Checkbox and grouped fields -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Checkbox and grouped choices</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Beyond text inputs:</strong> the wrapper also carries state for
<code>.form-check</code> controls — the ring around a required consent checkbox is the most
common invalid field these users meet. For a group of choices, put <code>.form-field</code> on a
<code><fieldset></code> and the <code>.form-label</code> on its <code><legend></code>,
so one error covers the whole group.
</p>
</div>
<div class="tw:max-w-md tw:space-y-4">
<div class="form-field is-invalid">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="ff-consent"
aria-invalid="true" aria-describedby="ff-consent-error">
<label class="form-check-label form-label-required" for="ff-consent">
I confirm the information in this application is correct
</label>
</div>
<p class="invalid-feedback" id="ff-consent-error">Confirm the information is correct before submitting.</p>
</div>
<fieldset class="form-field is-invalid">
<legend class="form-label form-label-required">Which entry year are you applying for?</legend>
<div class="form-check">
<input class="form-check-input" type="radio" name="ff-year" id="ff-year-7"
aria-describedby="ff-year-error">
<label class="form-check-label" for="ff-year-7">Year 7 (September 2026)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="ff-year" id="ff-year-12"
aria-describedby="ff-year-error">
<label class="form-check-label" for="ff-year-12">Year 12 (September 2026)</label>
</div>
<p class="invalid-feedback" id="ff-year-error">Choose the entry year you're applying for.</p>
</fieldset>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><fieldset class="form-field is-invalid"><legend class="form-label"></legend></code>
</div>
</div>
<!-- Hidden-until-invalid proof -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Feedback is hidden until the wrapper says otherwise</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Why this matters:</strong> <code>.invalid-feedback</code> on its own is visible the
moment it is in the DOM — <code>components/form.css</code> gives it an unconditional
<code>display: block</code>, and the <code>.is-invalid ~ .invalid-feedback</code> rule that
looked like a gate was a no-op. Inside <code>.form-field</code> it is genuinely conditional, so
both messages below are in the markup and neither renders.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field">
<label class="form-label" for="ff-quiet">Middle name</label>
<input type="text" class="form-control" id="ff-quiet">
<p class="invalid-feedback" id="ff-quiet-error">This error text is present in the DOM and not shown.</p>
<p class="valid-feedback" id="ff-quiet-ok">So is this success text.</p>
</div>
</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
<div class="tw:p-6 tw:max-w-7xl tw:mx-auto tw:space-y-8">
<!-- Introductory callout -->
<div class="callout">
<p class="tw:mb-1 tw:text-sm tw:font-mono">
<strong>PATTERN NAME:</strong> Form Field<br>
<strong>COMPOSES:</strong> .form-label (+ -required / -optional) | .form-control / .form-select / .form-check | .form-text | .invalid-feedback | .valid-feedback
</p>
<p class="tw:mb-0 tw:text-sm">
One wrapper that groups a field's label, control, helper text and feedback, and carries the
validation state <strong>once</strong><code>.form-field.is-invalid</code> tints the control
and reveals the error, instead of the caller marking every part by hand. Adds
<code>.form-field-errors</code> for the several-messages-for-one-field case the design system
previously had no shape for.
</p>
</div>
<!-- Default -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Default</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Default:</strong> label + control, no state. A visible <code><label for></code> is
always required — never a placeholder standing in for a label.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field">
<label class="form-label form-label-required" for="ff-name">Child's full name</label>
<input type="text" class="form-control" id="ff-name" aria-required="true">
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><div class="form-field"></code><br>
<code>  <label class="form-label form-label-required" for="ff-name">Child's full name</label></code><br>
<code>  <input type="text" class="form-control" id="ff-name" aria-required="true"></code><br>
<code></div></code>
</div>
</div>
<!-- With helper text -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">With helper text</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Helper text:</strong> <code>.form-text</code> below the control, referenced by the
control's <code>aria-describedby</code>. Helper text explains how to answer; it is not an error.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field">
<label class="form-label" for="ff-dob">Date of birth</label>
<input type="date" class="form-control" id="ff-dob" aria-describedby="ff-dob-help">
<p class="form-text" id="ff-dob-help">Use the date on your child's birth certificate.</p>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><input class="form-control" id="ff-dob" aria-describedby="ff-dob-help"></code><br>
<code><p class="form-text" id="ff-dob-help">Use the date on your child's birth certificate.</p></code>
</div>
</div>
<!-- Invalid — single error -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Invalid — single error</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Invalid:</strong> <code>.is-invalid</code> goes on the <code>.form-field</code> wrapper.
It tints the control's border and reveals the <code>.invalid-feedback</code> line — which stays
hidden inside a wrapper that is not marked invalid, so the error text can be server-rendered up
front. <code>aria-invalid="true"</code> and <code>aria-describedby</code> stay on the control:
those are the accessible name/description contract, not styling, and both the helper text and
the error must be referenced when both are present.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field is-invalid">
<label class="form-label form-label-required" for="ff-email">Email address</label>
<input type="email" class="form-control" id="ff-email" value="parent@"
aria-required="true" aria-invalid="true"
aria-describedby="ff-email-help ff-email-error">
<p class="form-text" id="ff-email-help">We'll send your application updates here.</p>
<p class="invalid-feedback" id="ff-email-error">Enter an email address in the format name@example.com.</p>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><div class="form-field is-invalid"></code><br>
<code>  <input class="form-control" id="ff-email" aria-invalid="true" aria-describedby="ff-email-help ff-email-error"></code><br>
<code>  <p class="form-text" id="ff-email-help"></p></code><br>
<code>  <p class="invalid-feedback" id="ff-email-error"></p></code><br>
<code></div></code>
</div>
</div>
<!-- Invalid — multiple errors -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Invalid — multiple errors</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Multi-error list:</strong> <code>.form-field-errors</code> is a real
<code><ul></code> whose <code>id</code> goes in the control's
<code>aria-describedby</code>, so a screen reader hears every message, not just the first. Ink
and type match <code>.invalid-feedback</code> exactly — one error and three errors must not look
like different components.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field is-invalid">
<label class="form-label form-label-required" for="ff-password">Create a password</label>
<input type="password" class="form-control" id="ff-password" value="abc"
aria-required="true" aria-invalid="true"
aria-describedby="ff-password-help ff-password-errors">
<p class="form-text" id="ff-password-help">You'll use this to check your application later.</p>
<ul class="form-field-errors" id="ff-password-errors">
<li>Use at least 8 characters.</li>
<li>Include one number.</li>
<li>Include one capital letter.</li>
</ul>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><ul class="form-field-errors" id="ff-password-errors"></code><br>
<code>  <li>Use at least 8 characters.</li></code><br>
<code></ul></code>
</div>
</div>
<!-- Valid -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Valid</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Valid:</strong> <code>.is-valid</code> on the wrapper tints the control green and
reveals <code>.valid-feedback</code>. Use it sparingly — confirm a field the user could
reasonably doubt (a postcode that resolved, a reference that was found), not every field they
filled in correctly.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field is-valid">
<label class="form-label" for="ff-postcode">Home postcode</label>
<input type="text" class="form-control" id="ff-postcode" value="SW1A 1AA"
aria-describedby="ff-postcode-ok">
<p class="valid-feedback" id="ff-postcode-ok">Matched: 12 Example Street, London.</p>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><div class="form-field is-valid"></code><code><p class="valid-feedback" id="…"></p></code>
</div>
</div>
<!-- Disabled -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Disabled</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Disabled:</strong> the wrapper adds nothing — <code>disabled</code> is an attribute on
the control and <code>.form-control:disabled</code> already styles it. Say <em>why</em> in the
helper text; a control that is dimmed and silent is a dead end for a first-time applicant.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field">
<label class="form-label" for="ff-school">Second choice school</label>
<select class="form-select" id="ff-school" disabled aria-describedby="ff-school-help">
<option>Choose a school…</option>
</select>
<p class="form-text" id="ff-school-help">Available once you've chosen a first preference.</p>
</div>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><select class="form-select" id="ff-school" disabled aria-describedby="ff-school-help"></code>
</div>
</div>
<!-- Checkbox and grouped fields -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Checkbox and grouped choices</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Beyond text inputs:</strong> the wrapper also carries state for
<code>.form-check</code> controls — the ring around a required consent checkbox is the most
common invalid field these users meet. For a group of choices, put <code>.form-field</code> on a
<code><fieldset></code> and the <code>.form-label</code> on its <code><legend></code>,
so one error covers the whole group.
</p>
</div>
<div class="tw:max-w-md tw:space-y-4">
<div class="form-field is-invalid">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="ff-consent"
aria-invalid="true" aria-describedby="ff-consent-error">
<label class="form-check-label form-label-required" for="ff-consent">
I confirm the information in this application is correct
</label>
</div>
<p class="invalid-feedback" id="ff-consent-error">Confirm the information is correct before submitting.</p>
</div>
<fieldset class="form-field is-invalid">
<legend class="form-label form-label-required">Which entry year are you applying for?</legend>
<div class="form-check">
<input class="form-check-input" type="radio" name="ff-year" id="ff-year-7"
aria-describedby="ff-year-error">
<label class="form-check-label" for="ff-year-7">Year 7 (September 2026)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="ff-year" id="ff-year-12"
aria-describedby="ff-year-error">
<label class="form-check-label" for="ff-year-12">Year 12 (September 2026)</label>
</div>
<p class="invalid-feedback" id="ff-year-error">Choose the entry year you're applying for.</p>
</fieldset>
</div>
<div class="tw:mt-4 tw:text-sm tw:text-gray-600">
<code><fieldset class="form-field is-invalid"><legend class="form-label"></legend></code>
</div>
</div>
<!-- Hidden-until-invalid proof -->
<div class="tw:border-t tw:pt-6">
<h2 class="h4 tw:mb-4">Feedback is hidden until the wrapper says otherwise</h2>
<div class="callout tw:mb-4">
<p class="tw:mb-0 tw:text-sm">
<strong>Why this matters:</strong> <code>.invalid-feedback</code> on its own is visible the
moment it is in the DOM — <code>components/form.css</code> gives it an unconditional
<code>display: block</code>, and the <code>.is-invalid ~ .invalid-feedback</code> rule that
looked like a gate was a no-op. Inside <code>.form-field</code> it is genuinely conditional, so
both messages below are in the markup and neither renders.
</p>
</div>
<div class="tw:max-w-md">
<div class="form-field">
<label class="form-label" for="ff-quiet">Middle name</label>
<input type="text" class="form-control" id="ff-quiet">
<p class="invalid-feedback" id="ff-quiet-error">This error text is present in the DOM and not shown.</p>
<p class="valid-feedback" id="ff-quiet-ok">So is this success text.</p>
</div>
</div>
</div>
</div>