Skip to content
goodtabs UI
BrowseBrowse components

Checkbox card

A selectable card with a checkbox, title, description and meta line.

npx shadcn@latest add https://ui.goodtabs.co/r/checkbox-card.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/checkbox-card

Three cards in a group, one checked. Click anywhere on a card to toggle it.

Single

One card on its own, for a single opt in.

Disabled

A disabled card fades and cannot be toggled.

Usage

import { CheckboxCard, CheckboxCardGroup } from "@/components/ui/checkbox-card"

<CheckboxCardGroup aria-label="Add-ons" columns={2}>
  <CheckboxCard title="Priority support" description="Replies within four hours." meta="$20 per month" />
  <CheckboxCard title="Extra seats" description="Five more members." meta="$8 per seat" defaultChecked />
</CheckboxCardGroup>

Props

PropTypeDefaultDescription
titleReactNodeThe card's title and the checkbox's accessible name.
descriptionReactNodeOne line under the title.
metaReactNodeMono meta line at the bottom, for example a price.
checkedbooleanControlled checked state.
defaultCheckedbooleanfalseStarting state when uncontrolled.
onCheckedChange(checked: boolean) => voidFires with the next boolean on every toggle.
disabledbooleanfalseDisables the checkbox and fades the card.
name, valuestringForm field name and value, forwarded to the checkbox.
columns1 | 2 | 31On CheckboxCardGroup: columns from the md and lg container widths.

Source

checkbox-card/checkbox-card.tsx
"use client";

import * as React from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";

type CheckboxCardProps = Omit<React.ComponentProps<"label">, "title" | "onChange"> & {
  /** The card's title. Also the checkbox's accessible name. */
  title: React.ReactNode;
  /** One line under the title. Also the checkbox's description. */
  description?: React.ReactNode;
  /** Mono meta line at the bottom, for example a price or a count. */
  meta?: React.ReactNode;
  /** Controlled checked state. */
  checked?: boolean;
  /** Starting state for the uncontrolled form. */
  defaultChecked?: boolean;
  /** Fires with the next boolean on every toggle. */
  onCheckedChange?: (checked: boolean) => void;
  /** Disables the checkbox and fades the whole card. */
  disabled?: boolean;
  /** Form field name, forwarded to the checkbox. */
  name?: string;
  /** Form field value, forwarded to the checkbox. */
  value?: string;
};

/**
 * A selectable card. The whole card is the label, so clicking anywhere toggles it.
 * Checked shows as a foreground border and a soft fill; keyboard focus rings the card.
 */
export function CheckboxCard({
  title,
  description,
  meta,
  checked,
  defaultChecked,
  onCheckedChange,
  disabled,
  name,
  value,
  className,
  ...props
}: CheckboxCardProps) {
  const id = React.useId();
  const titleId = `${id}-title`;
  const descriptionId = description ? `${id}-description` : undefined;

  return (
    <label
      data-slot="checkbox-card"
      className={cn(
        "group/field-label relative flex cursor-pointer gap-3 rounded-lg border bg-card p-3 text-left transition-colors duration-100 ease-out",
        "hover:bg-accent/40 has-[:focus-visible]:border-ring has-[:focus-visible]:ring-3 has-[:focus-visible]:ring-ring/50",
        "has-data-[state=checked]:border-foreground/40 has-data-[state=checked]:bg-muted/40",
        "has-data-disabled:cursor-not-allowed has-data-disabled:bg-card/60",
        className,
      )}
      {...props}
    >
      <Checkbox
        className="mt-0.5 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
        checked={checked}
        defaultChecked={defaultChecked}
        onCheckedChange={(next) => onCheckedChange?.(next === true)}
        disabled={disabled}
        name={name}
        value={value}
        aria-labelledby={titleId}
        aria-describedby={descriptionId}
      />
      <span className="flex min-w-0 flex-1 flex-col gap-0.5">
        <span
          id={titleId}
          className="text-sm leading-5 font-medium text-foreground group-has-data-disabled/field-label:text-muted-foreground"
        >
          {title}
        </span>
        {description ? (
          <span
            id={descriptionId}
            className="text-sm text-foreground/70 group-has-data-disabled/field-label:text-muted-foreground"
          >
            {description}
          </span>
        ) : null}
        {meta ? (
          <span className="mt-1 font-mono text-xs text-foreground/70 group-has-data-disabled/field-label:text-muted-foreground">
            {meta}
          </span>
        ) : null}
      </span>
    </label>
  );
}

type CheckboxCardGroupProps = React.ComponentProps<"div"> & {
  /** Columns from the md container width (448px) for 2, the lg width (512px) for 3. One below. */
  columns?: 1 | 2 | 3;
};

/** A grid of cards with gap-2. Give it an aria-label so the group has a name. */
export function CheckboxCardGroup({ columns = 1, className, ...props }: CheckboxCardGroupProps) {
  return (
    <div
      role="group"
      data-slot="checkbox-card-group"
      className={cn(
        "@container grid w-full gap-2",
        columns === 2 && "@md:grid-cols-2",
        columns === 3 && "@lg:grid-cols-3",
        className,
      )}
      {...props}
    />
  );
}

For AI agents

Hand your agent this page, the markdown at /components/checkbox-card/prompt.md, or the Copy AI prompt button above.

When to use

  • Picking several options that each deserve a title and a line of context: add-ons, features, permissions
  • Settings pages where a plain checkbox list would lose the explanation
  • Onboarding steps that ask what someone wants to turn on

When not to use

  • For a single choice from a set, use radio-cards
  • For long lists of simple options, a plain checkbox list is denser
  • As a toggle for an immediate action; use a switch

Agent notes

  • The whole card is a label around shadcn's Checkbox, so every click toggles it and the card carries the focus ring
  • onCheckedChange is a function: pass it from a client component. Uncontrolled with defaultChecked works from a server component
  • Checked styling targets radix's data-state=checked on the card and the checkbox, so it does not depend on the primitive's own checked classes
  • CheckboxCardGroup is a plain grid with role=group; give it an aria-label and use columns for 2 or 3 across