shadimage
soralabs
uibase-checkboxsource: ok · hit

Base Checkbox

An accessible, stylable checkbox built on Base UI with a Motion-friendly animated indicator.

npx shadcn add "https://ui.soralabs.studio/r/base-checkbox.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
ui-primitives@base-ui/react
animationmotion

Registry dependencies

utils@soralabs/hooks-use-controlled-state

Files1 · source cached 10/8/2026, 5:57:32 PM

registry/ui/base/checkbox/index.tsx
"use client";

import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
import { useControlledState } from "@/hooks/use-controlled-state";
import { cn } from "@/lib/utils";
import {
  motion,
  useMotionValue,
  useReducedMotion,
  useTransform,
} from "motion/react";
import type * as React from "react";
import { useId } from "react";

interface CheckboxCheckIconProps
  extends Omit<React.ComponentProps<"svg">, "children"> {
  /** Whether the checkbox checkmark is active. */
  checked: boolean;
  /** Whether reduced motion is enabled. */
  prefersReducedMotion?: boolean | null;
}

/**
 * Checkmark SVG icon featuring animated path length draw with dynamic round cap transform.
 */
function CheckboxCheckIcon({
  checked,
  className,
  prefersReducedMotion,
  ...props
}: CheckboxCheckIconProps) {
  const pathLength = useMotionValue(checked ? 1 : 0);
  const strokeLinecap = useTransform(() =>
    pathLength.get() === 0 ? "none" : "round"
  );

  return (
    <svg
      aria-hidden="true"
      className={cn("size-3.5 stroke-current", className)}
      fill="none"
      stroke="currentColor"
      strokeWidth="3"
      viewBox="0 0 24 24"
      {...props}
    >
      <motion.path
        animate={{ pathLength: checked ? 1 : 0 }}
        d="M4 12L10 18L20 6"
        style={
          {
            pathLength,
            strokeLinecap,
          } as React.ComponentProps<typeof motion.path>["style"]
        }
        transition={
          prefersReducedMotion
            ? { duration: 0 }
            : {
                type: "spring",
                bounce: 0,
                duration: checked ? 0.3 : 0.1,
              }
        }
      />
    </svg>
  );
}

type CheckboxRootProps = React.ComponentProps<typeof CheckboxPrimitive.Root>;

/**
 * Primitive root button for the checkbox component, wrapping Base UI's Checkbox.Root with Motion interaction states.
 * Supports full Tailwind CSS class overriding via `cn(...)`.
 */
function CheckboxRoot({ className, render, ...props }: CheckboxRootProps) {
  const prefersReducedMotion = useReducedMotion();

  return (
    <CheckboxPrimitive.Root
      className={cn(
        "peer relative flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-[4px] border border-input outline-none transition-colors 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 group-has-[:focus-visible]/field-label:not-data-checked:border-input group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:bg-input/30 dark:data-checked:bg-primary dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
        className
      )}
      data-slot="checkbox"
      render={
        render ?? (
          <motion.button
            data-primary-action
            type="button"
            whileHover={prefersReducedMotion ? undefined : { scale: 1.05 }}
            whileTap={prefersReducedMotion ? undefined : { scale: 0.95 }}
          />
        )
      }
      {...props}
    />
  );
}

type CheckboxIndicatorProps = React.ComponentProps<
  typeof CheckboxPrimitive.Indicator
> & {
  /** Additional CSS classes for the indicator SVG */
  className?: string;
};

/**
 * Animated checkmark indicator for the checkbox using Motion path length animation.
 */
function CheckboxIndicator({ className, ...props }: CheckboxIndicatorProps) {
  const prefersReducedMotion = useReducedMotion();

  return (
    <CheckboxPrimitive.Indicator
      className={cn(
        "grid place-content-center text-current transition-none [&>svg]:size-3.5",
        className
      )}
      data-slot="checkbox-indicator"
      keepMounted
      render={(indicatorProps, state) => (
        <CheckboxCheckIcon
          checked={Boolean(state.checked)}
          className={className}
          prefersReducedMotion={prefersReducedMotion}
          {...indicatorProps}
        />
      )}
      {...props}
    />
  );
}

interface CheckboxProps extends Omit<CheckboxRootProps, "render"> {
  /**
   * Optional class name for the animated checkmark SVG indicator.
   */
  indicatorClassName?: string;
  /**
   * Optional label rendered beside the checkbox (wraps both in a `<label>`).
   */
  label?: React.ReactNode;
  /**
   * Optional class name for the outer label wrapper.
   */
  labelClassName?: string;
}

/**
 * An accessible, stylable animated Checkbox component built with Base UI and Motion for React.
 * Supports full Tailwind CSS class overrides just like shadcn/ui.
 */
function Checkbox({
  checked: controlledChecked,
  defaultChecked = false,
  onCheckedChange,
  label,
  labelClassName,
  indicatorClassName,
  className,
  id,
  ref,
  disabled,
  ...props
}: CheckboxProps) {
  const generatedId = useId();
  const inputId = id ?? generatedId;

  const [isChecked, setIsChecked] = useControlledState({
    defaultValue: defaultChecked,
    onChange: onCheckedChange,
    value: controlledChecked,
  });

  const prefersReducedMotion = useReducedMotion();

  const control = (
    <CheckboxPrimitive.Root
      checked={isChecked}
      className={cn(
        "peer relative flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-[4px] border border-input outline-none transition-colors 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 group-has-[:focus-visible]/field-label:not-data-checked:border-input group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:bg-input/30 dark:data-checked:bg-primary dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
        className
      )}
      data-slot="checkbox"
      disabled={disabled}
      id={inputId}
      onCheckedChange={setIsChecked}
      ref={ref}
      render={
        <motion.button
          data-primary-action
          type="button"
          whileHover={prefersReducedMotion ? undefined : { scale: 1.05 }}
          whileTap={prefersReducedMotion ? undefined : { scale: 0.95 }}
        >
          <CheckboxCheckIcon
            checked={isChecked}
            className={indicatorClassName}
            prefersReducedMotion={prefersReducedMotion}
          />
        </motion.button>
      }
      {...props}
    />
  );

  if (label == null) {
    return control;
  }

  return (
    <div className="inline-flex items-center gap-2.5">
      {control}
      <label
        className={cn(
          "cursor-pointer select-none font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
          disabled && "cursor-not-allowed opacity-50",
          labelClassName
        )}
        htmlFor={inputId}
      >
        {label}
      </label>
    </div>
  );
}

export type {
  CheckboxCheckIconProps,
  CheckboxIndicatorProps,
  CheckboxProps,
  CheckboxRootProps,
};
export {
  Checkbox,
  Checkbox as BaseCheckbox,
  Checkbox as default,
  CheckboxCheckIcon,
  CheckboxIndicator,
  CheckboxRoot,
};

More from soralabs

Browse all components in this library →