Checkbox

PreviousNext

A customizable checkbox component

Docs
pureuiui

Preview

Loading preview…
registry/pure-ui/ui/checkbox/index.tsx
"use client";

import React, { createContext, useContext, useEffect, useState } from "react";
import { Checkbox as CheckboxPrimitive } from "@base-ui/react";
import { tv, type VariantProps } from "tailwind-variants";

import { cn } from "@/lib/classes";

const checkboxRootStyles = tv({
  base: [
    `group size-7 relative inline-flex items-center justify-center shrink-0 overflow-hidden outline-hidden focus:outline-hidden focus-visible:outline-hidden cursor-pointer focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring focus-visible:ring-offset-background`,
    `data-disabled:cursor-not-allowed data-disabled:grayscale data-disabled:scale-100 data-disabled:opacity-50`,
    `before:content-[''] before:absolute before:border-2 before:inset-0 before:border-border not-data-disabled:hover:before:bg-secondary/60`,
    `after:content-[''] after:absolute after:inset-0 after:bg-primary data-unchecked:after:scale-50 data-unchecked:after:opacity-0 after:origin-center data-checked:after:scale-100 data-checked:after:opacity-100`,
    ``,
  ],
  variants: {
    size: {
      sm: "size-4.5",
      default: "size-5",
      lg: "size-6",
    },
    radius: {
      none: "rounded-none before:rounded-none after:rounded-none",
      sm: "rounded-[6px] before:rounded-[6px] after:rounded-[6px]",
      default: "rounded-[7.2px] before:rounded-[7.2px] after:rounded-[7.2px]",
      lg: "rounded-[8.4px] before:rounded-[8.4px] after:rounded-[8.4px]",
      full: "rounded-full before:rounded-full after:rounded-full",
    },
    reduceMotion: {
      true: "transition-none before:transition-none after:transition-none",
      false:
        "before:transition-colors transition-transform after:[transition:0.2s_linear] after:[transition-property:opacity,scale,transform]",
    },
  },
  defaultVariants: {
    size: "default",
    radius: "default",
    reduceMotion: false,
  },
});

const checkboxIndicatorStyles = tv({
  base: [
    `relative z-10 flex items-center justify-center data-unchecked:opacity-0 data-checked:opacity-100 text-primary-foreground transition-opacity pointer-events-none`,
  ],
  variants: {
    size: {
      sm: "w-2.5 h-2.5",
      default: "w-3 h-3",
      lg: "w-4 h-4",
    },
    reduceMotion: {
      true: "transition-none",
      false: "transition-opacity",
    },
  },
  defaultVariants: {
    size: "default",
    reduceMotion: false,
  },
});

interface CheckboxRootProps
  extends React.ComponentProps<typeof CheckboxPrimitive.Root>,
    VariantProps<typeof checkboxRootStyles> {
  reduceMotion?: boolean;
}

interface CheckboxContextType {
  checked: boolean;
  onCheckedChange: CheckboxRootProps["onCheckedChange"];
  indeterminate: boolean | undefined;
  size: VariantProps<typeof checkboxRootStyles>["size"];
  reduceMotion?: boolean; // Add this
}

const CheckboxContext = createContext<CheckboxContextType | undefined>(
  undefined
);

const useCheckbox = () => {
  const context = useContext(CheckboxContext);
  if (!context) {
    throw new Error("useCheckbox must be used within a CheckboxProvider");
  }
  return context;
};

function CheckboxRoot({
  checked,
  defaultChecked,
  onCheckedChange,
  indeterminate,
  className,
  reduceMotion = false,
  size = "default",
  radius = "default",
  ...rest
}: CheckboxRootProps) {
  const [isChecked, setIsChecked] = useState(
    checked ?? defaultChecked ?? false
  );

  useEffect(() => {
    if (checked !== undefined) setIsChecked(checked);
  }, [checked]);

  const handleCheckedChange: CheckboxRootProps["onCheckedChange"] = (
    checked,
    eventDetails
  ) => {
    if (indeterminate && !onCheckedChange) {
      return;
    }

    setIsChecked(checked);
    onCheckedChange?.(checked, eventDetails);
  };

  return (
    <CheckboxContext.Provider
      value={{
        checked: isChecked,
        onCheckedChange: handleCheckedChange,
        indeterminate: indeterminate,
        size: size,
        reduceMotion: reduceMotion,
      }}
    >
      <CheckboxPrimitive.Root
        checked={isChecked}
        onCheckedChange={handleCheckedChange}
        indeterminate={indeterminate}
        className={cn(
          checkboxRootStyles({ size, radius, reduceMotion }),
          className
        )}
        {...rest}
      />
    </CheckboxContext.Provider>
  );
}

interface CheckboxIconProps extends React.ComponentProps<"svg"> {
  checked: boolean;
  indeterminate: boolean | undefined;
  reduceMotion?: boolean;
}

function CheckboxIcon(props: CheckboxIconProps) {
  const { checked, indeterminate, reduceMotion, ...rest } = props;

  if (indeterminate) {
    return (
      <svg stroke="currentColor" strokeWidth={3} viewBox="0 0 24 24" {...rest}>
        <line x1="21" x2="3" y1="12" y2="12" />
      </svg>
    );
  }

  return (
    <svg
      aria-hidden="true"
      fill="none"
      role="presentation"
      stroke="currentColor"
      strokeDasharray={22}
      strokeDashoffset={checked ? 44 : 66}
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth={2}
      style={{
        transition: reduceMotion ? "none" : `stroke-dashoffset 250ms linear`,
        transitionDelay: reduceMotion ? "0ms" : `200ms`,
      }}
      viewBox="0 0 17 18"
      {...rest}
    >
      <polyline points="1 9 7 14 15 4" />
    </svg>
  );
}

interface CheckboxIndicatorProps
  extends React.ComponentProps<typeof CheckboxPrimitive.Indicator> {
  icon?: React.ReactNode | ((props: CheckboxIconProps) => React.ReactNode);
}

function CheckboxIndicator({
  className,
  icon = CheckboxIcon,
  children,
  ...rest
}: CheckboxIndicatorProps) {
  const { checked, indeterminate, size, reduceMotion = false } = useCheckbox();

  return (
    <CheckboxPrimitive.Indicator
      data-slot="checkbox-indicator"
      className={cn(checkboxIndicatorStyles({ size, reduceMotion }), className)}
      {...rest}
      keepMounted
    >
      {children ?? (
        <CheckboxIcon
          checked={checked}
          indeterminate={indeterminate}
          reduceMotion={reduceMotion}
        />
      )}
    </CheckboxPrimitive.Indicator>
  );
}

interface CheckboxProps
  extends React.ComponentProps<typeof CheckboxPrimitive.Root>,
    VariantProps<typeof checkboxRootStyles> {
  reduceMotion?: boolean;
}

function Checkbox({
  className,
  reduceMotion,
  children,
  ...rest
}: CheckboxProps) {
  return (
    <CheckboxRoot
      className={cn(className)}
      reduceMotion={reduceMotion}
      {...rest}
    >
      <CheckboxIndicator>{children}</CheckboxIndicator>
    </CheckboxRoot>
  );
}

export { Checkbox, CheckboxRoot, CheckboxIndicator };

Installation

npx shadcn@latest add @pureui/checkbox

Usage

import { Checkbox } from "@/components/ui/checkbox"
<Checkbox />