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
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | The card's title and the checkbox's accessible name. | |
| description | ReactNode | One line under the title. | |
| meta | ReactNode | Mono meta line at the bottom, for example a price. | |
| checked | boolean | Controlled checked state. | |
| defaultChecked | boolean | false | Starting state when uncontrolled. |
| onCheckedChange | (checked: boolean) => void | Fires with the next boolean on every toggle. | |
| disabled | boolean | false | Disables the checkbox and fades the card. |
| name, value | string | Form field name and value, forwarded to the checkbox. | |
| columns | 1 | 2 | 3 | 1 | On 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