Checkbox

A control that allows the user to toggle between checked and not checked.

By clicking this checkbox, you agree to the terms.

"use client"

import * as React from "react"
import { cn } from "cn"
import { Checkbox as CheckboxPrimitive } from "@animate-ui/react/primitives/radix-ui"
import { AnimateUIProvider } from "@animate-ui/react/provider"

import { Tick02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldGroup,
  FieldLabel,
  FieldTitle,
} from "@/components/ui/field"
import { Label } from "@/components/ui/label"

function Checkbox({
  className,
  ...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
  return (
    <CheckboxPrimitive.Root
      data-slot="checkbox"
      className={cn(
        "border-input dark:bg-input/30 data-checked:text-primary-foreground aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-colors duration-300 group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input group-has-[:focus-visible]/field-label:data-checked:border-primary peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 before:absolute before:-inset-px before:rounded-[inherit] before:bg-primary before:opacity-0 before:scale-70 before:transition-[scale,opacity] before:duration-300 motion-reduce:before:scale-100 data-checked:before:opacity-100 data-checked:before:scale-100 disabled:cursor-not-allowed disabled:opacity-50",
        className
      )}
      {...props}
    >
      <CheckboxPrimitive.Indicator
        data-slot="checkbox-indicator"
        className="[&>svg]:size-3.5 relative grid place-content-center text-current transition-none"
      >
        <HugeiconsIcon icon={Tick02Icon} strokeWidth={2}
        />
      </CheckboxPrimitive.Indicator>
    </CheckboxPrimitive.Root>
  )
}

export { Checkbox }

export default function CheckboxDemo({
  checked: checkedProp = true,
  checkAnimation = "draw",
}: {
  checked?: boolean | "indeterminate"
  checkAnimation?: "draw" | "pop"
}) {
  const [checked, setChecked] = React.useState(checkedProp)
  const [prev, setPrev] = React.useState(checkedProp)
  if (checkedProp !== prev) {
    setPrev(checkedProp)
    setChecked(checkedProp)
  }

  return (
    <AnimateUIProvider checkAnimation={checkAnimation}>
      <FieldGroup className="max-w-sm">
        <Field orientation="horizontal">
          <Checkbox id="terms-checkbox" name="terms-checkbox" />
          <Label htmlFor="terms-checkbox">Accept terms and conditions</Label>
        </Field>
        <Field orientation="horizontal">
          <Checkbox
            id="terms-checkbox-2"
            name="terms-checkbox-2"
            checked={checked}
            onCheckedChange={setChecked}
          />
          <FieldContent>
            <FieldLabel htmlFor="terms-checkbox-2">
              Accept terms and conditions
            </FieldLabel>
            <FieldDescription>
              By clicking this checkbox, you agree to the terms.
            </FieldDescription>
          </FieldContent>
        </Field>
        <Field orientation="horizontal" data-disabled>
          <Checkbox id="toggle-checkbox" name="toggle-checkbox" disabled />
          <FieldLabel htmlFor="toggle-checkbox">Enable notifications</FieldLabel>
        </Field>
        <FieldLabel>
          <Field orientation="horizontal">
            <Checkbox id="toggle-checkbox-2" name="toggle-checkbox-2" />
            <FieldContent>
              <FieldTitle>Enable notifications</FieldTitle>
              <FieldDescription>
                You can enable or disable notifications at any time.
              </FieldDescription>
            </FieldContent>
          </Field>
        </FieldLabel>
      </FieldGroup>
    </AnimateUIProvider>
  )
}

Installation

Install the packages

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Add the shadcn/ui component

npx shadcn@latest add checkbox
pnpm dlx shadcn@latest add checkbox
yarn dlx shadcn@latest add checkbox
bun x shadcn@latest add checkbox

Animate it

The CLI finds your shadcn/ui components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.

npx @animate-ui/cli@latest animate checkbox
pnpm dlx @animate-ui/cli@latest animate checkbox
yarn dlx @animate-ui/cli@latest animate checkbox
bun x @animate-ui/cli@latest animate checkbox

Usage

Import the primitive from Animate UI instead of Radix UI. Everything else stays the same.

import { Checkbox } from '@animate-ui/react/primitives/radix-ui';

export function Terms() {
  return (
    <label className="flex items-center gap-2">
      <Checkbox.Root defaultChecked>
        <Checkbox.Indicator>
          <CheckIcon />
        </Checkbox.Indicator>
      </Checkbox.Root>
      Accept terms and conditions
    </label>
  );
}

Coming from the @radix-ui/react-checkbox package? Use the subpath: import * as CheckboxPrimitive from '@animate-ui/react/primitives/radix-ui/checkbox'.

Check animation

The check draws itself by default. With animation="pop" on the Indicator, it pops in from half its size instead, and shrinks back out:

<Checkbox.Root defaultChecked>
  <Checkbox.Indicator animation="pop">
    <CheckIcon />
  </Checkbox.Indicator>
</Checkbox.Root>

The same demo with checkAnimation="pop" (its Controls switch it back to draw):

By clicking this checkbox, you agree to the terms.

To pop every check of your app at once (checkboxes, menus, selects, comboboxes), set checkAnimation="pop" on an AnimateUIProvider instead.

Animation

The motion of Animate UI v1's checkbox:

  • Indicator: the check draws itself when the box is checked. Its stroke (pathLength) and its opacity go from 0 to 1 together in 0.3s, after a 0.2s delay, on Motion's default ease-out (v1's delay and curve, drawn a little slower than v1's 0.2s). Unchecking erases it in 0.3s, without the delay, then the indicator unmounts, as in Radix. The indicator element itself never moves: its content animates, so your layout and classes stay as they are. Content that isn't an SVG shape fades the same way.
  • With animation="pop" (or checkAnimation="pop" on an AnimateUIProvider), the check pops instead: after the same 0.2s, it springs in from half its size out of a 2px blur (stiffness 500, damping 24, a little past its size), and shrinks back into its blur in 0.15s when the box is unchecked. Its stroke stays whole. The demo's Controls switch between the two.
  • Root doesn't move: unlike v1's, it doesn't scale when pressed or hovered. The check (and, in the demo, the box's fill) answers the click.
  • In the demo, the box's fill is a layer of its own (before: in its classes) that grows from 70% while it fades in, in 300ms, and shrinks back as it fades out: the check draws over a box that is nearly full. The layer covers the box's border (before:-inset-px) and the border keeps its color, so no outline rings the growing fill. It's CSS, so copy the classes for the same effect; under reduced motion the layer only fades.
  • A box that is already checked when it appears (the page loading, a dialog opening) shows its check at rest, unlike v1's: only checking and unchecking animate it. On a server-rendered page, the check is there before any script runs.
  • Checking with Space animates the same way: the state changes at once, the animation never delays it.
  • Toggling again mid-animation turns back from where the check is.
  • indeterminate shows the indicator like checked. An icon swapped for the indeterminate state draws without the delay, as v1's indeterminate line did.
  • Under reduced motion, the check only fades, drawn or popped (its stroke stays whole, nothing scales).
  • With forceMount, the unchecked indicator stays in the DOM with its check hidden.
  • The box's own colours are the component's CSS: shadcn/ui's transition-colors fades them.

API Reference

Every part takes exactly the props of Radix UI's Checkbox. Animate UI adds the animation props below. With asChild, a part renders your element with Radix's behaviour, without the animation.

Root

Radix UI props only. It isn't animated.

Indicator

Radix UI props, plus:

PropTypeDefault
animation?
'draw' | 'pop'the closest AnimateUIProvider's checkAnimation, else 'draw'
transition?
Transitiondraw: { duration: 0.3, delay: 0.2, ease: [0, 0, 0.58, 1] }; pop: { type: 'spring', stiffness: 500, damping: 24, delay: 0.2 }
exitTransition?
Transitiondraw: { duration: 0.3, ease: [0, 0, 0.58, 1] }; pop: { duration: 0.15, ease: [0.23, 1, 0.32, 1] }

AnimateUIProvider's tempo scales these transitions with everything else.

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page