Skip to content
Noksha UIv0.1
Colour theme

forms

Checkbox

A real checkbox input with a drawn indicator, including the indeterminate state.

Examples

Basic

The three states a checkbox can be in โ€” unchecked, checked and indeterminate โ€” with nothing but native input attributes and CSS driving the look.

Bold styles

Fourteen looks built entirely from `className`, `containerClassName` and the `--cb-solid`/`--cb-ink` variables โ€” a gradient fill, neon, a diamond, a gradient ring, glass, and more, with no fork of the component underneath any of them.

Sizes

Three sizes on the same control scale the rest of the library uses.

Tones

The six semantic tones, each repointing the same `--cb-solid` and `--cb-ink` variables the box and check mark are built from.

Disabled

A disabled checkbox is still a real input โ€” `:disabled` drives the dimmed box and the cursor together, not just the colour.

With text

A label and a line of helper text, wired to the control through `Field.Description` rather than a hand-written `aria-describedby`.

New features and improvements, a few times a month.

Sign-ins from a new device or location.

Offers and other promotional email.

Group

Independent checkboxes with no shared parent โ€” each one owns its own boolean, and the count below is derived, not stored.

1 of 5 selected

Check all section

The classic three-state parent. Toggle a child and the parent goes half-checked on its own, since `indeterminate` is read straight off the selection.

Nested groups

Indeterminate composes: each group's own select-all checkbox rolls up into a master checkbox one level above it, with no special case for the extra level.

Selectable cards

The checkbox drives the whole card through `:has(:checked)` โ€” no click handler on the card itself, and no state beyond which id is selected.

Total: $12/month

Table row selection

The header checkbox is indeterminate when some but not all rows are selected, and drives every row through the same `checked`/`onCheckedChange` pair each row already uses on its own.

NameSize
invoice-0142.pdf128 KB
invoice-0143.pdf96 KB
invoice-0144.pdf212 KB
invoice-0145.pdf84 KB

Inline

Compact, `sm`-sized checkboxes in a wrapped row, for a filter bar rather than a form.

Custom checkbox

`className` restyles the box and `containerClassName` reaches the `--cb-solid` and `--cb-ink` variables directly โ€” both win over the tone class because caller classes are always the last ones merged.

Invalid

Field only marks the checkbox invalid after a first submit attempt, and `Field.Error` mounts nothing until then โ€” an always-present error paragraph gets announced as empty by some screen readers before the user has done anything wrong.

Controlled

`checked` and `onCheckedChange` are the entire contract โ€” the buttons below prove it by driving all four checkboxes from outside, with no internal state of their own to fight.

Native form

A real `<form>` submit, read with `FormData` โ€” every checked box posts its `value` under the shared `name`, and an unchecked one is simply absent. No JavaScript required to get there.

Own the source

Noksha ships as a package and as copy-paste source. Take the files and they are yours to change. These are read from the generated registry, so they are exactly what the library ships โ€” never a paraphrase of it.

Take these files, and field, internal helpers along with them โ€” the imports below point at them. Or let the CLI do it: npx @noksha-ui/cli add checkbox writes them, follows the same dependency graph, and fixes up the imports.

import { composeRefs, useIsomorphicLayoutEffect } from '@noksha-ui/core';
import * as React from 'react';
import { useFieldControl } from '../field/field.js';
import type { CheckboxProps } from './checkbox.types.js';
import {
  checkboxBoxVariants,
  checkboxInputVariants,
  checkboxMarkVariants,
  checkboxRootVariants,
} from './checkbox.variants.js';

const markProps = {
  viewBox: '0 0 16 16',
  fill: 'none',
  stroke: 'currentColor',
  strokeWidth: 2.5,
  strokeLinecap: 'round',
  strokeLinejoin: 'round',
} as const;

/**
 * A checkbox with an optional third, indeterminate state.
 *
 * ```tsx
 * <Checkbox name="terms" onCheckedChange={setAgreed} />
 * <Checkbox indeterminate checked={someSelected} />
 * ```
 */
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(
  {
    size = 'md',
    tone = 'accent',
    indeterminate = false,
    invalid,
    onCheckedChange,
    onChange,
    containerClassName,
    className,
    ...rest
  },
  forwardedRef,
) {
  const innerRef = React.useRef<HTMLInputElement>(null);

  const field = useFieldControl({
    id: rest.id,
    disabled: rest.disabled,
    required: rest.required,
    'aria-invalid': invalid || undefined,
    'aria-describedby': rest['aria-describedby'],
  });

  // `indeterminate` exists only as a DOM property, so it has to be written to
  // the node. Doing it in a layout effect means `:indeterminate` matches before
  // the first paint, and the dash never flashes in as a check.
  useIsomorphicLayoutEffect(() => {
    if (innerRef.current) innerRef.current.indeterminate = indeterminate;
  }, [indeterminate, rest.checked, rest.defaultChecked]);

  return (
    <span className={checkboxRootVariants({ tone, size, className: containerClassName })}>
      <input
        ref={composeRefs(forwardedRef, innerRef)}
        type="checkbox"
        {...rest}
        {...field}
        // The native property does not reach assistive tech on its own;
        // `aria-checked="mixed"` is what actually announces the third state.
        aria-checked={indeterminate ? 'mixed' : undefined}
        className={checkboxInputVariants()}
        onChange={(event) => {
          onCheckedChange?.(event.currentTarget.checked);
          onChange?.(event);
        }}
      />
      <span aria-hidden="true" className={checkboxBoxVariants({ className })} />
      <svg {...markProps} aria-hidden="true" className={checkboxMarkVariants({ mark: 'check' })}>
        <path d="M3.5 8.5 6.5 11.5 12.5 4.5" />
      </svg>
      <svg {...markProps} aria-hidden="true" className={checkboxMarkVariants({ mark: 'dash' })}>
        <path d="M4 8h8" />
      </svg>
    </span>
  );
});

Checkbox.displayName = 'Checkbox';

export { checkboxRootVariants as checkboxVariants };

API reference

Accepted values

CheckboxSize
smmdlg

CheckboxProps

Also accepts everything from Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'type'>.

PropTypeDescription
sizeCheckboxSizeโ€”
toneCheckboxToneโ€”
indeterminatebooleanThe third state: some but not all of the children are checked. It is a DOM property, not an attribute, so it cannot be expressed in JSX on a native input โ€” which is why it needs its own prop here.
invalidbooleanโ€”
onCheckedChange(checked: boolean) => voidConvenience over `onChange`, giving the boolean directly.
containerClassNamestringClasses for the outer wrapper; `className` styles the visible box.