Fiber UI LogoFiberUI

Checkbox

An accessible checkbox component for forms and selection lists. Supports indeterminate states, custom styling, and keyboard navigation, built on top of React Aria primitives.

Basic Usage

By clicking this checkbox, you agree to the terms and conditions.

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

export const Example1 = () => {
    return (
        <div className="flex flex-col gap-6">
            <div className="flex items-center gap-3">
                <Checkbox id="terms" />
                <Label htmlFor="terms">Accept terms and conditions</Label>
            </div>
            <div className="flex items-start gap-3">
                <Checkbox id="terms-2" defaultSelected />
                <div className="space-y-2">
                    <Label htmlFor="terms-2">Accept terms and conditions</Label>
                    <p className="text-muted-foreground text-sm">
                        By clicking this checkbox, you agree to the terms and
                        conditions.
                    </p>
                </div>
            </div>
            <div className="group-[data] flex items-start gap-3">
                <Checkbox id="toggle" isDisabled />
                <Label htmlFor="toggle">Enable notifications</Label>
            </div>
            <Label
                htmlFor="toggle-2"
                className="hover:bg-accent/50 has-aria-checked:border-primary has-aria-checked:bg-primary/20 dark:has-aria-checked:border-primary flex items-start gap-3 rounded-lg border p-3"
            >
                <Checkbox id="toggle-2" defaultSelected className="" />
                <div className="grid gap-1.5 font-normal">
                    <p className="text-sm font-medium leading-none">
                        Enable notifications
                    </p>
                    <p className="text-muted-foreground text-sm">
                        You can enable or disable notifications at any time.
                    </p>
                </div>
            </Label>
        </div>
    );
};

Controlled

Controlled checkbox

Checked: No

"use client";

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

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

    return (
        <div className="space-x-2">
            <div className="flex items-center gap-2">
                <Checkbox isSelected={checked} onChange={setChecked} />
                Controlled checkbox
            </div>

            <p>Checked: {checked ? "Yes" : "No"}</p>
        </div>
    );
};

Disabled

Disabled unchecked
Disabled checked
import { Checkbox } from "@repo/ui/components/checkbox";

export const Example3 = () => {
    return (
        <div className="flex flex-col space-y-2">
            <div className="flex items-center gap-2">
                <Checkbox isDisabled />
                Disabled unchecked
            </div>

            <div className="flex items-center gap-2">
                <Checkbox isDisabled isSelected />
                Disabled checked
            </div>
        </div>
    );
};

Indeterminate

Indeterminate checkbox
"use client";

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

export const Example4 = () => {
    const [selected, setSelected] = useState<boolean | "indeterminate">(
        "indeterminate",
    );

    return (
        <div className="space-y-4">
            <div className="flex items-center gap-2">
                <Checkbox
                    isIndeterminate={selected === "indeterminate"}
                    isSelected={selected === true}
                    onChange={(isSelected) => setSelected(isSelected)}
                />
                Indeterminate checkbox
            </div>

            <div className="flex gap-2">
                <Button
                    onPress={() => setSelected("indeterminate")}
                    variant="outline"
                >
                    Set Indeterminate
                </Button>
                <Button onPress={() => setSelected(true)} variant="outline">
                    Set Checked
                </Button>
                <Button onPress={() => setSelected(false)} variant="outline">
                    Set Unchecked
                </Button>
            </div>
        </div>
    );
};

Component Code

"use client";

import { Check, Minus } from "lucide-react";
import {
    Checkbox as AriaCheckbox,
    CheckboxProps as AriaCheckboxProps,
    composeRenderProps,
} from "react-aria-components";
import { tv, type VariantProps, cn } from "tailwind-variants";

const checkboxVariants = tv({
    base: "ring-offset-background group-data-focus-visible:ring-ring border-primary group-data-focus-visible:outline-none group-data-focus-visible:ring-2 group-data-focus-visible:ring-offset-2 flex h-5 w-5 shrink-0 items-center justify-center overflow-hidden rounded-md border",
    variants: {
        size: {
            default: "h-5 w-5",
            sm: "h-4 w-4",
            lg: "h-6 w-6",
        },
    },
    defaultVariants: {
        size: "default",
    },
});

const checkboxIconVariants = tv({
    base: "animate-in fade-in zoom-in transition-all duration-200 ease-out",
    variants: {
        size: {
            default: "h-4 w-4",
            sm: "h-3 w-3",
            lg: "h-5 w-5",
        },
    },
    defaultVariants: {
        size: "default",
    },
});

export interface CheckboxProps
    extends Omit<AriaCheckboxProps, "children">,
        VariantProps<typeof checkboxVariants> {
    className?: string;
}

export const Checkbox: React.FC<CheckboxProps> = (props) => {
    const { className, size, ...restProps } = props;
    return (
        <AriaCheckbox
            {...restProps}
            className={composeRenderProps(
                className,
                (className, { isDisabled }) =>
                    cn(
                        "group flex cursor-pointer items-center gap-2 text-sm [-webkit-tap-highlight-color:transparent]",
                        isDisabled && "cursor-not-allowed opacity-50",
                        className,
                    ) || "",
            )}
        >
            {({
                isHovered,
                isPressed,
                isDisabled,
                isSelected,
                isIndeterminate,
            }) => (
                <span
                    className={cn(
                        checkboxVariants({ size }),
                        (isSelected || isIndeterminate) &&
                            "bg-primary text-primary-foreground",
                    )}
                    data-hovered={isHovered}
                    data-pressed={isPressed}
                    data-disabled={isDisabled}
                    data-state={isSelected ? "checked" : null}
                    aria-checked={isSelected}
                    aria-hidden="true"
                >
                    {isIndeterminate ? (
                        <Minus className={checkboxIconVariants({ size })} />
                    ) : isSelected ? (
                        <Check className={checkboxIconVariants({ size })} />
                    ) : null}
                </span>
            )}
        </AriaCheckbox>
    );
};

Checkbox.displayName = "Checkbox";