@ctxr/skill-frontend-excellence 0.1.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.
@@ -0,0 +1,485 @@
1
+ # Forms and Feedback
2
+
3
+ Framework-agnostic patterns for forms that are easy to fill, easy to recover from, and accessible. Forms are where most products earn or lose users.
4
+
5
+ ## Form Anatomy
6
+
7
+ ```
8
+ [ Optional: page heading ]
9
+ [ Optional: short context: why we're asking ]
10
+
11
+ [ Field group ]
12
+ [ Label (always visible) ] [ Optional indicator: required, optional ]
13
+ [ Input ]
14
+ [ Helper text (always when complex) ]
15
+ [ Error message (when invalid) ]
16
+
17
+ [ ... more field groups ]
18
+
19
+ [ Submit button ] [ Cancel / secondary action ]
20
+ ```
21
+
22
+ ## Labels
23
+
24
+ ### Always visible
25
+
26
+ The label must be visible at all times, not just when the field is empty.
27
+
28
+ - **Wrap or associate**: `<label for="email">Email</label> <input id="email" />` or `<label>Email <input /></label>`.
29
+ - **Float labels** (label transitions to small position when focused/filled) are acceptable visually but the label must remain associated with the input.
30
+ - **Placeholder is not a label.** The placeholder disappears when typing, and most placeholders fail contrast.
31
+
32
+ ### Position
33
+
34
+ - **Top-aligned** is the default. Easiest to scan.
35
+ - **Left-aligned** for compact dense forms (settings).
36
+ - **Right-aligned** rarely; harder to scan.
37
+
38
+ ### Required vs optional
39
+
40
+ - Mark the smaller set, not the larger. If most fields are required, mark optional ones.
41
+ - Use both visual (asterisk) AND text (`required` somewhere visible).
42
+ - `aria-required="true"` (or use the native `required` attribute, which implies it).
43
+
44
+ ```html
45
+ <label for="email">
46
+ Email <span aria-hidden="true" class="required">*</span>
47
+ </label>
48
+ <input id="email" type="email" required aria-required="true" />
49
+ ```
50
+
51
+ ## Input Types
52
+
53
+ Use the right `type` and `inputmode` for the data:
54
+
55
+ | Data | type | inputmode | autocomplete |
56
+ |------|------|-----------|--------------|
57
+ | Email | email | email | email |
58
+ | Phone | tel | tel | tel |
59
+ | URL | url | url | url |
60
+ | Numeric ID, code | text | numeric | one-time-code (for OTP) |
61
+ | Money | text | decimal | (use a formatted text input) |
62
+ | Date | date | (auto) | bday or other |
63
+ | Search | search | search | (none) |
64
+ | Password | password | (auto) | new-password / current-password |
65
+ | Name | text | text | given-name / family-name / name |
66
+ | Address | text | text | street-address, address-line1, postal-code, country |
67
+ | Credit card | text | numeric | cc-number / cc-exp / cc-csc / cc-name |
68
+
69
+ ### `autocomplete` is critical
70
+
71
+ Browsers and password managers use `autocomplete` to fill fields automatically. Without it, users have to type or copy-paste, which they hate.
72
+
73
+ WHATWG `autocomplete` reference (commonly used):
74
+
75
+ - `name`, `given-name`, `additional-name`, `family-name`
76
+ - `email`, `tel`, `tel-national`
77
+ - `street-address`, `address-line1`, `address-line2`, `address-level1` (state), `address-level2` (city), `postal-code`, `country`
78
+ - `cc-name`, `cc-number`, `cc-exp`, `cc-csc`, `cc-type`
79
+ - `username`, `current-password`, `new-password`, `one-time-code`
80
+ - `organization`, `organization-title`
81
+ - `bday`, `bday-day`, `bday-month`, `bday-year`
82
+
83
+ Multi-form pages: prefix with the form purpose (`shipping`, `billing`):
84
+
85
+ ```html
86
+ <input autocomplete="shipping street-address" />
87
+ <input autocomplete="billing street-address" />
88
+ ```
89
+
90
+ ### Native vs custom controls
91
+
92
+ Native first:
93
+
94
+ - `<input type="date">` is fine on mobile; custom date pickers are appropriate for desktop ranges.
95
+ - `<input type="checkbox">` and `<input type="radio">` style with `:checked` selectors plus pseudo-elements; use ARIA pattern only when truly necessary.
96
+ - `<input type="range">` for sliders.
97
+ - `<select>` for short lists; combobox for long or async.
98
+ - `<input type="file">` for uploads; style the trigger with a `<label>` to match design.
99
+
100
+ Custom controls require:
101
+
102
+ - Full keyboard support (arrow keys, Enter/Space, Esc).
103
+ - ARIA role and state.
104
+ - Focus management.
105
+ - Touch and mouse parity.
106
+
107
+ Don't reinvent the date picker unless you have a year of work to invest.
108
+
109
+ ## Validation
110
+
111
+ ### When to validate
112
+
113
+ - **Don't validate on every keystroke.** It's distracting and often wrong (you haven't typed enough).
114
+ - **Validate on blur** (when the user leaves the field). Sufficient time has passed; the user has indicated they're done.
115
+ - **Re-validate on submit** as a safety net; the server validates regardless.
116
+ - **For password strength meters**, update on keystroke since the user expects live feedback.
117
+ - **For confirm-password**, validate when the second field loses focus.
118
+
119
+ ### Error placement
120
+
121
+ - **Inline, below the field.** The error is about THIS field; show it HERE.
122
+ - **`aria-invalid="true"` on the input.** Programmatic error state.
123
+ - **`aria-describedby`** on the input pointing to the error message id.
124
+ - **`role="alert"`** on the error so screen readers announce it.
125
+
126
+ ```html
127
+ <label for="email">Email</label>
128
+ <input
129
+ id="email"
130
+ type="email"
131
+ required
132
+ aria-invalid="true"
133
+ aria-describedby="email-error"
134
+ />
135
+ <p id="email-error" role="alert" class="error">
136
+ Enter a valid email address (example: name@example.com)
137
+ </p>
138
+ ```
139
+
140
+ ### Error message content
141
+
142
+ - **Cause AND fix.** "Email is required" beats "Validation error". "Enter a valid email address" beats "Invalid".
143
+ - **Plain language.** Don't expose backend codes ("ERR_INVALID_INPUT"); translate.
144
+ - **Don't blame the user.** "Couldn't save your draft" not "You haven't filled everything in".
145
+ - **Stay positive when possible.** "Try a longer password" beats "Password too short".
146
+
147
+ ### Multi-error submission
148
+
149
+ If the user submits with multiple errors:
150
+
151
+ 1. Move focus to the first invalid field (or to a top-of-form summary).
152
+ 2. Show a summary at the top with anchor links to each error:
153
+
154
+ ```html
155
+ <div role="alert" aria-live="assertive" class="error-summary">
156
+ <h2>3 issues to fix:</h2>
157
+ <ul>
158
+ <li><a href="#email">Enter a valid email</a></li>
159
+ <li><a href="#password">Password must be at least 8 characters</a></li>
160
+ <li><a href="#terms">Accept the terms to continue</a></li>
161
+ </ul>
162
+ </div>
163
+ ```
164
+
165
+ 3. Each invalid field also shows its inline error.
166
+
167
+ ### Server-side errors
168
+
169
+ After submit, server may reject with errors not caught client-side (email taken, payment failed). Treat them like any other inline error:
170
+
171
+ - Show inline near the relevant field if known.
172
+ - Show a top-of-form alert if the error doesn't map to a field (network failure).
173
+ - Provide a recovery action (retry, contact support).
174
+ - Preserve user input. Never make them retype.
175
+
176
+ ## Helper Text
177
+
178
+ Persistent text below the field, before any error, that explains:
179
+
180
+ - Format expectations ("3 to 30 characters, letters and numbers")
181
+ - Why we're asking ("Used for password reset only; never shown publicly")
182
+ - Examples ("e.g., +1 555 123 4567")
183
+
184
+ Place between input and error. When error shows, error replaces or appears below helper.
185
+
186
+ ## Submit Button
187
+
188
+ ### Location
189
+
190
+ - Primary submit at the bottom of the form, aligned to the form's primary text direction (left in LTR forms with fields stacked left, right in form-aligned-right cases).
191
+ - For mobile, full-width is acceptable for primary submit.
192
+
193
+ ### Label
194
+
195
+ - **Specific verb-noun.** "Create account" beats "Submit". "Save draft" beats "Save".
196
+ - **Match the action.** "Send invoice" if that's what happens.
197
+
198
+ ### Loading state
199
+
200
+ ```html
201
+ <button type="submit" disabled aria-busy="true">
202
+ <span class="spinner" aria-hidden="true"></span>
203
+ Creating account
204
+ </button>
205
+ ```
206
+
207
+ - Disable while submitting (prevents double-submit).
208
+ - Show spinner inside the button.
209
+ - Maintain button width to prevent layout shift.
210
+ - Optionally update the label ("Creating account" instead of "Create account").
211
+
212
+ ### Cancel
213
+
214
+ - Provide a secondary action when the user might back out (cancel, "back").
215
+ - Confirm if there are unsaved changes:
216
+
217
+ ```html
218
+ <button type="button" onclick="confirmExit()">Cancel</button>
219
+ ```
220
+
221
+ ```js
222
+ function confirmExit() {
223
+ if (formIsDirty()) {
224
+ if (confirm('Discard your changes?')) {
225
+ // navigate away
226
+ }
227
+ } else {
228
+ // navigate away
229
+ }
230
+ }
231
+ ```
232
+
233
+ ## Multi-Step Forms
234
+
235
+ For long forms, break into steps.
236
+
237
+ ### Stepper
238
+
239
+ Show progress and current step:
240
+
241
+ ```
242
+ [Step 1: Account] -> [Step 2: Profile] -> [Step 3: Payment]
243
+ Done In progress Upcoming
244
+ ```
245
+
246
+ - Current step visually distinct.
247
+ - Completed steps clickable (allow back navigation).
248
+ - Upcoming steps not clickable.
249
+
250
+ ### Save state
251
+
252
+ - Auto-save draft at each step transition.
253
+ - Restore on return.
254
+ - Allow exit without losing data.
255
+
256
+ ### Validation per step
257
+
258
+ - Validate the current step on "Next".
259
+ - Don't validate steps the user hasn't reached.
260
+ - Show validation errors before advancing.
261
+
262
+ ## Long Forms
263
+
264
+ For very long forms (> 10 fields):
265
+
266
+ - Group fields with `<fieldset>` + `<legend>`:
267
+
268
+ ```html
269
+ <fieldset>
270
+ <legend>Shipping address</legend>
271
+ <!-- fields -->
272
+ </fieldset>
273
+ ```
274
+
275
+ - Use visual section headings with horizontal lines or background variation.
276
+ - Auto-save drafts every 30-60s or on blur of significant fields.
277
+ - Show a "Saved 5s ago" microcopy.
278
+
279
+ ### Autosave UX
280
+
281
+ - Don't interrupt the user's typing with the save indicator.
282
+ - Show in a fixed corner ("Saving...", "Saved", "Failed to save - retry?").
283
+ - Use `aria-live="polite"` so screen readers announce status changes without interrupting.
284
+
285
+ ## Confirmation and Destructive Actions
286
+
287
+ ### Confirm before destructive
288
+
289
+ For "Delete account", "Cancel subscription", "Discard draft": require an explicit confirmation.
290
+
291
+ - Modal with clear title ("Delete account?").
292
+ - Body text states consequences ("This permanently deletes your account and all data. This cannot be undone.").
293
+ - Two buttons: "Cancel" (primary) and "Delete account" (destructive secondary, danger color).
294
+ - For very destructive actions, require typing the resource name to confirm: "Type your account email to confirm".
295
+
296
+ ### Don't confirm trivial actions
297
+
298
+ - Don't confirm "Save" (the action is non-destructive).
299
+ - Don't confirm "Send message" unless the user requested confirm-before-send.
300
+ - Don't confirm form resets if "Cancel" already exists.
301
+
302
+ ### Undo
303
+
304
+ For undoable destructive actions, prefer undo over confirm:
305
+
306
+ ```
307
+ [Toast: "Item deleted. Undo (5s)"]
308
+ ```
309
+
310
+ Undo is faster, less interruptive, and works with screen readers. Use confirm for actions that genuinely cannot be undone (account deletion, payment).
311
+
312
+ ## File Upload
313
+
314
+ - Show drop zone (visible on hover/dragover).
315
+ - Click-to-browse via styled `<label for="file-input">`.
316
+ - Show thumbnail/preview of selected files.
317
+ - Show progress bar during upload.
318
+ - Allow cancel during upload.
319
+ - Show error per file (size, type, network).
320
+ - Accept multiple files where appropriate.
321
+
322
+ ```html
323
+ <label for="resume" class="dropzone">
324
+ <input id="resume" type="file" accept=".pdf,.doc,.docx" hidden />
325
+ Drag and drop your resume, or click to browse.
326
+ PDF, DOC, or DOCX, up to 5 MB.
327
+ </label>
328
+ ```
329
+
330
+ Hide the native input, style the label as the drop zone.
331
+
332
+ ## Search Forms
333
+
334
+ - Single field with magnifier icon.
335
+ - Submit on Enter (native `<form>` behavior).
336
+ - Optionally submit on type with debounce (300ms).
337
+ - Show recent searches when empty.
338
+ - Show suggestions as the user types.
339
+ - Show clear-input button when populated.
340
+ - Show search results in real-time or after submit.
341
+
342
+ ```html
343
+ <form role="search" action="/search">
344
+ <label for="q" class="sr-only">Search</label>
345
+ <input id="q" name="q" type="search" autocomplete="off" />
346
+ <button type="submit">Search</button>
347
+ </form>
348
+ ```
349
+
350
+ `role="search"` on the form is a landmark for screen readers.
351
+
352
+ ## Login and Sign-Up
353
+
354
+ ### Login
355
+
356
+ ```html
357
+ <form>
358
+ <label for="username">Email</label>
359
+ <input id="username" name="username" type="email" autocomplete="username" required />
360
+
361
+ <label for="current-password">Password</label>
362
+ <input id="current-password" name="password" type="password" autocomplete="current-password" required />
363
+
364
+ <button type="submit">Sign in</button>
365
+
366
+ <a href="/forgot">Forgot password?</a>
367
+ <a href="/signup">Create account</a>
368
+ </form>
369
+ ```
370
+
371
+ - Email/username must use `autocomplete="username"` (counterintuitive, but the spec).
372
+ - Password must use `autocomplete="current-password"`.
373
+ - Always use a `<form>` (not just floating fields), so password managers detect it.
374
+
375
+ ### Sign-up
376
+
377
+ - New password: `autocomplete="new-password"`.
378
+ - Show password strength inline.
379
+ - Show "Show / hide" toggle for the password field.
380
+ - After submit, send to verification or onboarding, not back to a login screen.
381
+
382
+ ### Password reset
383
+
384
+ - Email field with `autocomplete="username"`.
385
+ - Submit reveals "Check your email" message regardless of whether the email exists (security best practice).
386
+ - The reset link page uses `autocomplete="new-password"`.
387
+
388
+ ### One-time codes
389
+
390
+ - `autocomplete="one-time-code"` so iOS suggests codes from SMS.
391
+ - `inputmode="numeric"` for numeric codes.
392
+ - `pattern="\d*"` for native validation on numeric codes.
393
+ - Auto-submit when the expected number of digits is entered.
394
+
395
+ ## Payment Forms
396
+
397
+ - Card number: `autocomplete="cc-number"`, `inputmode="numeric"`, `pattern` for digits.
398
+ - Expiry: `autocomplete="cc-exp"`, `inputmode="numeric"`.
399
+ - CVC: `autocomplete="cc-csc"`, `inputmode="numeric"`, `maxlength="4"`.
400
+ - Cardholder name: `autocomplete="cc-name"`.
401
+ - Use Stripe Elements / Apple Pay / Google Pay for production. Never collect raw card data.
402
+ - Address autofill: `autocomplete="postal-code"` etc.
403
+
404
+ ## Mobile-Specific
405
+
406
+ ### Keyboard
407
+
408
+ - Set the right `inputmode` and `type` so the right mobile keyboard appears.
409
+ - For numeric codes, `inputmode="numeric"` shows the number pad on iOS.
410
+ - For dates, `<input type="date">` shows the native date picker on most platforms.
411
+
412
+ ### Avoid auto-zoom
413
+
414
+ iOS auto-zooms when an input has font-size below 16px. Set inputs to >= 16px to prevent this.
415
+
416
+ ### Touch targets
417
+
418
+ Inputs at least 44px tall (`height: 44px` or sufficient padding). Same for buttons.
419
+
420
+ ### Sticky submit
421
+
422
+ For long mobile forms, consider a sticky submit bar at the bottom (with safe-area padding):
423
+
424
+ ```css
425
+ .sticky-submit {
426
+ position: sticky;
427
+ bottom: 0;
428
+ padding-bottom: max(16px, env(safe-area-inset-bottom));
429
+ }
430
+ ```
431
+
432
+ ## Form Accessibility
433
+
434
+ - Every input has a programmatic label.
435
+ - Required fields marked.
436
+ - Error messages associated via `aria-describedby`.
437
+ - Errors announced via `role="alert"` or `aria-live`.
438
+ - Focus moves to first invalid field on submit error.
439
+ - Group with `<fieldset>` + `<legend>`.
440
+ - Use `<form>` so password managers and SR landmarks detect it.
441
+
442
+ ## Common Form Mistakes
443
+
444
+ - Placeholder as label.
445
+ - No `autocomplete` attributes.
446
+ - Placeholders that look like real values ("name@example.com") confused with filled state.
447
+ - Validating on every keystroke.
448
+ - Errors only at the top, no inline indication.
449
+ - Generic errors ("Invalid", "Error").
450
+ - "Required" indicator on all fields when most are required.
451
+ - Submit button labeled "Submit".
452
+ - Disabled submit when fields are empty (instead, validate on submit attempt and show errors).
453
+ - No loading state on submit; user clicks twice and creates duplicates.
454
+ - No autosave on long forms.
455
+ - Fields without `type` (default `type="text"` shows the wrong keyboard on mobile).
456
+ - Asking for unnecessary data ("Why does signup need my phone?").
457
+ - Multi-line text in a single-line input.
458
+ - Cancel button styled as primary, submit as secondary.
459
+
460
+ ## Self-Healing for Forms
461
+
462
+ Before declaring work complete:
463
+
464
+ - [ ] Every input has a visible, programmatic label
465
+ - [ ] Right `type` and `inputmode` for the data
466
+ - [ ] `autocomplete` set per WHATWG spec
467
+ - [ ] Required fields marked visually + `aria-required`
468
+ - [ ] Errors inline, near the field, with cause + fix
469
+ - [ ] Errors announced via `role="alert"` or `aria-live`
470
+ - [ ] On submit error, focus moves to first invalid field
471
+ - [ ] Submit button has loading state, disabled during submission
472
+ - [ ] Submit button label is specific (verb-noun)
473
+ - [ ] Helper text for complex fields
474
+ - [ ] Long forms autosave; "Saved" indicator visible
475
+ - [ ] Multi-step forms show progress and allow back navigation
476
+ - [ ] Destructive actions confirm or provide undo
477
+ - [ ] Mobile: 16px+ font size, 44px+ touch targets
478
+ - [ ] `<form>` element used; password manager detects fields
479
+ - [ ] Tested with keyboard only, screen reader, and password manager
480
+
481
+ ## See Also
482
+
483
+ - [accessibility.md](accessibility.md) for input accessibility
484
+ - [ui-ux.md](ui-ux.md) for state design (loading, error, success)
485
+ - [motion.md](motion.md) for transitions on validation feedback