react.form.styles
An optional stylesheet for the react.form elements' caps-* classes, themed by --caps-form-* custom properties.
React ^19 · TypeScript only Written for React: use it from a TypeScript project with React installed.
1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 21 tests, run in TypeScript.
What it does
An optional stylesheet for the `react.form` elements, as a function that returns CSS text. The elements are fully usable without it: they render plain, accessible HTML with stable `caps-*` class names, and a browser's default styles work. This gives them a clean GOV.UK-like look in one line, and every colour, the radius and the font are CSS custom properties, so an app can restyle it with its own CSS without calling it again.
// app/layout.tsx: a server component, so the CSS is in the HTML, not in JavaScript
import { formStyles } from "#fune/react.form.styles@^1";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head><style>{formStyles({})}</style></head>
<body>{children}</body>
</html>
);
}
For example
formStyles()→ :root{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff;--cap… the default theme: GOV.UK colours, a yellow focus ring, dark mode by prefers-color-schemeformStyles(accent #00703c)→ :root{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#00703c;--caps-form-accent-text:#ffffff;--cap… an accent colour changes only --caps-form-accentformStyles(error #b10e1e)→ :root{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff;--cap… an error colour recolours every error bar, border and message through one property
The function
Written for React ^19, in TypeScript only.
export function formStyles(theme: FormTheme): string
| theme | FormTheme | every field optional: {} is the default GOV.UK-like look |
| returns | string | CSS text, one rule per line, for a <style> element or a .css file |
The types it declares, generated into your project
export type FormDarkMode = "media" | "attribute" | "none";
/** Overrides for the stylesheet's defaults; any left out keep them. */
export interface FormTheme {
/** buttons and ticked boxes; GOV.UK blue by default */
readonly accent?: string;
/** the focus ring; GOV.UK yellow by default */
readonly focus?: string;
/** error bars, borders and messages; GOV.UK red by default */
readonly error?: string;
/** text; near-black by default */
readonly text?: string;
/** hints and counts; dark grey by default */
readonly muted?: string;
/** input borders; near-black by default */
readonly border?: string;
/** input backgrounds; white by default */
readonly background?: string;
/** corner radius of inputs and buttons, such as 4px; 0 by default */
readonly radius?: string;
/** font-family; the system UI font by default, no web font */
readonly font?: string;
/** one selector to limit every rule to, such as .signup; the whole page by default */
readonly scope?: string;
/** media (the default) follows prefers-color-scheme, attribute follows data-theme="dark", none has no dark mode */
readonly dark?: FormDarkMode;
}
Your code names it in one line, in the file that uses it
import { formStyles } from "#fune/react.form.styles@^1";
import type { FormTheme } from "./react_form_styles_types.ts";
// [custom property, light default, the theme field that overrides it]
const LIGHT: readonly (readonly [string, string, keyof FormTheme | null])[] = [
["text", "#0b0c0c", "text"],
["muted", "#505a5f", "muted"],
["background", "#ffffff", "background"],
["border", "#0b0c0c", "border"],
["accent", "#1d70b8", "accent"],
["accent-text", "#ffffff", null],
["secondary", "#f3f2f1", null],
["focus", "#ffdd00", "focus"],
["focus-text", "#0b0c0c", null],
["error", "#d4351c", "error"],
["radius", "0", "radius"],
["font", 'system-ui,-apple-system,"Segoe UI",Roboto,Arial,sans-serif', "font"],
];
// The dark palette always replaces the light colours, the theme's included:
// a colour chosen for a light page is rarely readable on a dark one.
const DARK: readonly (readonly [string, string])[] = [
["text", "#f3f2f1"],
["muted", "#b1b4b6"],
["background", "#0b0c0c"],
["border", "#b1b4b6"],
["accent", "#5694ca"],
["accent-text", "#0b0c0c"],
["secondary", "#383f43"],
["error", "#f47067"],
];
const FOCUS_RING = "outline:3px solid var(--caps-form-focus);outline-offset:0;box-shadow:0 0 0 6px var(--caps-form-focus-text)";
// [selectors, declarations]; every selector is prefixed with the scope.
const RULES: readonly (readonly [string, string])[] = [
[".fune-field", "margin:0 0 1.875rem;font-family:var(--caps-form-font);font-size:1.1875rem;line-height:1.3;color:var(--caps-form-text)"],
[".fune-field--error", "padding-left:.9375rem;border-left:5px solid var(--caps-form-error)"],
[".fune-fieldset", "min-width:0;margin:0;padding:0;border:0"],
[".fune-label,.fune-legend", "display:block;max-width:100%;margin:0 0 .3125rem;padding:0;font-weight:400"],
[".fune-legend--heading", "margin-bottom:.9375rem"],
[".fune-legend-heading", "margin:0;font-size:2rem;font-weight:700;line-height:1.1"],
[".fune-hint", "margin:0 0 .9375rem;color:var(--caps-form-muted)"],
[".fune-error-message", "margin:0 0 .9375rem;font-weight:700;color:var(--caps-form-error)"],
[".fune-visually-hidden", "position:absolute!important;width:1px!important;height:1px!important;margin:0!important;padding:0!important;overflow:hidden!important;clip:rect(0 0 0 0)!important;clip-path:inset(50%)!important;border:0!important;white-space:nowrap!important"],
[".fune-input,.fune-textarea,.fune-select", "box-sizing:border-box;min-height:2.75rem;max-width:100%;padding:.3125rem .5rem;border:2px solid var(--caps-form-border);border-radius:var(--caps-form-radius);background:var(--caps-form-background);color:var(--caps-form-text);font:inherit"],
[".fune-input,.fune-textarea", "width:100%"],
[".fune-textarea", "display:block;min-height:6rem;resize:vertical"],
[".fune-select", "min-width:11.5em"],
[".fune-input--error,.fune-textarea--error,.fune-select--error", "border-color:var(--caps-form-error)"],
[".fune-input:focus,.fune-textarea:focus,.fune-select:focus", "outline:3px solid var(--caps-form-focus);outline-offset:0;box-shadow:inset 0 0 0 2px var(--caps-form-border)"],
[".fune-input:disabled,.fune-textarea:disabled,.fune-select:disabled", "opacity:.5;cursor:not-allowed"],
[".fune-input--width-2", "max-width:5.4ex"],
[".fune-input--width-4", "max-width:9ex"],
[".fune-input-wrapper", "display:flex"],
[".fune-input-wrapper .fune-input", "min-width:0"],
[".fune-input-prefix,.fune-input-suffix", "display:flex;align-items:center;padding:0 .5rem;border:2px solid var(--caps-form-border);background:var(--caps-form-secondary);white-space:nowrap"],
[".fune-input-prefix", "border-right:0"],
[".fune-input-suffix", "border-left:0"],
[".fune-character-count__message", "margin:.3125rem 0 0;color:var(--caps-form-muted)"],
[".fune-character-count__message--over", "font-weight:700;color:var(--caps-form-error)"],
[".fune-checkbox,.fune-radio", "display:grid;grid-template-columns:2.75rem 1fr;column-gap:.5rem;align-items:center;margin:0 0 .625rem"],
[".fune-checkbox-input,.fune-radio-input", "justify-self:center;width:2.5rem;height:2.5rem;margin:0;accent-color:var(--caps-form-accent);cursor:pointer"],
[".fune-checkboxes--small .fune-checkbox-input,.fune-radios--small .fune-radio-input", "width:1.5rem;height:1.5rem"],
[".fune-checkbox-input:focus-visible,.fune-radio-input:focus-visible", FOCUS_RING],
[".fune-checkbox-label,.fune-radio-label", "display:flex;align-items:center;min-height:2.75rem;margin:0;cursor:pointer"],
[".fune-checkbox-hint,.fune-radio-hint", "grid-column:2;margin:0 0 .3125rem"],
[".fune-checkboxes-divider,.fune-radios-divider", "width:2.75rem;margin:0 0 .625rem;text-align:center"],
[".fune-radios--inline", "display:flex;flex-wrap:wrap;column-gap:1.25rem"],
[".fune-date-input", "display:flex;flex-wrap:wrap;gap:0 1.25rem"],
[".fune-date-input-item .fune-field", "margin-bottom:0"],
[".fune-password-field__toggle", "min-width:4.5rem;min-height:2.75rem;margin-left:.5rem;padding:0 .75rem;border:2px solid var(--caps-form-border);border-radius:var(--caps-form-radius);background:var(--caps-form-secondary);color:var(--caps-form-text);font:inherit;font-weight:700;cursor:pointer"],
[".fune-password-field__toggle:focus-visible", FOCUS_RING],
[".fune-password-checklist", "margin:.625rem 0 0;padding:0;list-style:none"],
[".fune-password-checklist__item", "position:relative;margin:0 0 .3125rem;padding-left:1.5rem"],
// A mark as well as the colour: colour alone must not carry meaning (WCAG 1.4.1).
[".fune-password-checklist__item::before", "position:absolute;left:0;font-weight:700"],
[".fune-password-checklist__item--met::before", 'content:"\\2713"'],
[".fune-password-checklist__item--unmet", "color:var(--caps-form-muted)"],
[".fune-password-checklist__item--unmet::before", 'content:"\\2013"'],
[".fune-button", "display:inline-flex;align-items:center;justify-content:center;gap:.5rem;box-sizing:border-box;min-height:2.75rem;margin:0 0 1.875rem;padding:.5rem 1rem;border:2px solid transparent;border-radius:var(--caps-form-radius);background:var(--caps-form-accent);color:var(--caps-form-accent-text);font-family:var(--caps-form-font);font-size:1.1875rem;font-weight:700;line-height:1.2;text-decoration:none;cursor:pointer"],
[".fune-button--secondary", "border-color:var(--caps-form-border);background:var(--caps-form-secondary);color:var(--caps-form-text)"],
[".fune-button--warning", "background:var(--caps-form-error);color:var(--caps-form-accent-text)"],
[".fune-button:hover", "filter:brightness(.9)"],
[".fune-button:focus-visible", "outline:3px solid transparent;background:var(--caps-form-focus);color:var(--caps-form-focus-text);box-shadow:inset 0 -4px 0 var(--caps-form-focus-text)"],
[".fune-button:disabled,.fune-button[aria-disabled=true],.fune-button--loading", "opacity:.6;filter:none;cursor:not-allowed"],
[".fune-error-summary", "margin:0 0 1.875rem;padding:1rem;border:5px solid var(--caps-form-error);color:var(--caps-form-text);font-family:var(--caps-form-font);font-size:1.1875rem;line-height:1.3"],
[".fune-error-summary:focus", FOCUS_RING],
[".fune-error-summary-title", "margin:0 0 .9375rem;font-size:1.5rem;font-weight:700;line-height:1.25"],
[".fune-error-summary-list", "margin:0;padding:0;list-style:none"],
[".fune-error-summary-list li", "margin:0 0 .3125rem"],
[".fune-error-summary a", "font-weight:700;color:var(--caps-form-error)"],
[".fune-error-summary a:focus", "outline:3px solid transparent;background:var(--caps-form-focus);color:var(--caps-form-focus-text);box-shadow:0 4px var(--caps-form-focus-text);text-decoration:none"],
];
/**
* An optional stylesheet for the react.form elements' caps-* classes, as CSS
* text. Everything themeable is a --caps-form-* custom property, so an app
* can restyle it with its own CSS without calling this again.
*/
export function formStyles(theme: FormTheme): string {
const scope = option(theme, "scope");
const dark = option(theme, "dark") ?? "media";
if (dark !== "media" && dark !== "attribute" && dark !== "none") {
throw new Error(`invalid theme.dark: "${dark}" is not media, attribute or none`);
}
const root = scope ?? ":root";
const light = LIGHT.map(([name, value, key]) => `--caps-form-${name}:${(key ? option(theme, key) : null) ?? value}`);
const darkVars = DARK.map(([name, value]) => `--caps-form-${name}:${value}`).join(";") + ";color-scheme:dark";
const lines = [`${root}{${light.join(";")}}`];
if (dark === "media") lines.push(`@media (prefers-color-scheme:dark){${root}{${darkVars}}}`);
if (dark === "attribute") {
const selector = scope ? `[data-theme=dark] ${scope},${scope}[data-theme=dark]` : "[data-theme=dark]";
lines.push(`${selector}{${darkVars}}`);
}
for (const [selectors, declarations] of RULES) {
const scoped = scope ? selectors.split(",").map((s) => `${scope} ${s}`).join(",") : selectors;
lines.push(`${scoped}{${declarations}}`);
}
return lines.join("\n") + "\n";
}
// A theme value, or null when left out or empty. Values go into the CSS as
// they are, so anything that could end a declaration, a rule or the <style>
// element is refused rather than escaped.
function option(theme: FormTheme, key: keyof FormTheme): string | null {
const value = theme[key];
if (value === undefined || value === null || value === "") return null;
if (typeof value !== "string") throw new Error(`invalid theme.${key}: must be a string`);
const allowed = key === "scope" ? "" : ">";
for (const ch of value) {
if ((";{}<\\" + allowed).includes(ch)) throw new Error(`invalid theme.${key}: "${ch}" is not allowed, it could break out of the stylesheet`);
if (ch < " " || ch === "\u007f") throw new Error(`invalid theme.${key}: control characters are not allowed`);
}
if (value.includes("/*")) throw new Error(`invalid theme.${key}: "/*" is not allowed, it could break out of the stylesheet`);
if (key === "scope" && value.includes(",")) throw new Error(`invalid theme.scope: one selector only, no ","`);
let quote = "";
for (const ch of value) {
if (quote) {
if (ch === quote) quote = "";
} else if (ch === '"' || ch === "'") {
quote = ch;
}
}
if (quote) throw new Error(`invalid theme.${key}: a quote is not closed`);
return value;
}Install
npm install react react-dom
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project) with React ^19 installed (Functional Weave does not ship it, and fune build stops with the npm install line if it is missing), fune build resolves it and nothing else, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add react.form.styles
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./react.form.styles-1.0.0-typescript.fune, or fetch it from a terminal with fune pull react.form.styles@1.0.0:typescript.
It is React ^19 · TypeScript only, so there is no package for Python or Rust. The full package is one file too: react.form.styles-1.0.0.fune, 106,656 bytes, sha256 63ec153a2b6a17179e38f42224ca4903a413e1f7b9a669f41edfe7fb9555ab5d.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before react.form.styles
after — your function gets the result and the arguments, and returns the final result.
// fune: after react.form.styles
replace — it requires no other capability, so there is no dependency to replace.
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show react.form.styles --steps.
// fune: step react.form.styles after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript alone, with the React in tooling/react, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| the default theme: GOV.UK colours, a yellow focus ring, dark mode by prefers-color-scheme | → | :root{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff;--cap… | |
| an accent colour changes only --caps-form-accent | accent #00703c | → | :root{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#00703c;--caps-form-accent-text:#ffffff;--cap… |
| an error colour recolours every error bar, border and message through one property | error #b10e1e | → | :root{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff;--cap… |
| text, hint, border, background and focus colours together; the dark palette is unchanged | text #1a1a1a, muted #6b6b6b, border #767676, background #fafafa, focus #fd0 | → | :root{--caps-form-text:#1a1a1a;--caps-form-muted:#6b6b6b;--caps-form-background:#fafafa;--caps-form-border:#767676;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff;--cap… |
| a radius and a font; quoted family names are fine | radius 4px, font "Helvetica Neue",Arial,sans-serif | → | :root{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff;--cap… |
| a scope limits every rule and the custom properties to one container | scope .signup | → | .signup{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff;--c… |
| a scope may use a child combinator | scope main > form | → | main > form{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff… |
| dark none leaves out the dark palette | dark none | → | :root{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff;--cap… |
| dark attribute follows data-theme="dark" on any ancestor instead of the system setting | dark attribute | → | :root{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff;--cap… |
| dark attribute with a scope matches the attribute on an ancestor or on the container itself | dark attribute, scope .signup | → | .signup{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff;--c… |
Show the other 11 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| empty strings are the same as leaving a field out | accent , scope , font | → | :root{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff;--cap… |
| a double quote inside single quotes does not open a quote | font "Gill Sans",'O"Brien',sans-serif | → | :root{--caps-form-text:#0b0c0c;--caps-form-muted:#505a5f;--caps-form-background:#ffffff;--caps-form-border:#0b0c0c;--caps-form-accent:#1d70b8;--caps-form-accent-text:#ffffff;--cap… |
| a colour that would end the declaration and add a rule is refused | accent red;}body{display:none | → | error: invalid theme.accent: ";" is not allowed |
| a scope that would close the style element is refused | scope </style><script> | → | error: invalid theme.scope: "<" is not allowed |
| a font with a quote left open is refused | font "Helvetica Neue,Arial | → | error: invalid theme.font: a quote is not closed |
| a scope listing two selectors is refused, since only the first would be scoped | scope .a,.b | → | error: invalid theme.scope: one selector only |
| an unknown dark mode is refused | dark auto | → | error: invalid theme.dark: "auto" is not media, attribute or none |
| a comment opener, which would swallow the rest of the stylesheet, is refused | radius 4px/* | → | error: invalid theme.radius: "/*" is not allowed |
| a newline is refused | focus #fd0 | → | error: invalid theme.focus: control characters are not allowed |
| a brace in a value is refused | border red} | → | error: invalid theme.border: "}" is not allowed |
| a > in a colour is refused (only a scope may use it) | text #000> | → | error: invalid theme.text: ">" is not allowed |
More from the author
Or write it to a file at build time and link it like any other stylesheet:
import { writeFileSync } from "node:fs";
writeFileSync("public/forms.css", formStyles({ accent: "#00703c", radius: "4px" }));## The theme
Every field is optional; `{}` is the default, and an empty string is the same as leaving a field out.
| Field | Custom property | Default | |---|---|---| | `text` | `--caps-form-text` | `#0b0c0c` | | `muted` | `--caps-form-muted` (hints, counts) | `#505a5f` | | `background` | `--caps-form-background` (inputs) | `#ffffff` | | `border` | `--caps-form-border` | `#0b0c0c` | | `accent` | `--caps-form-accent` (buttons, ticked boxes) | `#1d70b8` | | | `--caps-form-accent-text` (text on the accent and warning buttons) | `#ffffff` | | | `--caps-form-secondary` (secondary button, prefix and suffix) | `#f3f2f1` | | `focus` | `--caps-form-focus` (the focus ring) | `#ffdd00` | | | `--caps-form-focus-text` (text on focus yellow, the ring's dark edge) | `#0b0c0c` | | `error` | `--caps-form-error` | `#d4351c` | | `radius` | `--caps-form-radius` | `0` | | `font` | `--caps-form-font` | the system UI font; no web font is loaded | | `scope` | | none: rules apply to the whole page | | `dark` | | `media` |
- **scope** is one selector, such as `.signup` or `main > form`: every rule becomes `.signup .fune-field`, and the custom properties are set on `.signup` instead of `:root`, so the styles stay inside that container. - **dark** is `media` (the dark palette under `@media (prefers-color-scheme: dark)`), `attribute` (under `[data-theme=dark]` on any ancestor, or on the scope's element itself, for a site with its own theme switch) or `none`. The dark palette replaces the colours, the theme's included, because a colour picked for a light page is rarely readable on a dark one; to theme dark mode too, set the custom properties in your own dark rule (or use `dark: "none"` and do it all yourself).
To theme without calling again, override the properties anywhere after the stylesheet: `.checkout { --caps-form-accent: #00703c; --caps-form-radius: 6px }`.
Theme values are put into the CSS as they are, so anything that could end a declaration or a rule, or close the `<style>` element, is refused rather than escaped: `;`, `{`, `}`, `<`, `\`, `/*`, control characters, a quote left open, and `>` except in the scope (where it is the child combinator). A scope must be one selector, since in `.a, .b` only `.b` would be scoped. The error names the field: `invalid theme.accent: ";" is not allowed, it could break out of the stylesheet`.
## What it styles
Every class the `react.form` elements render: `fune-field` (and `fune-field--error`, a red bar on the left, as GOV.UK marks a field in error), `fune-fieldset`, `fune-label`, `fune-legend` (`--heading`), `fune-legend-heading`, `fune-hint`, `fune-error-message`, `fune-visually-hidden` (the standard clip pattern: hidden on screen, read by screen readers), `fune-input` (`--error`, `--width-2`, `--width-4`), `fune-input-wrapper`, `fune-input-prefix`, `fune-input-suffix`, `fune-textarea` (`--error`), `fune-character-count__message` (`--over`), `fune-select` (`--error`), `fune-checkbox`/`fune-radio` with their `-input`, `-label` and `-hint`, `fune-checkboxes--small`, `fune-radios--small`, `fune-radios--inline`, the `-divider`s, `fune-date-input` and `fune-date-input-item`, `fune-password-field__toggle`, `fune-password-checklist` and its items (`--met` gets a tick and `--unmet` a dash, so the state is not shown by colour alone), `fune-button` (`--secondary`, `--warning`, `--loading`, and `disabled` or `aria-disabled="true"`) and `fune-error-summary` with its `-title`, `-list` and links.
The choices, from the GOV.UK Design System and WCAG 2.2:
- **Focus is loud**: a 3px yellow outline with a dark edge on checkboxes, radios and the error summary, the input border thickened inside the yellow on inputs, and a yellow button with a dark underline, so it shows on any background, light or dark (WCAG 2.4.7 and 2.4.13). - **Targets are at least 44px**: inputs, buttons, the password toggle and each checkbox and radio row (its label is clickable too). Checkboxes and radios are the native inputs, 40px, coloured with `accent-color`, so they keep every browser's keyboard and screen reader behaviour. - Text is 19px (1.1875rem), as GOV.UK uses for form text, and inputs inherit it, which also stops iOS zooming into a focused input.
Sources: GOV.UK Design System, https://design-system.service.gov.uk/ (components: text input, error message, checkboxes, radios, date input, button, error summary; "Colour" https://design-system.service.gov.uk/styles/colour/); WCAG 2.2, 2.5.8 Target Size and 2.4.13 Focus Appearance https://www.w3.org/TR/WCAG22/; MDN, `accent-color` https://developer.mozilla.org/en-US/docs/Web/CSS/accent-color.
Files
| Path | Bytes |
|---|---|
| README.md | 5,672 |
| impl/typescript.ts | 10,262 |
| vectors.json | 85,109 |