Susegad UI
Register
Theme
Palette

Components

Field note

The note under a form field that says what is wrong and how to fix it. Follows constraint validation with its own wording (or shows a server message), never nags on the first try, wires aria-describedby and aria-invalid, focuses the first problem on submit.

quiet warm playful

Open the live demo npx susegad add field-note

Stands on: Core, Core: components, Tokens

The prompt

the prompt

The note under a form field that says what is wrong and how to fix it: "Enter an email address like name@example.com."

It waits until you leave a field you changed, follows your typing once it is showing, and goes the moment the value is right.

<link rel="stylesheet" href="susegad/components/field-note/field-note.css">
<script type="module" src="susegad/components/field-note/field-note.js"></script>

<label for="email">Email</label>
<input id="email" type="email" required>
<sg-field-note for="email" validate></sg-field-note>

<!-- a server's message, shown as written -->
<sg-field-note for="dates">Those dates are booked. The next free nights start on 16 October.</sg-field-note>
AttributeValuesWhat it does
fora field's idThe field it belongs to. Absent: the nearest field before it.
validatepresent or absentFollow the browser's constraint validation and word the message. Absent: show the text the note holds.
toneerror, hinthint is a margin note that never marks the field invalid.
data-value-missing, data-type-mismatch, data-pattern-mismatch, …textYour wording for one check, beating ours.

The prompt

Make an inline error web component, <sg-field-note for="email" validate>, that sits under a form field and uses the browser's own constraint validation: required, type, pattern, minlength, maxlength, min, max and step. Choose the message from the first failing check in the order a person should fix things, and word it to say what happened and how to fix it, in the person's terms ("Enter your email.", "Use at least 8 characters. You have 5."), never blaming them. Let the page override any check's wording with a data attribute, and show setCustomValidity() messages as they are. Do not show anything while someone types into a field for the first time. Show the note when they leave a field they changed or try to submit, then update it as they type and remove it the moment the value is valid. On a submit, show every note, move focus to the first field with a problem, and keep the other notes silent so they do not all speak at once. While a note shows an error, add its id to the field's aria-describedby without removing ids already there, set aria-invalid="true" on the field, start the note with a visually hidden "Error:", and make the note a polite live region that exists before any message. Without validate, show whatever text the note holds, so a server can render an error. Give it three registers. Quiet: small text in the danger colour with a circled mark, fading in within 120 ms. Warm: a margin note in the hand face, with examples, numbers and addresses set in the body face so a 1 never reads as an l, and a pencil arrow that hooks up into the field, seeded so each arrow is a little different and always the same, drawn in one stroke when the note appears. Playful: the same, inked on as it appears: the arrow is drawn, the words are written left to right, then a wavy line underlines them. Under reduced motion, show it all at once.

Words to code

When you sayTechniqueWhat happens
the browser's own constraint validationnative firstThe note reads field.validity and validationMessage. The invalid event is cancelled, so the browser's bubble never covers the page, and a form with novalidate still gets its notes.
the first failing check in the order a person should fix thingsorderingCHECKS puts valueMissing first, then badInput, typeMismatch and the rest. pickMessage() takes the first that fails. Unit-tested.
say what happened and how to fix itcopySTRINGS holds one function per check that uses the field's own limits and label ("Enter a number from 1 to 6.", "Enter your email."). Every string is in one place for the writer.
do not show anything while someone types … the first timestate machinenextShown() tracks dirty and shown: input marks the field dirty; blur shows only a dirty, invalid field; once shown, input follows validity; submit and server messages show at once; reset clears. Unit-tested step by step.
move focus to the first field with a problemfocusOn invalid, each note checks in a microtask whether its field is the first invalid one in form.elements and whether focus is already on a problem. Only then does it focus its field, and only on a submit.
keep the other notes silentlive regionsA submit sets aria-live="off" on the notes for that update. The focused field reads its own note through aria-describedby, and later changes speak politely again.
without removing ids already thereARIAtokenList() adds or removes one id in a space-separated list. The note removes aria-invalid only if it set it.
examples, numbers, addressestypographyIn the hand face a 1 reads as an l. The warm skin's valueRuns() finds email and web addresses and numbers (digits joined by spaces, commas, colons, slashes or dashes) and wraps them in .sg-field-note__value, set in the body face with tabular figures. The text itself is unchanged; a test checks nothing is lost.
a pencil arrow … seededseedarrowPath(seed) in the warm skin makes a cubic curve from the note up to the field, and a two-stroke head that follows the curve's last direction. A Park–Miller generator seeded from the note's id wobbles each point.
drawn in one strokeWAAPIThe dash is set to the path's length and slid to 0: first the shaft, then the head. The animation is cancelled when it finishes.
the words are written left to rightclip-pathclip-path: inset(0 100% 0 0) to inset(0), with the time scaled to the message's length (up to 0.9 s).
a wavy line underlines themseedsquiggle(width, seed) makes quadratic waves to the text's measured width, redrawn when the words change.
under reduced motion, show it all at oncestillEvery skin checks ctx.motion === 'still' and skips its animations.

<sg-field-note> is the note under a form field that says what is wrong and how to fix it. It follows the browser's own validation, or shows a message from your server. It never nags while someone is still typing their first try.

Use

<link rel="stylesheet" href="susegad/components/field-note/field-note.css">
<script type="module" src="susegad/components/field-note/field-note.js"></script>

<label for="email">Email</label>
<input id="email" type="email" required>
<sg-field-note for="email" validate></sg-field-note>

<label for="phone">Phone</label>
<input id="phone" pattern="[0-9 ]{10,12}">
<sg-field-note for="phone" validate data-pattern-mismatch="Use 10 digits, like 98220 12345."></sg-field-note>
<sg-field-note for="phone" tone="hint">We only call about this booking.</sg-field-note>

A server can render an error directly, or swap one in with htmx:

<sg-field-note for="dates">Those dates are booked. The next free nights start on 16 October.</sg-field-note>

Style the field yourself from aria-invalid, which the note sets:

input[aria-invalid="true"] { border-color: var(--sg-danger); }

Attributes

AttributeValuesDefault
forthe field's idthe nearest field before the note
validatefollow constraint validationabsent: show the note's own text
toneerror, hinterror
data-value-missing, data-type-mismatch, data-pattern-mismatch, data-too-short, data-too-long, data-range-underflow, data-range-overflow, data-step-mismatch, data-bad-inputyour wording for that checkours
registerquiet, warm, playfulinherited

Methods and events

note.setMessage(text)Show a message from the page or a server; empty text clears it.
note.check()Check the field now, as a submit would. Returns whether it is valid.
note.shown, note.messageWhat it shows now.
sg-field-noteBubbles on every change: { shown, message, field }.

When it shows

  1. Typing into a field the first time: nothing.
  2. Leaving a field you changed, while it is invalid: the note appears.
  3. While it shows: it follows your typing and goes as soon as the value is valid.
  4. Trying to submit: every note shows, focus goes to the first problem, and only that one is read aloud.
  5. Resetting the form clears every note.

Registers

LookMotion
quietsmall text in the danger colour, a circled markfades in within 120 ms
warma margin note in the hand face, a pencil arrow hooked up into the field; examples, numbers and addresses in the body facethe arrow is drawn in one stroke
playfulthe margin note, underlined with a wavy linethe arrow is drawn, the words written left to right, then the underline

A hint is a margin note in the soft ink, with no arrow. Under reduced motion every register shows the finished note.

Starting over

A component that empties a field as part of a new choice (a date range starting a new stay empties the departure) dispatches sg-reset on the field. The note starts over, as on a form reset: nothing is wrong until the person leaves the field empty or submits.

Accessibility

  • While the note shows an error, its id is in the field's aria-describedby (ids already there are kept) and the field has aria-invalid="true". The note removes aria-invalid only if it set it.
  • A note never sits inside its field's label, where it would become part of the field's name. One written there steps out to just after the label when it connects. The field's name in a message ("Enter your departure.") comes from a copy of the label with notes, hidden text and pictures removed, or from aria-labelledby when set.
  • The note starts with a visually hidden "Error:", so the tone is heard. The mark and the arrow are decoration (aria-hidden).
  • The note is a polite live region from the start. After a submit it stays silent for that update, because focus moves to the first problem and the field reads its own note.
  • Focus moves only on a submit attempt (to the first problem, as the browser does), never on blur or while typing.
  • A note never appears in the middle of a click. Pressing Send blurs the field you were in; if its note appeared then, it would push Send down and the click would miss. While a pointer is down, the note waits until the click has finished.
  • A note that is asked to show what it already shows writes nothing, so a screen reader never reads it twice.
  • Contrast: the danger colour is 5.1:1 or more on every surface, in every palette and theme.
  • In warm and playful the sentence is in the hand, but anything a person must copy (an example, a number, an email or web address) is set in the body face with tabular figures, because in the hand a 1 reads as an l. valueRuns() in the warm skin finds them.

Checks

node --test packages/components/field-note/field-note.test.js covers message choice, the showing state machine, aria-describedby handling and the seeded arrow. node packages/components/field-note/field-note.check.mjs checks the behaviour in Chromium: first-try typing, blur, fixing, submit and focus, silence after a submit, server messages and hints.