Functional Weave
Code in TypeScript

react.form.checkbox

One checkbox with its label beside it, such as "I agree to the terms" (GOV.UK single checkbox).

React ^19 · TypeScript only A React component: use it from a TypeScript project with React installed. A client component ("use client").

1.0.1 · published 2026-10-03 by charlie · Anterra

Pinned by 14 tests, run in TypeScript.

What it does

One checkbox on its own, with its label to the right of the box: "I agree to the terms", "Remember me on this device", "Send me news by email". It is the GOV.UK Design System checkbox used to toggle a single option on or off, with an optional hint under the label and an error message above the box.

"use client";
import { Checkbox } from "#fune/react.form.checkbox@^1";

<Checkbox id="terms" label="I agree to the terms and conditions" required
  error={errors.terms} checked={agreed} onChange={setAgreed} />

For example

  1. a checkbox with its label to the right, submitting yes

    <Checkbox id="terms" label="I agree to the terms and conditions" />

    renders

    <div class="fune-field"><div class="fune-checkbox"><input class="fune-checkbox-input" id="terms" type="checkbox" name="terms" value="yes"/><label class="fune-label fune-checkbox-label" for="terms">I agree to the terms and conditions</label></div></div>
  2. a name and value of its own

    <Checkbox
      id="c1"
      name="marketing"
      value="opt-in"
      label="Send me news by email"
    />

    renders

    <div class="fune-field"><div class="fune-checkbox"><input class="fune-checkbox-input" id="c1" type="checkbox" name="marketing" value="opt-in"/><label class="fune-label fune-checkbox-label" for="c1">Send me news by email</label></div></div>

The function

Written for React ^19, in TypeScript only. A component: it takes its props and renders HTML, which the tests below pin exactly.

export function Checkbox(props: CheckboxProps): JSX.Element

Its props, CheckboxProps. A ? marks one the caller may leave out.

labelReactNodewhat ticking it means, shown to the right of the box
id?stringthe checkbox's id; React's useId() when left out
name?stringwhat it is submitted as; the id when left out
value?stringwhat is submitted when it is ticked; yes when left out
checked?booleanwhether it is ticked, for a controlled checkbox (with onChange)
defaultChecked?booleanwhether it starts ticked, for an uncontrolled one
onChange?(value: boolean) => voidcalled with true when it is ticked and false when it is cleared
onBlur?() => voidcalled when the checkbox loses focus, to mark the field touched
hint?ReactNodehelp under the label
error?ReactNodewhat is wrong; marks the checkbox invalid and describes it
required?booleanthe browser refuses the form until it is ticked
disabled?booleanshown but not changeable, and not submitted
className?stringadded to the wrapper's classes
rendersJSX.Element

The type it declares, generated into your project

/** One thing a person turns on or off, such as agreeing to the terms. */
export interface CheckboxProps {
  /** what ticking it means, shown to the right of the box */
  readonly label: ReactNode;
  /** the checkbox's id; React's useId() when left out */
  readonly id?: string;
  /** what it is submitted as; the id when left out */
  readonly name?: string;
  /** what is submitted when it is ticked; yes when left out */
  readonly value?: string;
  /** whether it is ticked, for a controlled checkbox (with onChange) */
  readonly checked?: boolean;
  /** whether it starts ticked, for an uncontrolled one */
  readonly defaultChecked?: boolean;
  /** called with true when it is ticked and false when it is cleared */
  readonly onChange?: (value: boolean) => void;
  /** called when the checkbox loses focus, to mark the field touched */
  readonly onBlur?: () => void;
  /** help under the label */
  readonly hint?: ReactNode;
  /** what is wrong; marks the checkbox invalid and describes it */
  readonly error?: ReactNode;
  /** the browser refuses the form until it is ticked */
  readonly required?: boolean;
  /** shown but not changeable, and not submitted */
  readonly disabled?: boolean;
  /** added to the wrapper's classes */
  readonly className?: string;
}

Your code names it in one line, in the file that uses it

import { Checkbox } from "#fune/react.form.checkbox@^1";
impl/typescript.tsx · 58 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

"use client";

import { useId, type JSX, type ReactNode } from "react";
import { fieldIds } from "./react_form_form_field.ts";  ← from react.form.form-field ^1.0.0 · built alongside by fune
import type { CheckboxProps } from "./react_form_checkbox_types.ts";

function present(node: ReactNode): boolean {
  return node !== null && node !== undefined && node !== false && node !== "";
}

/**
 * One GOV.UK checkbox on its own: the error message above, then the box with
 * its label to the right and its hint under the label. It cannot use
 * FormField, whose label comes before the control, so it takes the same ids
 * from fieldIds. Unlike a checkbox in a group, it is a control of its own, so
 * the error describes it and marks it aria-invalid.
 */
export function Checkbox(props: CheckboxProps): JSX.Element {
  const auto = useId();
  const { label, value, checked, defaultChecked, onChange, onBlur, hint, error, required, disabled, className } = props;
  const id = props.id ?? auto;
  const ids = fieldIds(id, present(hint), present(error));
  const classes = ["fune-field", ids.error ? "fune-field--error" : null, className || null].filter(Boolean).join(" ");
  return (
    <div className={classes}>
      {ids.error ? (
        <p className="fune-error-message" id={ids.error}>
          <span className="fune-visually-hidden">Error:</span> {error}
        </p>
      ) : null}
      <div className="fune-checkbox">
        <input
          className="fune-checkbox-input"
          id={ids.control}
          name={props.name ?? id}
          type="checkbox"
          value={value ?? "yes"}
          checked={checked}
          defaultChecked={defaultChecked}
          required={required}
          disabled={disabled}
          aria-describedby={ids.describedBy ?? undefined}
          aria-invalid={ids.error ? true : undefined}
          onBlur={onBlur ? () => onBlur() : undefined}
          onChange={(event) => onChange?.(event.target.checked)}
        />
        <label className="fune-label fune-checkbox-label" htmlFor={ids.control}>
          {label}
        </label>
        {ids.hint ? (
          <div className="fune-hint fune-checkbox-hint" id={ids.hint}>
            {hint}
          </div>
        ) : null}
      </div>
    </div>
  );
}

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 its 1 dependency, 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.checkbox
Download for TypeScript react.form.checkbox-1.0.1-typescript.fune · 17,599 bytes sha256 46381b3b15d08066221a2842a329224b3a7f48ec7e2992a9fdc9939bd854f8f4

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./react.form.checkbox-1.0.1-typescript.fune, or fetch it from a terminal with fune pull react.form.checkbox@1.0.1: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.checkbox-1.0.1.fune, 17,571 bytes, sha256 12202cc73d1122e81fe87afe337ff39422336f629324450d07cadd9278b372ca.

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.checkbox

after — your function gets the result and the arguments, and returns the final result.

// fune: after react.form.checkbox

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace react.form.form-field in react.form.checkbox

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.checkbox --steps.

// fune: step react.form.checkbox 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. A component’s tests are render tests: the props go in, and the HTML renderToStaticMarkup makes of them has to match exactly; a React warning during the render fails the test.

  1. a checkbox with its label to the right, submitting yes

    <Checkbox id="terms" label="I agree to the terms and conditions" />

    renders

    <div class="fune-field"><div class="fune-checkbox"><input class="fune-checkbox-input" id="terms" type="checkbox" name="terms" value="yes"/><label class="fune-label fune-checkbox-label" for="terms">I agree to the terms and conditions</label></div></div>
  2. a name and value of its own

    <Checkbox
      id="c1"
      name="marketing"
      value="opt-in"
      label="Send me news by email"
    />

    renders

    <div class="fune-field"><div class="fune-checkbox"><input class="fune-checkbox-input" id="c1" type="checkbox" name="marketing" value="opt-in"/><label class="fune-label fune-checkbox-label" for="c1">Send me news by email</label></div></div>
  3. controlled and ticked renders checked

    <Checkbox id="terms" label="I agree" checked />

    renders

    <div class="fune-field"><div class="fune-checkbox"><input class="fune-checkbox-input" id="terms" type="checkbox" name="terms" checked="" value="yes"/><label class="fune-label fune-checkbox-label" for="terms">I agree</label></div></div>
  4. controlled and clear renders no checked attribute

    <Checkbox id="terms" label="I agree" checked={false} />

    renders

    <div class="fune-field"><div class="fune-checkbox"><input class="fune-checkbox-input" id="terms" type="checkbox" name="terms" value="yes"/><label class="fune-label fune-checkbox-label" for="terms">I agree</label></div></div>
  5. defaultChecked renders checked too

    <Checkbox
      id="remember"
      label="Remember me on this device"
      defaultChecked
    />

    renders

    <div class="fune-field"><div class="fune-checkbox"><input class="fune-checkbox-input" id="remember" type="checkbox" name="remember" checked="" value="yes"/><label class="fune-label fune-checkbox-label" for="remember">Remember me on this device</label></div></div>
  6. a hint goes under the label and describes the checkbox

    <Checkbox
      id="remember"
      label="Remember me"
      hint="Do not tick this on a shared computer"
    />

    renders

    <div class="fune-field"><div class="fune-checkbox"><input class="fune-checkbox-input" id="remember" type="checkbox" aria-describedby="remember-hint" name="remember" value="yes"/><label class="fune-label fune-checkbox-label" for="remember">Remember me</label><div class="fune-hint fune-checkbox-hint" id="remember-hint">Do not tick this on a shared computer</div></div></div>
  7. an error goes above the box, marks it invalid and describes it

    <Checkbox
      id="terms"
      label="I agree to the terms"
      error="Agree to the terms to continue"
    />

    renders

    <div class="fune-field fune-field--error"><p class="fune-error-message" id="terms-error"><span class="fune-visually-hidden">Error:</span> Agree to the terms to continue</p><div class="fune-checkbox"><input class="fune-checkbox-input" id="terms" type="checkbox" aria-describedby="terms-error" aria-invalid="true" name="terms" value="yes"/><label class="fune-label fune-checkbox-label" for="terms">I agree to the terms</label></div></div>
  8. hint and error: described by both, hint first

    <Checkbox
      id="t"
      label="I agree"
      hint="You can read the terms first"
      error="Agree to the terms"
      checked={false}
    />

    renders

    <div class="fune-field fune-field--error"><p class="fune-error-message" id="t-error"><span class="fune-visually-hidden">Error:</span> Agree to the terms</p><div class="fune-checkbox"><input class="fune-checkbox-input" id="t" type="checkbox" aria-describedby="t-hint t-error" aria-invalid="true" name="t" value="yes"/><label class="fune-label fune-checkbox-label" for="t">I agree</label><div class="fune-hint fune-checkbox-hint" id="t-hint">You can read the terms first</div></div></div>
  9. required and disabled are boolean attributes

    <Checkbox id="t" label="I agree" required disabled />

    renders

    <div class="fune-field"><div class="fune-checkbox"><input class="fune-checkbox-input" id="t" type="checkbox" required="" disabled="" name="t" value="yes"/><label class="fune-label fune-checkbox-label" for="t">I agree</label></div></div>
  10. false booleans, an empty hint and a null error render nothing

    <Checkbox
      id="t"
      label="I agree"
      required={false}
      disabled={false}
      hint=""
      error={null}
      className="fune-field--compact"
    />

    renders

    <div class="fune-field fune-field--compact"><div class="fune-checkbox"><input class="fune-checkbox-input" id="t" type="checkbox" name="t" value="yes"/><label class="fune-label fune-checkbox-label" for="t">I agree</label></div></div>
Show the other 4 tests
  1. an error and a class name: the error class comes first

    <Checkbox
      id="t"
      label="I agree"
      error="Agree to continue"
      className="wide"
    />

    renders

    <div class="fune-field fune-field--error wide"><p class="fune-error-message" id="t-error"><span class="fune-visually-hidden">Error:</span> Agree to continue</p><div class="fune-checkbox"><input class="fune-checkbox-input" id="t" type="checkbox" aria-describedby="t-error" aria-invalid="true" name="t" value="yes"/><label class="fune-label fune-checkbox-label" for="t">I agree</label></div></div>
  2. label and value are escaped

    <Checkbox id="t" label="Terms & <conditions>" value="a\"b" />

    renders

    <div class="fune-field"><div class="fune-checkbox"><input class="fune-checkbox-input" id="t" type="checkbox" name="t" value="a&quot;b"/><label class="fune-label fune-checkbox-label" for="t">Terms &amp; &lt;conditions&gt;</label></div></div>
  3. an empty value is submitted as empty, not yes

    <Checkbox id="t" label="I agree" value="" />

    renders

    <div class="fune-field"><div class="fune-checkbox"><input class="fune-checkbox-input" id="t" type="checkbox" name="t" value=""/><label class="fune-label fune-checkbox-label" for="t">I agree</label></div></div>
  4. an id with a space is refused

    <Checkbox id="the terms" label="I agree" />

    error: a field id cannot contain spaces

More from the author

- **onChange** is called with `true` when the box is ticked and `false` when it is cleared, not with the event. - **checked** with **onChange** is a controlled checkbox; **defaultChecked** alone is an uncontrolled one, fine for a plain `<form>` post. Do not pass both. - **onBlur** is called, with nothing, when the box loses focus, so a form can mark the field touched (`form.state`'s blur action). - **value** is what the form posts when the box is ticked. It is `yes` when left out, rather than the browser's `on`, so a server reads `terms=yes`; nothing at all is posted when the box is clear. `""` is kept as `""`. - **id** is the checkbox's id and, unless **name** says otherwise, the name it is posted under. Left out, it is React's `useId()`; pass one whenever an error summary has to link to the box.

Why not `FormField`: its label comes before the control, and a checkbox's label goes after the box, so this component takes the same ids from `fieldIds` (`<id>-hint`, `<id>-error`, described hint first) and lays them out itself. Unlike a box in a `react.form.checkbox-group`, a lone checkbox is a control of its own, so the error describes the input and marks it `aria-invalid="true"`. The hint goes under the label, where GOV.UK puts an item hint.

For several related choices use `react.form.checkbox-group`; for a yes or no answer that must be given either way, use `react.form.radio-group` with Yes and No, since an unticked box cannot tell "no" from "did not answer".

It is a client component (it listens for changes), so the built file starts with `"use client"`.

Classes: `fune-field` (and `fune-field--error`), `fune-error-message`, `fune-visually-hidden`, `fune-checkbox` (the box and its label), `fune-checkbox-input`, `fune-label fune-checkbox-label`, `fune-hint fune-checkbox-hint`.

Sources: GOV.UK Design System, "Checkboxes" (toggling a single option, item hints) https://design-system.service.gov.uk/components/checkboxes/ and "Error message" https://design-system.service.gov.uk/components/error-message/.

## Notices

Portions derived from GOV.UK Frontend (https://github.com/alphagov/govuk-frontend), Copyright (c) 2017 Crown Copyright (Government Digital Service), under the MIT License; the full notice is in NOTICE.

1.0.1 adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE1,217
README.md2,896
impl/typescript.tsx2,207
vectors.json6,415