For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to any URL or sending an Accept: text/markdown header. An agent skill is available at /.well-known/agent-skills/site-skill.md.
1
Support

Checkbox

A shadcn-based Checkbox with fluid indicator transitions, mixed state, and accessible form semantics.

Playground

Live preview
Click the label or press Space to toggle. Code captures configuration, not transient clicks. Initially checked remounts the checkbox; Reset restores configuration and interaction state.
Customize Checkbox
Customize
checkbox-demo.tsx
"use client";

import { Checkbox } from "@/components/ui/checkbox";

export function CheckboxDemo() {
  return (
    <label className="flex items-center gap-3 text-sm">
      <Checkbox animated={true} aria-invalid={false} aria-label={"Enable notifications"} defaultChecked={false} disabled={false} indeterminate={false} readOnly={false} />
      <span>{"Enable notifications"}</span>
    </label>
  );
}

Installation

Initialize shadcn in your React project, then install this component from the Vandor UI registry:

$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/checkbox.json

Dependencies

Installed automatically by the registry CLI. For manual setup:

Dependencies3 required packages
  • @base-ui/react@^1.8.0
  • cn
  • motion@^12.38.0
$ pnpm add @base-ui/react@^1.8.0 cn motion@^12.38.0

Usage

"use client";
 
import { Checkbox } from "@/components/ui/checkbox";
 
export function Demo() {
  return (
    <label className="flex items-center gap-3 text-sm">
      <Checkbox defaultChecked />
      Enable notifications
    </label>
  );
}

Use your configured UI alias if it differs. Requires React 19, Tailwind CSS 4 with shadcn theme tokens (input, primary, primary-foreground, ring, and destructive), Base UI 1.8+, and Motion 12.38+. No website-specific CSS, providers, or icon package is needed. The upstream MIT notice is included in the source. For manual installation, install the packages listed above and copy the complete Source into your configured UI directory, preserving the notice.

Examples

Basic

Wrap the control and text in a label to make the whole row clickable. defaultChecked sets the initial uncontrolled state; changing it does not update an existing checkbox. The playground remounts the control when this configuration changes.

checkbox-demo.tsx
"use client";
/* eslint-disable jsx-a11y/label-has-associated-control -- Checkbox renders a native input inside the enclosing label. */

import { Checkbox } from "@/components/ui/checkbox";

export const CheckboxDemo = () => (
    <label className="flex items-center gap-3 text-sm">
      <Checkbox defaultChecked />
      Enable notifications
    </label>
  );

States and validation

Disabled controls cannot change and are omitted from submission. Read-only controls retain focusability and their submitted value. Connect errors or helper text through aria-describedby; aria-invalid exposes invalid semantics and styling but does not implement validation itself.

Notification preferences

Accept the terms before continuing.

checkbox-states.tsx
"use client";
/* eslint-disable jsx-a11y/label-has-associated-control -- Checkbox renders a native input inside the enclosing label. */

import { Checkbox } from "@/components/ui/checkbox";

export const CheckboxStates = () => (
    <fieldset className="flex flex-col gap-4">
      <legend className="mb-4 text-sm font-medium">
        Notification preferences
      </legend>
      <label className="flex items-center gap-3 text-sm">
        <Checkbox disabled defaultChecked />
        Disabled
      </label>
      <label className="flex items-center gap-3 text-sm">
        <Checkbox readOnly defaultChecked />
        Read only
      </label>
      <label className="flex items-center gap-3 text-sm">
        <Checkbox aria-invalid aria-describedby="checkbox-error" />
        Accept terms
      </label>
      <p id="checkbox-error" className="text-sm text-destructive">
        Accept the terms before continuing.
      </p>
    </fieldset>
  );

Controlled selection and indeterminate state

Use checked with onCheckedChange for controlled state. The callback receives a boolean and Base UI event details immediately, before any animation finishes. details.cancel() rejects an update. indeterminate is independent of checked: a mixed state displays a line and announces aria-checked="mixed". Manage both values yourself, as in this select-all example.

Notification channels1 of 3 selected
checkbox-group.tsx
"use client";
/* eslint-disable jsx-a11y/label-has-associated-control -- Checkbox renders a native input inside the enclosing label. */

import { useState } from "react";

import { Checkbox } from "@/components/ui/checkbox";

const channels = ["Email", "Push", "SMS"];

export const CheckboxGroup = () => {
  const [selected, setSelected] = useState<string[]>(["Email"]);
  return (
    <fieldset className="flex flex-col gap-4">
      <legend className="mb-4 text-sm font-medium">
        Notification channels
      </legend>
      <label className="flex items-center gap-3 text-sm font-medium">
        <Checkbox
          checked={selected.length === channels.length}
          indeterminate={
            selected.length > 0 && selected.length < channels.length
          }
          onCheckedChange={(checked) => setSelected(checked ? channels : [])}
        />
        Select all
      </label>
      {channels.map((channel) => (
        <label key={channel} className="flex items-center gap-3 text-sm">
          <Checkbox
            checked={selected.includes(channel)}
            onCheckedChange={(checked) =>
              setSelected((current) =>
                checked
                  ? [...current, channel]
                  : current.filter((item) => item !== channel)
              )
            }
          />
          {channel}
        </label>
      ))}
      <output className="text-xs text-muted-foreground">
        {selected.length} of {channels.length} selected
      </output>
    </fieldset>
  );
};

Native form submission and reset

Set name and value to include a checked checkbox in FormData. The default checked value is "on"; unchecked inputs are omitted unless uncheckedValue is provided. Base UI does not synchronize uncontrolled visual state with native form reset. When reset is needed, use controlled state and restore it in the form's onReset, as shown here. Use required for native constraint validation; use form to associate an external control with a form.

Submit to inspect the form value.
checkbox-form.tsx
"use client";
/* eslint-disable jsx-a11y/label-has-associated-control -- Checkbox renders a native input inside the enclosing label. */

import { useState } from "react";

import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";

export const CheckboxForm = () => {
  const [checked, setChecked] = useState(true);
  const [result, setResult] = useState("Submit to inspect the form value.");
  return (
    <form
      className="flex flex-col gap-4"
      onSubmit={(event) => {
        event.preventDefault();
        setResult(
          `updates: ${new FormData(event.currentTarget).get("updates") ?? "omitted"}`
        );
      }}
      onReset={() => {
        setChecked(true);
        setResult("Submit to inspect the form value.");
      }}
    >
      <label className="flex items-center gap-3 text-sm">
        <Checkbox
          name="updates"
          value="weekly"
          checked={checked}
          onCheckedChange={setChecked}
        />
        Weekly updates
      </label>
      <div className="flex gap-2">
        <Button type="submit" size="sm">
          Submit
        </Button>
        <Button type="reset" variant="outline" size="sm">
          Reset
        </Button>
      </div>
      <output className="text-xs text-muted-foreground" aria-live="polite">
        {result}
      </output>
    </form>
  );
};

Behavior & Accessibility

  • Base UI preserves checkbox keyboard behavior: Tab focuses and Space toggles. Enter is not the toggle key.
  • The default visible root is a span with checkbox semantics plus a hidden native input. Use an enclosing label or Base UI Field for naming. For a sibling htmlFor label, use render={<button />} and nativeButton as described in the Base UI documentation.
  • ref targets the visible root; inputRef targets the native input. Base UI root props, event handlers, state-aware className/style, and composition are forwarded. Children are reserved for the built-in indicator.
  • The check mark draws along its stroke, from the short arm into the long arm, over 220 ms at a steady pace. Unchecking draws it back over 140 ms, hiding it only after withdrawal finishes. Mixed-state morphing uses a short 180 ms ease-out transition. There is no scale, bounce, fade-in, or delayed state update. Reduced-motion preferences make indicator transitions immediate and disable CSS color transitions. animated={false} disables indicator motion explicitly; color transitions remain unless reduced motion is preferred.
  • Light and dark styles inherit your shadcn tokens. A custom className merges with defaults. Do not remove accessible focus treatment without a replacement.

Storybook Stories (Optional)

Install Checkbox first, then add portable stories to your existing React Storybook:

$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/checkbox-stories.json

This installs only checkbox.stories.tsx beside Checkbox in your configured UI directory, without reinstalling Checkbox, adding packages, or editing .storybook/. Review overwrite prompts for existing stories. Supply @storybook/react matching your installed Storybook version and React; stories are typechecked and composed against Storybook React 10.6.1. No extra example dependencies are required.

Include ../components/ui/**/*.stories.@(js|jsx|mjs|ts|tsx) in your existing .storybook/main.ts stories patterns, adjusting for your UI directory. Import your application's Tailwind/global stylesheet in .storybook/preview.ts; include the UI/stories directory in Tailwind source scanning. Stories inherit your theme and decorators. Apply your dark-theme selector inside the preview iframe, not only to the Storybook manager. The autodocs tag needs your existing @storybook/addon-docs configuration; Canvas and Controls do not.

Props

Component props, types, defaults, and descriptions
PropType / DescriptionDefault
animatedboolean

Draws and withdraws the check stroke, with mixed-state morphing. Reduced motion makes these immediate.

true
aria-invalidboolean | 'true' | 'false'

Invalid styling and semantics. Associate error text using aria-describedby.

Not set
checkedboolean

Controlled checked state. Pair with onCheckedChange.

Not set
classNamestring | ((state: Checkbox.Root.State) => string)

Classes merged with defaults, optionally derived from Base UI checkbox state.

Not set
defaultCheckedboolean

Initial uncontrolled checked state. Use checked for controlled state.

false
disabledboolean

Prevents interaction and excludes the input from form submission.

false
indeterminateboolean

Displays a mixed state independently of checked. Manage this prop when selecting a group.

false
inputRefRef<HTMLInputElement>

Ref to the native input. The ordinary ref targets the visible root.

Not set
namestring

Name of the hidden native form input.

Not set
nativeButtonboolean

Set true when render supplies a native button.

false
onCheckedChange(checked: boolean, details: Checkbox.Root.ChangeEventDetails) => void

Called immediately with the next boolean and Base UI event details. Call details.cancel() to reject a change.

Not set
readOnlyboolean

Prevents changes while preserving focus and submitted value.

false
renderReactElement | ComponentRenderFn

Base UI root composition. For sibling labels, use render={<button />} and nativeButton.

Not set (span)
requiredboolean

Requires a checked input for native form validation.

false
uncheckedValuestring

Optional submitted value when unchecked. Otherwise unchecked inputs are omitted.

Not set
valuestring

Submitted value when checked.

"on" (form submission)

The reference lists component-specific props and selected native props, not the complete React API. Other native attributes are forwarded to the rendered element.

Source

"use client";
 
// Adapted from shadcn/ui's Base Nova Checkbox (MIT).
// https://ui.shadcn.com/docs/components/base/checkbox
/*
MIT License
Copyright (c) 2023 shadcn
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
import { cn } from "cn";
import { motion, useReducedMotion } from "motion/react";
import type { ComponentProps } from "react";
 
export type CheckboxProps = Omit<
  ComponentProps<typeof CheckboxPrimitive.Root>,
  "children"
> & {
  /** Animate indicator changes. Reduced-motion preferences take precedence. */
  animated?: boolean;
};
 
export const Checkbox = ({
  className,
  animated = true,
  ...props
}: CheckboxProps) => {
  const reducedMotion = useReducedMotion();
  const shouldAnimate = animated && !reducedMotion;
 
  return (
    <CheckboxPrimitive.Root
      {...props}
      data-slot="checkbox"
      className={(state) =>
        cn(
          "peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input outline-none transition-colors duration-150 motion-reduce:transition-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-disabled:cursor-not-allowed data-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground data-indeterminate:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground dark:bg-input/30 dark:data-checked:bg-primary dark:data-indeterminate:bg-primary",
          typeof className === "function" ? className(state) : className
        )
      }
    >
      <CheckboxPrimitive.Indicator
        keepMounted
        data-slot="checkbox-indicator"
        render={(indicatorProps, state) => {
          const visible = state.checked || state.indeterminate;
          const strokeDuration = visible ? 0.22 : 0.14;
          return (
            <span
              {...indicatorProps}
              aria-hidden="true"
              className="grid place-content-center text-current"
            >
              <span className="grid place-content-center">
                <svg
                  aria-hidden="true"
                  className="size-3.5"
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="3"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                >
                  <motion.path
                    initial={false}
                    animate={{
                      d: state.indeterminate
                        ? "M5 12 L12 12 L19 12"
                        : "M5 12 L10 17 L19 7",
                      opacity: visible ? 1 : 0,
                      pathLength: visible ? 1 : 0,
                    }}
                    transition={{
                      d: {
                        duration: shouldAnimate ? 0.18 : 0,
                        ease: [0.22, 1, 0.36, 1],
                      },
                      // Keep the stroke visible during withdrawal. Hide the
                      // round-cap dot only after the path has drawn back to zero.
                      opacity: {
                        delay: shouldAnimate && !visible ? 0.14 : 0,
                        duration: 0,
                      },
                      pathLength: {
                        duration: shouldAnimate ? strokeDuration : 0,
                        ease: "linear",
                      },
                    }}
                  />
                </svg>
              </span>
            </span>
          );
        }}
      />
    </CheckboxPrimitive.Root>
  );
};
 

Credits

  • shadcn/ui: Adapted Base Nova Checkbox foundation. Vandor UI adds Motion stroke drawing and withdrawal, mixed-state morphing, and reduced-motion support.