Checkbox
Free, copy-and-go Checkbox components built on the SevenUI Checkbox primitive.Read the primitive docs.
A Monday morning summary of new comments, mentions, and finished tasks.
Release notes and new features, about twice a month.
"use client";
import { Checkbox } from "@/components/ui/checkbox";
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
const preferences = [
{
name: "weeklyDigest",
label: "Send me the weekly digest",
description:
"A Monday morning summary of new comments, mentions, and finished tasks.",
defaultChecked: true,
},
{
name: "productUpdates",
label: "Product announcements",
description: "Release notes and new features, about twice a month.",
defaultChecked: false,
},
];
export default function Checkbox01() {
return (
<FieldGroup className="w-full max-w-sm gap-4">
{preferences.map((preference) => (
<Field
key={preference.name}
name={preference.name}
orientation="horizontal"
>
<Checkbox defaultChecked={preference.defaultChecked} />
<FieldContent>
<FieldLabel>{preference.label}</FieldLabel>
<FieldDescription>{preference.description}</FieldDescription>
</FieldContent>
</Field>
))}
</FieldGroup>
);
}
npx shadcn@latest add @sevenui/component/checkbox-01pnpm dlx shadcn@latest add @sevenui/component/checkbox-01yarn dlx shadcn@latest add @sevenui/component/checkbox-01bunx --bun shadcn@latest add @sevenui/component/checkbox-01"use client";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
// Each size keeps the hit area generous; only the visible box changes.
const sizes = [
{
id: "checkbox-02-sm",
label: "Select row",
usage: "Small, for dense tables",
className: "size-3.5 rounded-[3px] [&_svg]:size-3!",
labelClassName: "text-xs",
},
{
id: "checkbox-02-default",
label: "Remember this device",
usage: "Default, for forms and settings",
className: "",
labelClassName: "text-sm",
},
{
id: "checkbox-02-lg",
label: "Add oat milk to the list",
usage: "Large, for touch-first screens",
className: "size-5 rounded-md [&_svg]:size-4!",
labelClassName: "text-base",
},
];
export default function Checkbox02() {
return (
<div className="flex w-full max-w-sm flex-col divide-y divide-border rounded-xl border border-border bg-card">
{sizes.map((size) => (
<div key={size.id} className="flex items-center gap-3 px-4 py-3.5">
<Checkbox
id={size.id}
defaultChecked
className={size.className}
/>
<div className="flex min-w-0 flex-col gap-0.5">
<Label htmlFor={size.id} className={size.labelClassName}>
{size.label}
</Label>
<span className="text-xs text-muted-foreground">{size.usage}</span>
</div>
</div>
))}
</div>
);
}
npx shadcn@latest add @sevenui/component/checkbox-02pnpm dlx shadcn@latest add @sevenui/component/checkbox-02yarn dlx shadcn@latest add @sevenui/component/checkbox-02bunx --bun shadcn@latest add @sevenui/component/checkbox-02"use client";
import * as React from "react";
import { LockIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/components/ui/field";
const initial = { sharing: false, expiry: true };
export default function Checkbox03() {
const [sharing, setSharing] = React.useState(initial.sharing);
const [expiry, setExpiry] = React.useState(initial.expiry);
const [saved, setSaved] = React.useState(initial);
const dirty = sharing !== saved.sharing || expiry !== saved.expiry;
return (
<form
className="w-full max-w-md rounded-xl border border-border bg-card p-5 text-card-foreground"
onSubmit={(event) => {
event.preventDefault();
setSaved({ sharing, expiry });
}}
>
<FieldSet>
<FieldLegend>Workspace policies</FieldLegend>
<p className="-mt-1.5 text-sm text-muted-foreground">
Some settings are managed by your organization or your plan.
</p>
<FieldGroup className="gap-4">
{/* Read-only: shows a value the user can't change here. */}
<Field orientation="horizontal">
<Checkbox readOnly checked className="data-readonly:opacity-70" />
<FieldContent>
<FieldLabel className="flex-wrap items-center gap-y-1">
Require two-factor authentication
<Badge variant="secondary" className="gap-1">
<LockIcon aria-hidden="true" />
Managed
</Badge>
</FieldLabel>
<FieldDescription>
Enforced by Northwind IT for every member.
</FieldDescription>
</FieldContent>
</Field>
{/* Disabled: unavailable on the current plan. */}
<Field orientation="horizontal" disabled>
<Checkbox />
<FieldContent>
<FieldLabel>Allow guest access</FieldLabel>
<FieldDescription>
Available on the Business plan.
</FieldDescription>
</FieldContent>
</Field>
{/* Dependent: the child only applies while its parent is on. */}
<div className="flex flex-col gap-3">
<Field orientation="horizontal">
<Checkbox
checked={sharing}
onCheckedChange={(checked) => setSharing(checked)}
/>
<FieldContent>
<FieldLabel>Allow public file links</FieldLabel>
<FieldDescription>
Anyone with the link can view the file.
</FieldDescription>
</FieldContent>
</Field>
<Field
orientation="horizontal"
disabled={!sharing}
className="ms-2 border-s border-border ps-5"
>
<Checkbox
checked={sharing && expiry}
onCheckedChange={(checked) => setExpiry(checked)}
/>
<FieldContent>
<FieldLabel>Expire links after 30 days</FieldLabel>
<FieldDescription>
{sharing
? "Old links stop working automatically."
: "Turn on public file links to change this."}
</FieldDescription>
</FieldContent>
</Field>
</div>
</FieldGroup>
</FieldSet>
<div className="mt-6 flex flex-wrap items-center justify-end gap-3">
<p role="status" className="mr-auto text-sm text-muted-foreground">
{dirty ? "Unsaved changes" : "All changes saved"}
</p>
<Button type="submit" disabled={!dirty}>
Save policies
</Button>
</div>
</form>
);
}
npx shadcn@latest add @sevenui/component/checkbox-03pnpm dlx shadcn@latest add @sevenui/component/checkbox-03yarn dlx shadcn@latest add @sevenui/component/checkbox-03bunx --bun shadcn@latest add @sevenui/component/checkbox-03"use client";
import * as React from "react";
import { DatabaseBackupIcon, HeadsetIcon, ShieldCheckIcon } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLegend,
FieldSet,
FieldTitle,
} from "@/components/ui/field";
const addons = [
{
id: "backups",
title: "Daily backups",
description: "Point-in-time restore for the last 30 days.",
price: 12,
icon: DatabaseBackupIcon,
},
{
id: "sso",
title: "SAML single sign-on",
description: "Connect Okta, Entra ID, or Google Workspace.",
price: 24,
icon: ShieldCheckIcon,
},
{
id: "support",
title: "Priority support",
description: "Replies within 4 business hours.",
price: 49,
icon: HeadsetIcon,
},
];
export default function Checkbox04() {
const id = React.useId();
const [selected, setSelected] = React.useState<string[]>(["backups"]);
const total = addons
.filter((addon) => selected.includes(addon.id))
.reduce((sum, addon) => sum + addon.price, 0);
return (
<FieldSet className="w-full max-w-md">
<FieldLegend>Add-ons</FieldLegend>
<p className="-mt-1.5 text-sm text-muted-foreground">
Billed monthly with your Team plan.
</p>
<FieldGroup className="gap-3">
{addons.map((addon) => {
const Icon = addon.icon;
const checkboxId = `${id}-${addon.id}`;
return (
<label
key={addon.id}
htmlFor={checkboxId}
className="cursor-pointer rounded-lg border border-border p-3 transition-colors hover:bg-muted/50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-focus-visible:border-ring has-focus-visible:ring-3 has-focus-visible:ring-ring/50 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10"
>
<Field orientation="horizontal" className="items-start">
<Checkbox
id={checkboxId}
className="mt-0.5 after:hidden"
checked={selected.includes(addon.id)}
onCheckedChange={(checked) =>
setSelected((current) =>
checked
? [...current, addon.id]
: current.filter((value) => value !== addon.id),
)
}
/>
<FieldContent>
<FieldTitle>
<Icon
aria-hidden="true"
className="size-4 text-muted-foreground"
/>
{addon.title}
</FieldTitle>
<FieldDescription>{addon.description}</FieldDescription>
</FieldContent>
<span className="shrink-0 text-sm font-medium tabular-nums">
${addon.price}
<span className="font-normal text-muted-foreground">/mo</span>
</span>
</Field>
</label>
);
})}
</FieldGroup>
<div className="flex items-center justify-between border-t border-border pt-4 text-sm">
<span className="text-muted-foreground">Add-ons total</span>
<span aria-live="polite" className="font-semibold tabular-nums">
${total}/mo
</span>
</div>
</FieldSet>
);
}
npx shadcn@latest add @sevenui/component/checkbox-04pnpm dlx shadcn@latest add @sevenui/component/checkbox-04yarn dlx shadcn@latest add @sevenui/component/checkbox-04bunx --bun shadcn@latest add @sevenui/component/checkbox-04Selective sync
Unchecked folders stay online only and free up space on this Mac.
- 12.7 GB
- 9.6 GB
- 1.2 GB
- 8.4 GB
- 3.1 GB
- 15.0 GB
- 3.2 GB
7.9 GB on this Mac
"use client";
import * as React from "react";
import { ChevronRightIcon, FolderIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
// Swaps the default check for a dash while the box is indeterminate.
const mixedIndicator =
"data-indeterminate:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground data-indeterminate:[&_svg]:hidden data-indeterminate:before:h-0.5 data-indeterminate:before:w-2 data-indeterminate:before:rounded-full data-indeterminate:before:bg-current";
type Folder = {
id: string;
name: string;
// Only leaf folders carry a size; parents sum their children.
sizeGb?: number;
children?: Folder[];
};
const tree: Folder[] = [
{
id: "design",
name: "Design",
children: [
{
id: "brand",
name: "Brand assets",
children: [
{ id: "logos", name: "Logos", sizeGb: 1.2 },
{ id: "photography", name: "Photography", sizeGb: 8.4 },
],
},
{ id: "figma", name: "Figma exports", sizeGb: 3.1 },
],
},
{
id: "engineering",
name: "Engineering",
children: [
{ id: "builds", name: "Release builds", sizeGb: 14.6 },
{ id: "specs", name: "Specs", sizeGb: 0.4 },
],
},
{
id: "finance",
name: "Finance",
children: [
{ id: "invoices", name: "Invoices 2026", sizeGb: 0.9 },
{ id: "decks", name: "Board decks", sizeGb: 2.3 },
],
},
];
const FREE_SPACE_GB = 20;
function leaves(folder: Folder): Folder[] {
return folder.children ? folder.children.flatMap(leaves) : [folder];
}
function sizeOf(folders: Folder[]) {
return folders.reduce((sum, folder) => sum + (folder.sizeGb ?? 0), 0);
}
const initialSynced = ["logos", "figma", "specs", "invoices", "decks"];
export default function Checkbox05() {
const id = React.useId();
const [synced, setSynced] = React.useState<string[]>(initialSynced);
const [open, setOpen] = React.useState<string[]>(["design", "brand"]);
// What is already on this Mac; "Update sync" moves the baseline.
const [applied, setApplied] = React.useState<string[]>(initialSynced);
const [justSynced, setJustSynced] = React.useState(false);
const syncedSize = sizeOf(
tree.flatMap(leaves).filter((leaf) => synced.includes(leaf.id)),
);
const appliedSize = sizeOf(
tree.flatMap(leaves).filter((leaf) => applied.includes(leaf.id)),
);
const initialSize = sizeOf(
tree.flatMap(leaves).filter((leaf) => initialSynced.includes(leaf.id)),
);
// Syncing more folders uses up disk; making some online-only frees it.
const freeSpace = FREE_SPACE_GB + initialSize - appliedSize;
const extra = syncedSize - appliedSize;
const overLimit = extra > freeSpace;
const dirty =
synced.length !== applied.length ||
synced.some((leaf) => !applied.includes(leaf));
function setMany(ids: string[], checked: boolean) {
setJustSynced(false);
setSynced((current) =>
checked
? [...new Set([...current, ...ids])]
: current.filter((value) => !ids.includes(value)),
);
}
function renderFolder(folder: Folder, depth: number) {
const ids = leaves(folder).map((leaf) => leaf.id);
const count = ids.filter((leaf) => synced.includes(leaf)).length;
const isOpen = open.includes(folder.id);
const checkboxId = `${id}-${folder.id}`;
const groupId = `${id}-${folder.id}-group`;
const size = sizeOf(leaves(folder));
return (
<li key={folder.id}>
<div
className="flex items-center gap-2 rounded-md py-1.5 pe-2 hover:bg-muted/60"
style={{ paddingInlineStart: `${depth * 1.25 + 0.25}rem` }}
>
{folder.children ? (
<Button
variant="ghost"
size="icon-xs"
aria-expanded={isOpen}
aria-controls={groupId}
aria-label={`${isOpen ? "Collapse" : "Expand"} ${folder.name}`}
onClick={() =>
setOpen((current) =>
isOpen
? current.filter((value) => value !== folder.id)
: [...current, folder.id],
)
}
>
<ChevronRightIcon
aria-hidden="true"
className="transition-transform duration-200 ease-out data-[open=true]:rotate-90 motion-reduce:transition-none"
data-open={isOpen}
/>
</Button>
) : (
<span aria-hidden="true" className="size-6 shrink-0" />
)}
<Checkbox
id={checkboxId}
className={mixedIndicator}
checked={count === ids.length}
indeterminate={count > 0 && count < ids.length}
onCheckedChange={(checked) => setMany(ids, checked)}
/>
<label
htmlFor={checkboxId}
className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 text-sm select-none"
>
<FolderIcon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground"
/>
<span className="truncate">{folder.name}</span>
</label>
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">
{size.toFixed(1)} GB
</span>
</div>
{folder.children && isOpen ? (
<ul id={groupId} aria-label={folder.name}>
{folder.children.map((child) => renderFolder(child, depth + 1))}
</ul>
) : null}
</li>
);
}
return (
<div className="w-full max-w-sm rounded-xl border border-border bg-card text-card-foreground">
<div className="border-b border-border px-4 py-3">
<p id={`${id}-title`} className="text-sm font-medium">
Selective sync
</p>
<p className="text-xs text-muted-foreground">
Unchecked folders stay online only and free up space on this Mac.
</p>
</div>
<ul aria-labelledby={`${id}-title`} className="p-2">
{tree.map((folder) => renderFolder(folder, 0))}
</ul>
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-border px-4 py-3">
<p
aria-live="polite"
className="text-xs text-muted-foreground tabular-nums data-[over=true]:text-destructive"
data-over={overLimit}
>
{overLimit
? `Needs ${extra.toFixed(1)} GB, only ${freeSpace.toFixed(1)} GB free`
: justSynced
? `Synced. ${syncedSize.toFixed(1)} GB on this Mac`
: `${syncedSize.toFixed(1)} GB on this Mac`}
</p>
<Button
size="sm"
disabled={!dirty || overLimit}
onClick={() => {
setApplied(synced);
setJustSynced(true);
}}
>
Update sync
</Button>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/checkbox-05pnpm dlx shadcn@latest add @sevenui/component/checkbox-05yarn dlx shadcn@latest add @sevenui/component/checkbox-05bunx --bun shadcn@latest add @sevenui/component/checkbox-05Request reviewers
Pick up to 2 people for “Refine date picker focus”.
1 more allowed
"use client";
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
const MAX_REVIEWERS = 2;
const teammates = [
{ id: "maya", name: "Maya Chen", role: "Frontend lead", initials: "MC" },
{
id: "daniel",
name: "Daniel Okafor",
role: "Design systems",
initials: "DO",
},
{ id: "priya", name: "Priya Raman", role: "Accessibility", initials: "PR" },
{ id: "lucas", name: "Lucas Moreau", role: "Platform", initials: "LM" },
];
export default function Checkbox06() {
const id = React.useId();
const [reviewers, setReviewers] = React.useState<string[]>(["priya"]);
const [requested, setRequested] = React.useState(false);
const limitReached = reviewers.length >= MAX_REVIEWERS;
return (
<div className="w-full max-w-sm rounded-xl border border-border bg-card">
<div className="flex flex-col gap-0.5 border-b border-border px-4 py-3">
<p id={`${id}-title`} className="text-sm font-medium">
Request reviewers
</p>
<p id={`${id}-hint`} className="text-xs text-muted-foreground">
Pick up to {MAX_REVIEWERS} people for “Refine date picker focus”.
</p>
</div>
<ul
aria-labelledby={`${id}-title`}
aria-describedby={`${id}-hint`}
className="flex flex-col p-1.5"
>
{teammates.map((person) => {
const checkboxId = `${id}-${person.id}`;
const checked = reviewers.includes(person.id);
const disabled = !checked && limitReached;
return (
<li key={person.id}>
<label
htmlFor={checkboxId}
data-disabled={disabled || undefined}
className="flex cursor-pointer items-center gap-3 rounded-lg px-2.5 py-2 transition-colors hover:bg-muted/60 has-data-checked:bg-muted data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:bg-transparent"
>
<Avatar>
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback className="text-xs">
{person.initials}
</AvatarFallback>
</Avatar>
<span className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-medium">
{person.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{person.role}
</span>
</span>
<Checkbox
id={checkboxId}
className="after:hidden"
checked={checked}
disabled={disabled}
onCheckedChange={(value) => {
setRequested(false);
setReviewers((current) =>
value
? [...current, person.id]
: current.filter((item) => item !== person.id),
);
}}
/>
</label>
</li>
);
})}
</ul>
<div className="flex items-center justify-between gap-3 border-t border-border px-4 py-3">
<p aria-live="polite" className="text-xs text-muted-foreground">
{requested
? "Review requested"
: limitReached
? "Limit reached"
: `${MAX_REVIEWERS - reviewers.length} more allowed`}
</p>
<Button
size="sm"
disabled={reviewers.length === 0 || requested}
onClick={() => setRequested(true)}
>
{reviewers.length > 1
? `Request ${reviewers.length} reviews`
: "Request review"}
</Button>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/checkbox-06pnpm dlx shadcn@latest add @sevenui/component/checkbox-06yarn dlx shadcn@latest add @sevenui/component/checkbox-06bunx --bun shadcn@latest add @sevenui/component/checkbox-06Finish setting up Acme
2 of 5 done"use client";
import * as React from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { Spinner } from "@/components/ui/spinner";
const initialTasks = [
{ id: "domain", label: "Connect your custom domain", done: true },
{ id: "invite", label: "Invite two teammates", done: true },
{ id: "import", label: "Import your first contacts", done: false },
{ id: "billing", label: "Add a billing contact", done: false },
{ id: "branding", label: "Upload a logo for invoices", done: false },
];
// Simulated server round-trip before a change is confirmed.
const SAVE_DELAY_MS = 700;
export default function Checkbox07() {
const id = React.useId();
const [tasks, setTasks] = React.useState(initialTasks);
const [saving, setSaving] = React.useState<string[]>([]);
const timers = React.useRef(new Map<string, ReturnType<typeof setTimeout>>());
React.useEffect(() => {
const pending = timers.current;
return () => {
for (const timer of pending.values()) clearTimeout(timer);
};
}, []);
function toggle(taskId: string, done: boolean) {
// Ignore repeat toggles until the pending save settles.
if (timers.current.has(taskId)) return;
setTasks((current) =>
current.map((task) => (task.id === taskId ? { ...task, done } : task)),
);
setSaving((current) => [...current, taskId]);
const timer = setTimeout(() => {
timers.current.delete(taskId);
setSaving((current) => current.filter((item) => item !== taskId));
}, SAVE_DELAY_MS);
timers.current.set(taskId, timer);
}
const doneCount = tasks.filter((task) => task.done).length;
const percent = Math.round((doneCount / tasks.length) * 100);
return (
<div className="w-full max-w-sm rounded-xl border border-border bg-card p-4">
<div className="flex items-baseline justify-between gap-3">
<p id={`${id}-title`} className="text-sm font-medium">
Finish setting up Acme
</p>
<span
aria-live="polite"
className="text-xs text-muted-foreground tabular-nums"
>
{doneCount} of {tasks.length} done
</span>
</div>
<div
aria-hidden="true"
className="mt-3 h-1 overflow-hidden rounded-full bg-muted"
>
<div
className="h-full rounded-full bg-primary transition-[width] duration-500 ease-out"
style={{ width: `${percent}%` }}
/>
</div>
<ul aria-labelledby={`${id}-title`} className="mt-3 flex flex-col">
{tasks.map((task) => {
const checkboxId = `${id}-${task.id}`;
const isSaving = saving.includes(task.id);
return (
<li key={task.id} className="flex items-center gap-3 py-2">
<span className="relative grid size-5 shrink-0 place-items-center">
<Checkbox
id={checkboxId}
checked={task.done}
aria-busy={isSaving || undefined}
onCheckedChange={(checked) => toggle(task.id, checked)}
className="size-5 rounded-full transition-[background-color,border-color,transform] duration-200 ease-out active:scale-90 data-checked:[&_svg]:animate-in data-checked:[&_svg]:zoom-in-50 [&_svg]:size-3.5! motion-reduce:transition-none"
/>
{isSaving ? (
<span className="absolute inset-0 grid place-items-center rounded-full bg-card">
<Spinner
aria-label="Saving"
className="size-4 text-muted-foreground"
/>
</span>
) : null}
</span>
<label
htmlFor={checkboxId}
className="group relative cursor-pointer text-sm"
data-done={task.done || undefined}
>
<span className="transition-colors duration-300 group-data-done:text-muted-foreground">
{task.label}
</span>
<span
aria-hidden="true"
className="absolute inset-x-0 top-1/2 h-px origin-left scale-x-0 bg-muted-foreground transition-transform duration-300 ease-out group-data-done:scale-x-100 motion-reduce:transition-none"
/>
</label>
</li>
);
})}
</ul>
</div>
);
}
npx shadcn@latest add @sevenui/component/checkbox-07pnpm dlx shadcn@latest add @sevenui/component/checkbox-07yarn dlx shadcn@latest add @sevenui/component/checkbox-07bunx --bun shadcn@latest add @sevenui/component/checkbox-07- Subtotal (2 items)
- $184.00
- Shipping
- $6.50
- Tax
- $15.18
- Total
- $205.68
Includes the 30-day return policy and the final-sale terms for discounted items.
Tracking links only. No promotions.
"use client";
import { useState } from "react";
import { LockIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import { Separator } from "@/components/ui/separator";
const lines = [
{ label: "Subtotal (2 items)", amount: "$184.00" },
{ label: "Shipping", amount: "$6.50" },
{ label: "Tax", amount: "$15.18" },
];
export default function Checkbox08() {
const [accepted, setAccepted] = useState(false);
const [updates, setUpdates] = useState(true);
const [attempted, setAttempted] = useState(false);
const [placed, setPlaced] = useState(false);
const showError = attempted && !accepted;
function placeOrder() {
setAttempted(true);
if (accepted) setPlaced(true);
}
if (placed) {
return (
<div className="w-full max-w-sm rounded-xl border border-border bg-card p-6 text-card-foreground">
<p className="text-base font-medium">Order NW-10482 placed</p>
<p className="mt-1 text-sm text-muted-foreground">
{updates
? "We'll email tracking details to jordan@northwind.co as soon as it ships."
: "Tracking details will appear under Orders in your account."}
</p>
<Button
variant="outline"
className="mt-5 w-full"
onClick={() => {
setPlaced(false);
setAttempted(false);
setAccepted(false);
}}
>
Back to checkout
</Button>
</div>
);
}
return (
<div className="w-full max-w-sm rounded-xl border border-border bg-card p-5 text-card-foreground">
<dl className="flex flex-col gap-2 text-sm">
{lines.map((line) => (
<div key={line.label} className="flex justify-between gap-4">
<dt className="text-muted-foreground">{line.label}</dt>
<dd className="tabular-nums">{line.amount}</dd>
</div>
))}
<Separator className="my-1" />
<div className="flex justify-between gap-4 text-base font-medium">
<dt>Total</dt>
<dd className="tabular-nums">$205.68</dd>
</div>
</dl>
<div className="mt-5 flex flex-col gap-4">
<Field orientation="horizontal" invalid={showError}>
<Checkbox
name="terms"
checked={accepted}
aria-invalid={showError || undefined}
onCheckedChange={(checked) => setAccepted(checked)}
/>
<FieldContent>
<FieldLabel>I agree to the Terms of Sale</FieldLabel>
<FieldDescription className="text-xs">
Includes the 30-day return policy and the final-sale terms for
discounted items.
</FieldDescription>
{showError ? (
<FieldError className="text-xs">
Accept the Terms of Sale to place your order.
</FieldError>
) : null}
</FieldContent>
</Field>
<Field orientation="horizontal">
<Checkbox
name="orderUpdates"
checked={updates}
onCheckedChange={(checked) => setUpdates(checked)}
/>
<FieldContent>
<FieldLabel>Email me shipping updates</FieldLabel>
<FieldDescription className="text-xs">
Tracking links only. No promotions.
</FieldDescription>
</FieldContent>
</Field>
</div>
<Button size="lg" className="mt-5 w-full" onClick={placeOrder}>
<LockIcon aria-hidden="true" data-icon="inline-start" />
Place order · $205.68
</Button>
</div>
);
}
npx shadcn@latest add @sevenui/component/checkbox-08pnpm dlx shadcn@latest add @sevenui/component/checkbox-08yarn dlx shadcn@latest add @sevenui/component/checkbox-08bunx --bun shadcn@latest add @sevenui/component/checkbox-08Cookie preferences
Choose what Fieldnote may store in your browser. You can change this any time from the footer.
- Always on
Sign-in, security, and your cart. The site can't work without these.
- 3 vendors
Remember your language, region, and recently viewed items.
- 4 vendors
Anonymous page views that show us which features get used.
- 11 vendors
Measure ad campaigns and show you relevant offers elsewhere.
"use client";
import { useId, useState } from "react";
import { CookieIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
const categories = [
{
id: "necessary",
title: "Strictly necessary",
description:
"Sign-in, security, and your cart. The site can't work without these.",
vendors: 2,
locked: true,
},
{
id: "functional",
title: "Functional",
description: "Remember your language, region, and recently viewed items.",
vendors: 3,
locked: false,
},
{
id: "analytics",
title: "Analytics",
description: "Anonymous page views that show us which features get used.",
vendors: 4,
locked: false,
},
{
id: "marketing",
title: "Marketing",
description: "Measure ad campaigns and show you relevant offers elsewhere.",
vendors: 11,
locked: false,
},
];
const optional = categories
.filter((category) => !category.locked)
.map((category) => category.id);
export default function Checkbox09() {
const id = useId();
const [allowed, setAllowed] = useState<string[]>(["functional"]);
const [savedMessage, setSavedMessage] = useState<string | null>(null);
function save(next: string[]) {
setAllowed(next);
setSavedMessage(
next.length === 0
? "Only necessary cookies are on."
: `Saved. ${next.length} of ${optional.length} optional categories on.`,
);
}
return (
<section
aria-labelledby={`${id}-title`}
className="w-full max-w-md rounded-xl border border-border bg-card text-card-foreground shadow-lg"
>
<div className="flex items-start gap-3 p-4">
<CookieIcon
aria-hidden="true"
className="mt-0.5 size-5 shrink-0 text-muted-foreground"
/>
<div>
<h2 id={`${id}-title`} className="text-sm font-medium">
Cookie preferences
</h2>
<p className="mt-0.5 text-sm text-muted-foreground">
Choose what Fieldnote may store in your browser. You can change
this any time from the footer.
</p>
</div>
</div>
<ul className="border-y border-border">
{categories.map((category) => {
const checkboxId = `${id}-${category.id}`;
return (
<li
key={category.id}
className="flex items-start gap-3 border-b border-border px-4 py-3 last:border-b-0"
>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-baseline gap-x-2">
<label
htmlFor={checkboxId}
className="cursor-pointer text-sm font-medium data-[locked=true]:cursor-default"
data-locked={category.locked}
>
{category.title}
</label>
<span className="text-xs text-muted-foreground tabular-nums">
{category.locked
? "Always on"
: `${category.vendors} vendors`}
</span>
</div>
<p
id={`${checkboxId}-description`}
className="mt-0.5 text-xs text-muted-foreground"
>
{category.description}
</p>
</div>
<Checkbox
id={checkboxId}
aria-describedby={`${checkboxId}-description`}
className="mt-0.5"
checked={category.locked || allowed.includes(category.id)}
disabled={category.locked}
onCheckedChange={(checked) => {
setSavedMessage(null);
setAllowed((current) =>
checked
? [...current, category.id]
: current.filter((value) => value !== category.id),
);
}}
/>
</li>
);
})}
</ul>
<div className="p-4">
<p
aria-live="polite"
className="text-xs text-muted-foreground not-empty:mb-3"
>
{savedMessage}
</p>
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<Button variant="ghost" onClick={() => save([])}>
Reject all
</Button>
<Button variant="outline" onClick={() => save(allowed)}>
Save choices
</Button>
<Button onClick={() => save(optional)}>Accept all</Button>
</div>
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/checkbox-09pnpm dlx shadcn@latest add @sevenui/component/checkbox-09yarn dlx shadcn@latest add @sevenui/component/checkbox-09bunx --bun shadcn@latest add @sevenui/component/checkbox-09"use client";
import { useId, useState } from "react";
import { XIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
type Product = {
brand: string;
material: string;
inStock: boolean;
};
// Sample catalog the facet counts are derived from.
const products: Product[] = [
{ brand: "Arket", material: "Linen", inStock: true },
{ brand: "Arket", material: "Cotton", inStock: true },
{ brand: "Arket", material: "Wool", inStock: false },
{ brand: "COS", material: "Cotton", inStock: true },
{ brand: "COS", material: "Wool", inStock: true },
{ brand: "COS", material: "Linen", inStock: false },
{ brand: "Everlane", material: "Cotton", inStock: true },
{ brand: "Everlane", material: "Cotton", inStock: true },
{ brand: "Everlane", material: "Cashmere", inStock: true },
{ brand: "Norse Projects", material: "Wool", inStock: true },
{ brand: "Norse Projects", material: "Cotton", inStock: false },
{ brand: "Norse Projects", material: "Linen", inStock: true },
];
const facets = [
{
key: "brand",
label: "Brand",
options: ["Arket", "COS", "Everlane", "Norse Projects"],
},
{
key: "material",
label: "Material",
options: ["Cotton", "Linen", "Wool", "Cashmere"],
},
] as const;
type FacetKey = (typeof facets)[number]["key"];
type Selection = Record<FacetKey, string[]>;
function matches(
product: Product,
selection: Selection,
inStockOnly: boolean,
skip?: FacetKey,
) {
if (inStockOnly && !product.inStock) return false;
return facets.every(({ key }) => {
if (key === skip || selection[key].length === 0) return true;
return selection[key].includes(product[key]);
});
}
function slug(value: string) {
return value.toLowerCase().replace(/\s+/g, "-");
}
export default function Checkbox10() {
const baseId = useId();
const [selection, setSelection] = useState<Selection>({
brand: ["COS"],
material: [],
});
const [inStockOnly, setInStockOnly] = useState(false);
const resultCount = products.filter((product) =>
matches(product, selection, inStockOnly),
).length;
const applied = facets.flatMap(({ key }) =>
selection[key].map((value) => ({ key, value })),
);
function toggle(key: FacetKey, value: string, checked: boolean) {
setSelection((prev) => ({
...prev,
[key]: checked
? [...prev[key], value]
: prev[key].filter((item) => item !== value),
}));
}
return (
<aside
aria-label="Product filters"
className="w-full max-w-xs rounded-xl border border-border bg-card p-4 text-card-foreground"
>
<div className="flex items-baseline justify-between gap-3">
<h3 className="text-sm font-medium">Filters</h3>
<p
aria-live="polite"
className="text-xs text-muted-foreground tabular-nums"
>
{resultCount} {resultCount === 1 ? "result" : "results"}
</p>
</div>
{applied.length > 0 ? (
<div className="mt-3 flex flex-wrap items-center gap-1.5">
{applied.map(({ key, value }) => (
<Button
key={`${key}-${value}`}
variant="secondary"
size="xs"
aria-label={`Remove ${value} filter`}
onClick={() => toggle(key, value, false)}
>
{value}
<XIcon aria-hidden="true" data-icon="inline-end" />
</Button>
))}
<Button
variant="link"
size="xs"
className="px-1"
onClick={() => setSelection({ brand: [], material: [] })}
>
Clear all
</Button>
</div>
) : null}
{facets.map((facet) => (
<fieldset key={facet.key} className="mt-4">
<legend className="mb-2 text-xs font-medium text-muted-foreground">
{facet.label}
</legend>
<div className="flex flex-col gap-0.5">
{facet.options.map((option) => {
// Count what this option would show given the other facets.
const count = products.filter(
(product) =>
product[facet.key] === option &&
matches(product, selection, inStockOnly, facet.key),
).length;
const checked = selection[facet.key].includes(option);
const id = `${baseId}-${facet.key}-${slug(option)}`;
return (
<div
key={option}
className="-mx-2 flex items-center gap-2.5 rounded-md px-2 py-1.5 hover:bg-muted/60"
>
<Checkbox
id={id}
checked={checked}
disabled={count === 0 && !checked}
onCheckedChange={(value) =>
toggle(facet.key, option, value)
}
/>
<Label
htmlFor={id}
className="flex-1 font-normal peer-data-disabled:text-muted-foreground"
>
{option}
</Label>
<span className="text-xs text-muted-foreground tabular-nums">
{count}
</span>
</div>
);
})}
</div>
</fieldset>
))}
<Separator className="my-4" />
<div className="flex items-center gap-2.5">
<Checkbox
id={`${baseId}-in-stock`}
checked={inStockOnly}
onCheckedChange={(value) => setInStockOnly(value)}
/>
<Label htmlFor={`${baseId}-in-stock`} className="font-normal">
Ready to ship only
</Label>
</div>
</aside>
);
}
npx shadcn@latest add @sevenui/component/checkbox-10pnpm dlx shadcn@latest add @sevenui/component/checkbox-10yarn dlx shadcn@latest add @sevenui/component/checkbox-10bunx --bun shadcn@latest add @sevenui/component/checkbox-10"use client";
import { useId, useState } from "react";
import { CheckIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Field,
FieldError,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
const sources = [
{ id: "search", label: "Search engine" },
{ id: "podcast", label: "Podcast or newsletter" },
{ id: "friend", label: "A friend or colleague" },
{ id: "social", label: "LinkedIn or X" },
{ id: "event", label: "Conference or meetup" },
{ id: "other", label: "Something else" },
];
// Picking this clears every other answer, and vice versa.
const NONE = "none";
export default function Checkbox11() {
const id = useId();
const [answers, setAnswers] = useState<string[]>(["podcast"]);
const [otherText, setOtherText] = useState("");
const [attempted, setAttempted] = useState(false);
const [sent, setSent] = useState(false);
const otherChecked = answers.includes("other");
const otherMissing = otherChecked && otherText.trim() === "";
const showOtherError = attempted && otherMissing;
function toggle(value: string, checked: boolean) {
setAnswers((current) => {
if (!checked) return current.filter((item) => item !== value);
if (value === NONE) return [NONE];
return [...current.filter((item) => item !== NONE), value];
});
}
if (sent) {
return (
<div className="flex w-full max-w-sm flex-col items-center gap-3 rounded-xl border border-border bg-card p-6 text-center text-card-foreground">
<span className="grid size-9 place-items-center rounded-full bg-primary text-primary-foreground">
<CheckIcon aria-hidden="true" className="size-4" />
</span>
<div>
<p className="text-sm font-medium">Thanks, that helps a lot</p>
<p className="mt-0.5 text-sm text-muted-foreground">
You're all set. Your workspace is ready.
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
setSent(false);
setAttempted(false);
}}
>
Change my answer
</Button>
</div>
);
}
return (
<form
noValidate
className="w-full max-w-sm rounded-xl border border-border bg-card p-5 text-card-foreground"
onSubmit={(event) => {
event.preventDefault();
setAttempted(true);
if (answers.length > 0 && !otherMissing) setSent(true);
}}
>
<FieldSet className="gap-3">
<FieldLegend className="mb-1 leading-snug">
How did you hear about Tandem?
</FieldLegend>
<p className="text-sm text-muted-foreground">
Select all that apply.
</p>
<div className="flex flex-col gap-1">
{sources.map((source) => (
<Field
key={source.id}
orientation="horizontal"
className="rounded-md px-2 py-1.5 hover:bg-muted/60"
>
<Checkbox
name="source"
value={source.id}
checked={answers.includes(source.id)}
onCheckedChange={(checked) => toggle(source.id, checked)}
/>
<FieldLabel className="flex-1 font-normal">
{source.label}
</FieldLabel>
</Field>
))}
{otherChecked ? (
<Field invalid={showOtherError} className="ps-8 pe-2 pb-1">
<FieldLabel htmlFor={`${id}-other`} className="sr-only">
Where did you hear about us?
</FieldLabel>
<Input
id={`${id}-other`}
placeholder="e.g. a YouTube review"
value={otherText}
aria-invalid={showOtherError || undefined}
onChange={(event) => setOtherText(event.target.value)}
/>
{showOtherError ? (
<FieldError>Tell us where, or uncheck this option.</FieldError>
) : null}
</Field>
) : null}
<div className="my-1 h-px bg-border" />
<Field
orientation="horizontal"
className="rounded-md px-2 py-1.5 hover:bg-muted/60"
>
<Checkbox
name="source"
value={NONE}
checked={answers.includes(NONE)}
onCheckedChange={(checked) => toggle(NONE, checked)}
/>
<FieldLabel className="flex-1 font-normal">
I don't remember
</FieldLabel>
</Field>
</div>
</FieldSet>
<div className="mt-5 flex items-center justify-between gap-3">
<p
aria-live="polite"
className="text-xs text-muted-foreground data-[error=true]:text-destructive"
data-error={attempted && answers.length === 0}
>
{attempted && answers.length === 0
? "Pick at least one answer."
: "Step 3 of 3"}
</p>
<Button type="submit">Finish</Button>
</div>
</form>
);
}
npx shadcn@latest add @sevenui/component/checkbox-11pnpm dlx shadcn@latest add @sevenui/component/checkbox-11yarn dlx shadcn@latest add @sevenui/component/checkbox-11bunx --bun shadcn@latest add @sevenui/component/checkbox-11Export customers
1,284 rows match your current filters.
Preview
Name,Email,Plan,MRR Ada Brooks,ada@lumen.io,Team,240 Kenji Sato,kenji@orbital.jp,Pro,49 Lena Fischer,lena@kraftwerk.de,Team,180
4 of 6 columns
"use client";
import { useId, useState } from "react";
import { DownloadIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
const columns = [
{ id: "name", label: "Name" },
{ id: "email", label: "Email" },
{ id: "plan", label: "Plan" },
{ id: "mrr", label: "MRR" },
{ id: "country", label: "Country" },
{ id: "signedUp", label: "Signed up" },
] as const;
type ColumnId = (typeof columns)[number]["id"];
// The first rows of the export, used for the live preview.
const rows: Record<ColumnId, string>[] = [
{
name: "Ada Brooks",
email: "ada@lumen.io",
plan: "Team",
mrr: "240",
country: "US",
signedUp: "2026-03-14",
},
{
name: "Kenji Sato",
email: "kenji@orbital.jp",
plan: "Pro",
mrr: "49",
country: "JP",
signedUp: "2026-05-02",
},
{
name: "Lena Fischer",
email: "lena@kraftwerk.de",
plan: "Team",
mrr: "180",
country: "DE",
signedUp: "2026-07-21",
},
];
const TOTAL_ROWS = 1284;
export default function Checkbox12() {
const id = useId();
const [included, setIncluded] = useState<ColumnId[]>([
"name",
"email",
"plan",
"mrr",
]);
const [header, setHeader] = useState(true);
const [downloaded, setDownloaded] = useState(false);
// Keep the original column order no matter the click order.
const active = columns.filter((column) => included.includes(column.id));
const lines = [
...(header ? [active.map((column) => column.label).join(",")] : []),
...rows.map((row) => active.map((column) => row[column.id]).join(",")),
];
function download() {
const blob = new Blob([`${lines.join("\n")}\n`], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "customers.csv";
link.click();
URL.revokeObjectURL(url);
setDownloaded(true);
}
return (
<div className="w-full max-w-xl rounded-xl border border-border bg-card text-card-foreground">
<div className="border-b border-border px-4 py-3">
<h3 className="text-sm font-medium">Export customers</h3>
<p className="text-xs text-muted-foreground tabular-nums">
{TOTAL_ROWS.toLocaleString("en-US")} rows match your current filters.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-[11rem_1fr]">
<div className="min-w-0 p-4">
<fieldset className="flex min-w-0 flex-col gap-2.5">
<legend className="mb-2 text-xs font-medium text-muted-foreground">
Columns
</legend>
{columns.map((column) => {
const checkboxId = `${id}-${column.id}`;
return (
<div key={column.id} className="flex items-center gap-2.5">
<Checkbox
id={checkboxId}
checked={included.includes(column.id)}
onCheckedChange={(checked) => {
setDownloaded(false);
setIncluded((current) =>
checked
? [...current, column.id]
: current.filter((value) => value !== column.id),
);
}}
/>
<Label htmlFor={checkboxId} className="font-normal">
{column.label}
</Label>
</div>
);
})}
<Separator className="my-1" />
<div className="flex items-center gap-2.5">
<Checkbox
id={`${id}-header`}
checked={header}
onCheckedChange={(checked) => {
setDownloaded(false);
setHeader(checked);
}}
/>
<Label htmlFor={`${id}-header`} className="font-normal">
Header row
</Label>
</div>
</fieldset>
</div>
<div className="flex min-w-0 flex-col gap-2 border-t border-border p-4 sm:border-t-0 sm:border-s">
<p className="text-xs font-medium text-muted-foreground">Preview</p>
{active.length > 0 ? (
<pre className="rounded-md bg-muted p-3 font-mono text-xs leading-relaxed whitespace-pre overflow-x-auto">
{lines.join("\n")}
</pre>
) : (
<p className="rounded-md border border-dashed border-border p-3 text-xs text-muted-foreground">
Pick at least one column to export.
</p>
)}
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-border px-4 py-3">
<p
aria-live="polite"
className="text-xs text-muted-foreground tabular-nums"
>
{downloaded
? "Downloaded customers.csv"
: `${active.length} of ${columns.length} columns`}
</p>
<Button size="sm" disabled={active.length === 0} onClick={download}>
<DownloadIcon aria-hidden="true" data-icon="inline-start" />
Download CSV
</Button>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/checkbox-12pnpm dlx shadcn@latest add @sevenui/component/checkbox-12yarn dlx shadcn@latest add @sevenui/component/checkbox-12bunx --bun shadcn@latest add @sevenui/component/checkbox-12Project lead
4 members5 of 9 permissions granted
No unsaved changes
"use client";
import { useId, useState } from "react";
import { ShieldCheckIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
// Swaps the default check for a dash while the box is indeterminate.
const mixedIndicator =
"data-indeterminate:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground data-indeterminate:[&_svg]:hidden data-indeterminate:before:h-0.5 data-indeterminate:before:w-2 data-indeterminate:before:rounded-full data-indeterminate:before:bg-current";
const groups = [
{
id: "projects",
label: "Projects",
permissions: [
{ id: "projects.view", label: "View all projects" },
{ id: "projects.create", label: "Create projects" },
{ id: "projects.archive", label: "Archive projects" },
],
},
{
id: "members",
label: "Members",
permissions: [
{ id: "members.invite", label: "Invite members" },
{ id: "members.roles", label: "Change member roles" },
{ id: "members.remove", label: "Remove members" },
],
},
{
id: "billing",
label: "Billing",
permissions: [
{ id: "billing.invoices", label: "Download invoices" },
{ id: "billing.payment", label: "Update payment method" },
{ id: "billing.plan", label: "Change plan" },
],
},
];
const initialSaved = [
"projects.view",
"projects.create",
"projects.archive",
"members.invite",
"billing.invoices",
];
export default function Checkbox13() {
const baseId = useId();
const [saved, setSaved] = useState<string[]>(initialSaved);
const [granted, setGranted] = useState<string[]>(initialSaved);
const [justSaved, setJustSaved] = useState(false);
const total = groups.reduce(
(sum, group) => sum + group.permissions.length,
0,
);
const changes =
granted.filter((id) => !saved.includes(id)).length +
saved.filter((id) => !granted.includes(id)).length;
function setMany(ids: string[], checked: boolean) {
setJustSaved(false);
setGranted((prev) =>
checked
? Array.from(new Set([...prev, ...ids]))
: prev.filter((id) => !ids.includes(id)),
);
}
return (
<div className="w-full max-w-md rounded-xl border border-border bg-card text-card-foreground">
<div className="flex items-start gap-3 p-4">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted">
<ShieldCheckIcon aria-hidden="true" className="size-4" />
</div>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-sm font-medium">Project lead</h3>
<Badge variant="secondary">4 members</Badge>
</div>
<p className="mt-0.5 text-sm text-muted-foreground tabular-nums">
{granted.length} of {total} permissions granted
</p>
</div>
</div>
<div className="flex flex-col gap-2 px-4 pb-4">
{groups.map((group) => {
const ids = group.permissions.map((permission) => permission.id);
const count = ids.filter((id) => granted.includes(id)).length;
const groupId = `${baseId}-${group.id}`;
return (
<fieldset
key={group.id}
className="rounded-lg border border-border"
aria-labelledby={`${groupId}-label`}
>
<div className="flex items-center gap-3 rounded-t-lg bg-muted/50 px-3 py-2.5">
<Checkbox
id={groupId}
className={mixedIndicator}
checked={count === ids.length}
indeterminate={count > 0 && count < ids.length}
onCheckedChange={(checked) => setMany(ids, checked)}
/>
<Label
id={`${groupId}-label`}
htmlFor={groupId}
className="flex-1"
>
{group.label}
</Label>
<span className="text-xs text-muted-foreground tabular-nums">
{count}/{ids.length}
</span>
</div>
<ul className="flex flex-col gap-2.5 px-3 py-3 ps-10">
{group.permissions.map((permission) => {
const id = `${baseId}-${permission.id.replace(".", "-")}`;
const changed =
granted.includes(permission.id) !==
saved.includes(permission.id);
return (
<li key={permission.id} className="flex items-center gap-3">
<Checkbox
id={id}
checked={granted.includes(permission.id)}
onCheckedChange={(checked) =>
setMany([permission.id], checked)
}
/>
<Label htmlFor={id} className="flex-1 font-normal">
{permission.label}
</Label>
{changed ? (
<span className="text-xs text-muted-foreground">
Edited
</span>
) : null}
</li>
);
})}
</ul>
</fieldset>
);
})}
</div>
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-border p-3 ps-4">
<p
aria-live="polite"
className="text-sm text-muted-foreground tabular-nums"
>
{changes === 0
? justSaved
? "Role saved"
: "No unsaved changes"
: `${changes} unsaved ${changes === 1 ? "change" : "changes"}`}
</p>
<div className="flex items-center gap-2">
<Button
variant="ghost"
disabled={changes === 0}
onClick={() => setGranted(saved)}
>
Reset
</Button>
<Button
disabled={changes === 0}
onClick={() => {
setSaved(granted);
setJustSaved(true);
}}
>
Save role
</Button>
</div>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/checkbox-13pnpm dlx shadcn@latest add @sevenui/component/checkbox-13yarn dlx shadcn@latest add @sevenui/component/checkbox-13bunx --bun shadcn@latest add @sevenui/component/checkbox-13Find a time
Tue, Sep 29
Everyone free
First opening: 11:30 AM – 12:00 PM
"use client";
import { useId, useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
const DAY_START = 9;
const DAY_END = 18;
const MIN_SLOT = 0.5; // hours
// Static class strings so Tailwind can see every chart color.
const calendars = [
{
id: "jordan",
name: "Jordan Lee",
note: "You",
block: "bg-chart-1",
},
{
id: "priya",
name: "Priya Shah",
note: "Design",
block: "bg-chart-2",
},
{
id: "marcus",
name: "Marcus Chen",
note: "Engineering",
block: "bg-chart-3",
},
{
id: "room",
name: "Harbor room",
note: "8 seats",
block: "bg-chart-4",
},
];
// Busy blocks for Tuesday, in decimal hours.
const busy: Record<string, [number, number][]> = {
jordan: [
[9, 10],
[13, 14.5],
[16, 17],
],
priya: [
[9.5, 11],
[12, 13],
[15, 16],
],
marcus: [
[10, 11.5],
[14, 15],
],
room: [
[11, 12],
[14.5, 15.5],
],
};
const ticks = [9, 12, 15, 18];
function formatTime(hours: number) {
const h = Math.floor(hours);
const m = Math.round((hours - h) * 60);
const suffix = h >= 12 ? "PM" : "AM";
const display = h > 12 ? h - 12 : h;
return `${display}:${m.toString().padStart(2, "0")} ${suffix}`;
}
function position(start: number, end: number) {
const span = DAY_END - DAY_START;
return {
left: `${((start - DAY_START) / span) * 100}%`,
width: `${((end - start) / span) * 100}%`,
};
}
// Gaps of at least MIN_SLOT where none of the visible calendars are busy.
function freeSlots(ids: string[]) {
const blocks = ids.flatMap((id) => busy[id]).sort((a, b) => a[0] - b[0]);
const slots: [number, number][] = [];
let cursor = DAY_START;
for (const [start, end] of blocks) {
if (start - cursor >= MIN_SLOT) slots.push([cursor, start]);
cursor = Math.max(cursor, end);
}
if (DAY_END - cursor >= MIN_SLOT) slots.push([cursor, DAY_END]);
return slots;
}
export default function Checkbox14() {
const baseId = useId();
const [visible, setVisible] = useState<string[]>([
"jordan",
"priya",
"marcus",
]);
const [booked, setBooked] = useState<string | null>(null);
const slots = freeSlots(visible);
const first = slots[0];
const firstLabel = first
? `${formatTime(first[0])} – ${formatTime(Math.min(first[0] + 1, first[1]))}`
: null;
return (
<div className="w-full max-w-xl rounded-xl border border-border bg-card p-4 text-card-foreground">
<div className="flex items-baseline justify-between gap-3">
<h3 className="text-sm font-medium">Find a time</h3>
<p className="text-xs text-muted-foreground">Tue, Sep 29</p>
</div>
<div className="mt-4 flex flex-col gap-3">
{calendars.map((calendar) => {
const checked = visible.includes(calendar.id);
const id = `${baseId}-${calendar.id}`;
return (
<div
key={calendar.id}
className="grid grid-cols-1 gap-1.5 sm:grid-cols-[14rem_1fr] sm:items-center sm:gap-3"
>
<div className="flex items-center gap-2.5">
<Checkbox
id={id}
checked={checked}
onCheckedChange={(value) => {
setBooked(null);
setVisible((prev) =>
value
? [...prev, calendar.id]
: prev.filter((item) => item !== calendar.id),
);
}}
/>
<Label htmlFor={id} className="min-w-0 gap-1.5">
<span
aria-hidden="true"
className={`size-2 shrink-0 rounded-full ${calendar.block}`}
/>
<span className="truncate">{calendar.name}</span>
<span className="shrink-0 font-normal text-muted-foreground">
{calendar.note}
</span>
</Label>
</div>
<div
aria-hidden="true"
className="relative h-5 rounded-sm bg-muted/60 data-[visible=false]:opacity-40"
data-visible={checked}
>
{busy[calendar.id].map(([start, end]) => (
<span
key={start}
className={`absolute inset-y-0.5 rounded-[3px] ${checked ? calendar.block : "bg-muted-foreground/30"}`}
style={position(start, end)}
/>
))}
</div>
</div>
);
})}
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[14rem_1fr] sm:items-center sm:gap-3">
<p className="text-sm font-medium">Everyone free</p>
<div>
<div
aria-hidden="true"
className="relative h-5 rounded-sm bg-muted/60"
>
{slots.map(([start, end]) => (
<span
key={start}
className="absolute inset-y-0.5 rounded-[3px] border border-dashed border-foreground/40 bg-background"
style={position(start, end)}
/>
))}
</div>
<div
aria-hidden="true"
className="relative mt-1 h-4 text-[10px] text-muted-foreground tabular-nums"
>
{ticks.map((tick) => (
<span
key={tick}
className="absolute -translate-x-1/2 first:translate-x-0 last:-translate-x-full"
style={{ left: position(DAY_START, tick).width }}
>
{tick > 12 ? tick - 12 : tick}
{tick >= 12 ? "p" : "a"}
</span>
))}
</div>
</div>
</div>
</div>
<div className="mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-border pt-4">
<p aria-live="polite" className="text-sm text-muted-foreground">
{booked
? `Booked ${booked}. Invites sent.`
: visible.length === 0
? "Select at least one calendar."
: firstLabel
? `First opening: ${firstLabel}`
: "No shared opening today."}
</p>
<Button
disabled={visible.length === 0 || !firstLabel || booked !== null}
onClick={() => setBooked(firstLabel)}
>
{booked ? "Booked" : "Book slot"}
</Button>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/checkbox-14pnpm dlx shadcn@latest add @sevenui/component/checkbox-14yarn dlx shadcn@latest add @sevenui/component/checkbox-14bunx --bun shadcn@latest add @sevenui/component/checkbox-14