Skip to content
Download ZIP

Blue accent palette · 12.1 KB ZIP File receipt View as Markdown View code

Preview

Fit to the available width. The frame follows the height of its content; previews taller than the maximum auto-height scroll inside it.

Give this component to your coding agent Copy a prompt that fetches this exact version and palette through the PageSugar MCP server.
cmp_form_field_01 · version 1.0.0 · Blue accent palette
Using the PageSugar MCP server, fetch component cmp_form_field_01 version 1.0.0 with variant "blue", first inspect its requirements and license status and confirm this project uses Svelte 5 and Tailwind CSS 4. Retrieve every manifest file, including binary assets and any manifest-only response files, preserving relative paths. Then integrate the source and follow its usage notes. Run project checks, review the browser result and report anything unverified. Do not substitute another version or invent missing files.

Not connected yet? Set up the MCP server

Code

Palette
Blue accent
Version
1.0.0
Digest
Full digest
sha256-d61d686e3f410c5c80c4d76ea7678417633fba023b4cf4bc38278a961c354670
parts/FieldError.svelte Svelte · 1.7 KB Raw
<!--
	One specific validation message, between the hint and the control (the GOV.UK order), so a
	phone keyboard or a magnified screen never hides it under the control. A visually hidden
	prefix ("Error:") and the mark carry the meaning without colour. It is not a live region: it is
	read with the control through aria-describedby, and an error summary announces it on submit.
-->
<script lang="ts">
	import type { FormFieldSize } from '../types';

	interface Props {
		id: string;
		/** The message. Empty or omitted, nothing renders. */
		message?: string;
		/** Read before the message by assistive technology. Translate it with the page. */
		prefix?: string;
		/** Show the warning mark beside the message. */
		showIcon?: boolean;
		size?: FormFieldSize;
	}

	let { id, message, prefix = 'Error:', showIcon = true, size = 'default' }: Props = $props();
</script>

{#if message}
	<p
		{id}
		class={[
			'flex max-w-[34em] items-start gap-2 text-start font-medium text-[var(--_error)]',
			size === 'compact' ? 'text-xs/4' : 'text-[13px]/5'
		]}
	>
		{#if showIcon}
			<!-- One line tall, so the mark stays level with the first line of a wrapped message. -->
			<span class="grid h-lh shrink-0 place-items-center" aria-hidden="true">
				<svg class={size === 'compact' ? 'size-3.5' : 'size-4'} viewBox="0 0 16 16" fill="none">
					<circle cx="8" cy="8" r="6.25" stroke="currentColor" stroke-width="1.5" />
					<path d="M8 4.75v3.75" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" />
					<circle cx="8" cy="11.1" r="1" fill="currentColor" />
				</svg>
			</span>
		{/if}
		<span class="min-w-0 text-pretty break-words"
			><span class="sr-only">{`${prefix} `}</span>{message}</span
		>
	</p>
{/if}

Pass the label, and hint and error when you have them, and render the control in the control snippet by spreading the attributes it receives: id, aria-describedby, aria-invalid, required and the field's control class. The field wires the ids and styles the control; it does not validate values, decide when to show an error, move focus, keep form state, trim text to the count's limit, or group radios and checkboxes (use a fieldset for those).

Suggested location
src/lib/components/form-field-01
Required props
labelcontrol

Limitations

  • The field shows the error you pass; it never validates. Set error after your own validation, usually on submit, and pair it with an error summary at the top of the form.
  • The control styling applies only when you spread attributes.class onto a native input, select or textarea. Leave it off, or merge your own classes with it, to style the control yourself.
  • The drawn chevron applies to single, unsized selects; multiple and sized selects keep the platform's appearance.
  • The count counts what you pass as count.value (rounded to a whole, non-negative number); it does not read the control. Characters beyond the limit are allowed and shown as too many, never trimmed.
  • The count's announcement needs the browser; without JavaScript the visible count and the limit still render.
  • The error rule hangs 12 px into the start gutter, so a container with overflow hidden needs at least that much padding.
  • Not for groups of radios or checkboxes; those need a fieldset and legend.

Example

Svelte
<script lang="ts">
	import FormField from '$lib/components/form-field-01/FormField.svelte';

	let email = $state('');
	let error = $state<string>();

	function submit(event: SubmitEvent) {
		error = email.includes('@')
			? undefined
			: 'Enter an email address in the correct format, like name@example.com';
		if (error) event.preventDefault();
	}
</script>

<form method="post" novalidate onsubmit={submit} class="grid max-w-md gap-6">
	<FormField label="Email address" hint="We send your sign-in link here." {error} required>
		{#snippet control(attributes)}
			<input {...attributes} type="email" name="email" autocomplete="email" bind:value={email} />
		{/snippet}
	</FormField>
	<button type="submit">Send sign-in link</button>
</form>

Form field#

One field in the GOV.UK order: label, hint, error, control, then an optional character count. The field owns the ids. Your control receives them through the control snippet:

Svelte
<FormField label="Due date" hint="For example, 14 11 2026" {error} required>
	{#snippet control(attributes)}
		<input {...attributes} name="due" inputmode="numeric" bind:value={due} />
	{/snippet}
</FormField>

attributes is { id, 'aria-describedby', 'aria-invalid', required, class }:

Attribute What it holds
id The id prop, or one generated per instance. The label's for points at it.
aria-describedby The hint, then the error, then the count's limit, only for those that render.
aria-invalid 'true' while error is set, otherwise absent (never 'false').
required The required prop.
class The field's control styling for a native input, select or textarea.

Styling the control#

Spread everything and the control is finished: 40 px tall (36 px compact from sm up, at least 44 px on coarse pointers), 16 px text below sm so iOS does not zoom, a drawn chevron in the muted token on a single select, and disabled, read-only, invalid and autofill states. To set a width, merge a class:

Svelte
<input {...attributes} class="{attributes.class} max-w-40" />

To use your own control component, spread every attribute except class and style it yourself; the component must forward id and the aria-* attributes to its focusable element.

Errors#

The field shows the error you pass. It never validates and never decides when to show one: validate on submit, set error to a message that says what to do ("Enter a date in the past", not "Invalid"), and put an error summary at the top of the form that links to each field. The message is not a live region, because it is read with the control when focus arrives.

An error hangs a two-pixel rule 12 px into the start gutter beside the whole field, so a container with overflow: hidden needs at least that much padding on its start side.

Markers#

Pass id only when your own code needs it; the part ids add -hint, -error and -limit to it, so keep it unique on the page.

required shows requiredLabel ('(required)') after the label; optionalLabel shows on fields that are not required. GOV.UK recommends leaving required fields unmarked and marking the optional ones: set requiredLabel="" and optionalLabel="(optional)". Markers sit inside the label, so they are part of the control's accessible name.

Character count#

Pass count={{ value: text.length, max: 280 }}. The limit sentence is read with the control, the visible count follows the value, and a polite status repeats it one second after the value stops changing. With countThreshold, the count appears only when that many characters or fewer remain; its line is reserved until then. Nothing trims the value. Translate the wording with countMessages:

Svelte
countMessages={{
	limit: (max) => `Vous pouvez saisir jusqu'à ${max} caractères`,
	remaining: (n) => `Il vous reste ${n} caractères`,
	over: (n) => `Vous avez ${n} caractères en trop`
}}

Retoning#

On a #09090b band:

CSS
.band {
	background: #09090b;
	--form-field-accent: #fafafa;
	--form-field-ink: #fafafa;
	--form-field-muted: #a1a1aa;
	--form-field-border: rgb(255 255 255 / 0.4);
	--form-field-hairline: rgb(255 255 255 / 0.1);
	--form-field-surface: #18181b;
	--form-field-error: #f87171;
	--form-field-color-scheme: dark;
}

Props and content inputs#

On this page
NameTypeRequiredDefaultDescription
labelstringYesNoneVisible label text: the question or the name of the value. Always rendered as a <label for> the control.
controlSnippet<[FormFieldControlAttributes]>YesNoneRenders the control. Spread the attributes onto it: { id, 'aria-describedby', 'aria-invalid', required, class }.
hintstring | SnippetNoNoneGuidance under the label: a format or a constraint, not the label again. A snippet when it holds a link.
errorstringNoNoneThe current validation message. When set, it renders between the hint and the control, the control gets aria-invalid, and a rule hangs beside the field.
requiredbooleanNofalsePasses native required to the control and shows requiredLabel.
requiredLabelstringNo'(required)'Marker after the label of a required field, inside the accessible name. Set '' when you mark optional fields instead.
optionalLabelstringNoNoneMarker after the label of a field that is not required, such as '(optional)'. Omitted, no marker renders.
idstringNoNoneThe control's id. Generated per instance with $props.id() when omitted or empty; the part ids add -hint, -error and -limit to it, so keep it unique.
labelHiddenbooleanNofalseHides the label visually; it stays the control's accessible name. For a search field whose button explains it.
labelHeading1 | 2NoNoneWraps the label in an <h1> or <h2> at 24 px, for a page that asks one question.
size'default' | 'compact'No'default'default: 40 px controls, 14 px labels, 13 px hints. compact: 36 px controls from sm up, 13 px labels, 12 px hints. Both use 16 px control text below sm (40 px tall there) and at least 44 px on coarse pointers.
errorPrefixstringNo'Error:'Visually hidden text read before the error. Translate it with the page.
errorIconbooleanNotrueShows the warning mark beside the error, level with its first line.
countFormFieldCountNoNone{ value, max }: characters typed and the limit. Renders the count under the control and adds the limit to aria-describedby.
countThresholdnumberNoNoneShow the count only once this many characters or fewer remain. Its line is reserved meanwhile, so nothing moves when it appears.
countMessagesPartial<FormFieldCountMessages>NoNoneThe count's wording: limit(max), remaining(n) and over(n), each returning a sentence. Defaults to English.
classstringNoNoneExtra classes for the field's root, such as a width or grid placement.

Customization#

On this page

Change the words through props and retone the field through eight --form-field-* CSS variables. Hover edges, the placeholder and the disabled fill are mixed from those, so they follow any retone.

  • Markers: GOV.UK recommends marking optional fields and leaving required ones unmarked. For that, set requiredLabel='' on every required field and optionalLabel='(optional)' on the optional ones. Keep markers in words; an asterisk alone is not read reliably.
  • Hints and errors: a hint gives the format or a constraint ('For example, 27 3 1985'), never the label again. An error says what to do ('Enter a date in the past'), not 'Invalid input'.
  • Width: the control fills the field. Set the width on the field with class, or on the control by merging classes: <input {...attributes} class="{attributes.class} max-w-40" /> for a date or a postcode.
  • Ids: pass id when your own code needs the control's id. The hint, error and limit ids are that id plus -hint, -error and -limit, so keep every id you pass unique on the page. An empty id is treated as none.
  • Your own control: spread every attribute except class onto a custom component that forwards them to its input, and style it yourself.
  • Accent: --form-field-accent colours the two-pixel focus ring. Keep it 3:1 against the page.
  • Error: --form-field-error colours the message, the edge, the rule and a count over its limit. The default is red-700 (6.5:1 on white). On a dark fill use a lighter red such as #f87171.
  • Neutrals: --form-field-ink is the label and value, --form-field-muted the hint, marker and count, --form-field-border the control's edge (keep it 3:1), --form-field-hairline the disabled edge, --form-field-surface the control fill.
  • Worked retone for a #09090b band: --form-field-accent: #fafafa; --form-field-ink: #fafafa; --form-field-muted: #a1a1aa; --form-field-border: rgb(255 255 255 / 0.4); --form-field-hairline: rgb(255 255 255 / 0.1); --form-field-surface: #18181b; --form-field-error: #f87171; --form-field-color-scheme: dark.
  • Density: size='compact' for settings panels and tables; the default for sign-up, booking and checkout forms. Space fields 24 px apart (gap-6) so each hint groups with its own control.

Public CSS variables

VariableToken
--form-field-accentaccent
--form-field-inkink
--form-field-mutedmuted
--form-field-hairlinehairline
--form-field-borderborder
--form-field-surfacesurface
--form-field-errorerror
--form-field-color-schemecolorScheme

Accessibility#

On this page
  • The label is a native <label for> the control's id, so clicking it focuses the control and it is the control's accessible name, marker included. With labelHidden it is visually hidden, not removed.
  • aria-describedby lists the hint, then the error, then the count's limit sentence, and only those that render; with none, the attribute is absent.
  • aria-invalid='true' is set only while error is present; it is never set to 'false'.
  • The error is read with the control through aria-describedby and is not a live region, so it is not announced twice. Announce errors on submit with an error summary that links to each field, and move focus there yourself.
  • The error carries a visually hidden prefix ('Error:', translate it with errorPrefix), a warning mark, a two-pixel control edge and a rule beside the field, so it never depends on colour.
  • The limit sentence is read once with the control, the current count is ordinary text under it, and a polite status repeats the count one second after the value stops changing (cleared first, so the same sentence can be announced again). Nothing is announced on load.
  • Disabled and read-only are set on the control by you; the field styles them. Explain in the hint why a disabled value can't change.
  • Focus shows a two-pixel ring in the accent, two pixels outside the control's radius. Controls are at least 44 px tall on coarse pointers, and their text is 16 px below the sm breakpoint so iOS does not zoom. In forced-colours mode the edge becomes a real system-colour border.

Release details#

On this page
Integration
  • Local interaction
  • Works without client-side JavaScript
  • Server-side rendering supported
Dependencies
No additional runtime packages beyond Svelte and Tailwind CSS
License

MIT. Default license approval is pending; see the license status before adopting the source.

Version history
  • 1.0.0 (Published) Current release · 30 September 2026

Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.