Aura Design System

Checkbox

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

Preview

Loading...
import { useState } from "react";
import {
  Checkbox,
  CheckboxGroup,
  CheckboxGroupItem,
} from "@/components/ui/Checkbox";

export function CheckboxDemo() {
  return {
  const [checked, setChecked] = useState(false);

  return (
    <div className="flex items-center gap-1">
      <Checkbox
        id="default"
        checked={checked}
        onCheckedChange={(checked) => setChecked(checked as boolean)}
      />
      <label
        htmlFor="default"
        className="text-sm font-medium leading-none cursor-pointer"
      >
        Accept terms and conditions
      </label>
    </div>
  );
};

Installation

Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces

pnpm dlx shadcn@latest add @aura/checkbox

Manual

Install the following dependencies:

pnpm install @radix-ui/react-icons radix-ui

Copy and paste the class names utility into your utils/class-names.ts file.

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Copy and paste the Checkbox component into your components/ui/Checkbox.tsx file.

"use client";
/**
 * @description A control that allows the user to toggle between checked and not checked.
 */
import * as React from "react";
import { Checkbox as CheckboxRadix } from "radix-ui";
import { CheckIcon } from "@radix-ui/react-icons";

import { cn } from "@/utils/class-names";

function Checkbox({
  className,
  ...props
}: React.ComponentProps<typeof CheckboxRadix.Root>) {
  return (
    <CheckboxRadix.Root
      data-slot="checkbox"
      className={cn(
        "border border-gray-a6 flex size-1.5 items-center justify-center rounded outline-none hover:bg-accent-2 cursor-pointer",
        className
      )}
      {...props}
    >
      <CheckboxIndicator />
    </CheckboxRadix.Root>
  );
}

function CheckboxIndicator({
  className,
  ...props
}: React.ComponentProps<typeof CheckboxRadix.Indicator>) {
  return (
    <CheckboxRadix.Indicator {...props}>
      <CheckIcon className={cn(className, "text-accent-9")} />
    </CheckboxRadix.Indicator>
  );
}

interface CheckboxGroupProps extends React.HTMLAttributes<HTMLDivElement> {
  value?: string[];
  onValueChange?: (value: string[]) => void;
  defaultValue?: string[];
}

function CheckboxGroup({
  className,
  value,
  onValueChange,
  defaultValue,
  children,
  ...props
}: CheckboxGroupProps) {
  const [internalValue, setInternalValue] = React.useState<string[]>(
    defaultValue ?? []
  );
  const controlledValue = value ?? internalValue;

  const handleValueChange = React.useCallback(
    (itemValue: string, checked: boolean) => {
      const newValue = checked
        ? [...controlledValue, itemValue]
        : controlledValue.filter((v) => v !== itemValue);

      if (onValueChange) {
        onValueChange(newValue);
      } else {
        setInternalValue(newValue);
      }
    },
    [controlledValue, onValueChange]
  );

  return (
    <div
      data-slot="checkbox-group"
      className={cn("flex flex-col gap-1", className)}
      {...props}
    >
      {React.Children.map(children, (child) => {
        if (
          React.isValidElement<CheckboxGroupItemProps>(child) &&
          child.type === CheckboxGroupItem
        ) {
          const childProps = child.props;
          return React.cloneElement(child, {
            ...childProps,
            checked: controlledValue.includes(childProps.value),
            onCheckedChange: (checked: boolean) =>
              handleValueChange(childProps.value, checked),
          });
        }
        return child;
      })}
    </div>
  );
}

interface CheckboxGroupItemProps
  extends Omit<React.ComponentProps<typeof CheckboxRadix.Root>, "onCheckedChange"> {
  value: string;
  label?: React.ReactNode;
  description?: React.ReactNode;
  onCheckedChange?: (checked: boolean) => void;
}

function CheckboxGroupItem({
  className,
  value,
  label,
  description,
  id,
  ...props
}: CheckboxGroupItemProps) {
  const checkboxId = id || `checkbox-${value}`;

  return (
    <div
      data-slot="checkbox-group-item"
      className={cn("flex items-start gap-1", className)}
    >
      <Checkbox id={checkboxId} {...props} />
      {(label || description) && (
        <div className="flex flex-col gap-1">
          {label && (
            <label
              htmlFor={checkboxId}
              className="text-sm font-medium leading-none cursor-pointer"
            >
              {label}
            </label>
          )}
          {description && (
            <p className="text-sm text-gray-a11 m-0">{description}</p>
          )}
        </div>
      )}
    </div>
  );
}

export { Checkbox, CheckboxIndicator, CheckboxGroup, CheckboxGroupItem };

Usage

Checked

export const Checked = () => (
  <div className="flex items-center gap-1">
    <Checkbox id="checked" defaultChecked />
    <label
      htmlFor="checked"
      className="text-sm font-medium leading-none cursor-pointer"
    >
      I agree to the privacy policy
    </label>
  </div>
)

Unchecked

export const Unchecked = () => (
  <div className="flex items-center gap-1">
    <Checkbox id="unchecked" />
    <label
      htmlFor="unchecked"
      className="text-sm font-medium leading-none cursor-pointer"
    >
      Subscribe to newsletter
    </label>
  </div>
)

Disabled

export const Disabled = () => (
  <div className="flex flex-col gap-1">
    <div className="flex items-center gap-1">
      <Checkbox id="disabled-unchecked" disabled />
      <label
        htmlFor="disabled-unchecked"
        className="text-sm font-medium leading-none text-gray-a8 cursor-not-allowed"
      >
        Disabled unchecked
      </label>
    </div>
    <div className="flex items-center gap-1">
      <Checkbox id="disabled-checked" disabled defaultChecked />
      <label
        htmlFor="disabled-checked"
        className="text-sm font-medium leading-none text-gray-a8 cursor-not-allowed"
      >
        Disabled checked
      </label>
    </div>
  </div>
)

WithDescription

export const WithDescription = () => {
  const [checked, setChecked] = useState(false);

  return (
    <div className="flex items-start gap-1">
      <Checkbox
        id="with-description"
        checked={checked}
        onCheckedChange={(checked) => setChecked(checked as boolean)}
      />
      <div className="flex flex-col gap-1">
        <label
          htmlFor="with-description"
          className="text-sm font-medium leading-none cursor-pointer"
        >
          Marketing emails
        </label>
        <p className="text-sm text-gray-a11 m-0">
          Receive emails about new products, features, and more.
        </p>
      </div>
    </div>
  );
};

Group

export const Group = () => {
  const [selectedItems, setSelectedItems] = useState<string[]>([]);

  const items = [
    {
      value: "react",
      label: "React",
      description: "A JavaScript library for building user interfaces",
    },
    {
      value: "typescript",
      label: "TypeScript",
      description: "JavaScript with syntax for types",
    },
    {
      value: "tailwind",
      label: "Tailwind CSS",
      description: "A utility-first CSS framework",
    },
    {
      value: "nextjs",
      label: "Next.js",
      description: "The React Framework for the Web",
    },
  ];

  return (
    <div className="flex flex-col gap-1">
      <div className="text-sm font-semibold">Select your tech stack:</div>
      <CheckboxGroup value={selectedItems} onValueChange={setSelectedItems}>
        {items.map((item) => (
          <CheckboxGroupItem
            key={item.value}
            value={item.value}
            label={item.label}
            description={item.description}
          />
        ))}
      </CheckboxGroup>
      {selectedItems.length > 0 && (
        <div className="mt-2 text-sm text-gray-a11">
          Selected: {selectedItems.length} item
          {selectedItems.length !== 1 ? "s" : ""}
        </div>
      )}
    </div>
  );
};

Indeterminate

export const Indeterminate = () => {
  const [selectedItems, setSelectedItems] = useState<string[]>(["sub-1"]);

  const allItems = ["sub-1", "sub-2", "sub-3"];
  const allChecked = allItems.every((item) => selectedItems.includes(item));
  const someChecked = selectedItems.length > 0 && !allChecked;

  const handleParentChange = (checked: boolean) => {
    setSelectedItems(checked ? allItems : []);
  };

  const handleChildChange = (itemId: string, checked: boolean) => {
    setSelectedItems((prev) =>
      checked ? [...prev, itemId] : prev.filter((id) => id !== itemId)
    );
  };

  return (
    <div className="flex flex-col gap-1.5">
      <div className="flex items-center gap-1">
        <Checkbox
          id="parent"
          checked={allChecked ? true : someChecked ? "indeterminate" : false}
          onCheckedChange={(checked) => handleParentChange(checked as boolean)}
        />
        <label
          htmlFor="parent"
          className="text-sm font-semibold leading-none cursor-pointer"
        >
          Select all notifications
        </label>
      </div>
      <div className="ml-1.5 flex flex-col gap-1 border-l-2 border-gray-a6 pl-1.5">
        {allItems.map((item, index) => (
          <div key={item} className="flex items-center gap-1">
            <Checkbox
              id={item}
              checked={selectedItems.includes(item)}
              onCheckedChange={(checked) =>
                handleChildChange(item, checked as boolean)
              }
            />
            <label
              htmlFor={item}
              className="text-sm font-medium leading-none cursor-pointer"
            >
              Notification {index + 1}
            </label>
          </div>
        ))}
      </div>
    </div>
  );
};