Accordion
A set of collapsible panels with headings. Built with Base UI and Tailwind CSS. Copy-paste ready.
import {
Accordion,
AccordionItem,
AccordionPanel,
AccordionTrigger,
} from "@/components/ui/accordion";
export default function Particle() {
const items = [
{
content:
"Base UI is a library of high-quality unstyled React components for design systems and web apps.",
id: "1",
title: "What is Base UI?",
},
{
content:
"Head to the \"Quick start\" guide in the docs. If you've used unstyled libraries before, you'll feel at home.",
id: "2",
title: "How do I get started?",
},
{
content: "Of course! Base UI is free and open source.",
id: "3",
title: "Can I use it for my project?",
},
];
return (
<Accordion className="w-full" defaultValue={["3"]}>
{items.map((item) => (
<AccordionItem key={item.id} value={item.id}>
<AccordionTrigger>{item.title}</AccordionTrigger>
<AccordionPanel>{item.content}</AccordionPanel>
</AccordionItem>
))}
</Accordion>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/accordion
Usage
import {
Accordion,
AccordionItem,
AccordionPanel,
AccordionTrigger,
} from "@/components/ui/accordion"<Accordion>
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionPanel>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionPanel>
</AccordionItem>
</Accordion>Examples
Single Accordion
Only one panel can be open at a time. Opening a new item automatically closes the previously open one.
import {
Accordion,
AccordionItem,
AccordionPanel,
AccordionTrigger,
} from "@/components/ui/accordion";
export default function Particle() {
return (
<Accordion className="w-full">
<AccordionItem value="item-1">
<AccordionTrigger>What is Base UI?</AccordionTrigger>
<AccordionPanel>
Base UI is a library of high-quality unstyled React components for
design systems and web apps.
</AccordionPanel>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>How do I get started?</AccordionTrigger>
<AccordionPanel>
Head to the "Quick start" guide in the docs. If you've used unstyled
libraries before, you'll feel at home.
</AccordionPanel>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Can I use it for my project?</AccordionTrigger>
<AccordionPanel>
Of course! Base UI is free and open source.
</AccordionPanel>
</AccordionItem>
</Accordion>
);
}
Multiple Accordion
Set type="multiple" to allow any number of panels to be open simultaneously.
import {
Accordion,
AccordionItem,
AccordionPanel,
AccordionTrigger,
} from "@/components/ui/accordion";
export default function Particle() {
return (
<Accordion className="w-full" multiple>
<AccordionItem value="item-1">
<AccordionTrigger>What is Base UI?</AccordionTrigger>
<AccordionPanel>
Base UI is a library of high-quality unstyled React components for
design systems and web apps.
</AccordionPanel>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>How do I get started?</AccordionTrigger>
<AccordionPanel>
Head to the "Quick start" guide in the docs. If you've used unstyled
libraries before, you'll feel at home.
</AccordionPanel>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Can I use it for my project?</AccordionTrigger>
<AccordionPanel>
Of course! Base UI is free and open source.
</AccordionPanel>
</AccordionItem>
</Accordion>
);
}
Controlled Accordion
Manage the open state externally with value and onValueChange to drive accordion state from parent logic.
Open items: None
"use client";
import { useState } from "react";
import {
Accordion,
AccordionItem,
AccordionPanel,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
export default function Particle() {
const [value, setValue] = useState<string[]>([]);
return (
<div className="flex w-full flex-col gap-4">
<Accordion className="w-full" onValueChange={setValue} value={value}>
<AccordionItem value="item-1">
<AccordionTrigger>What is Base UI?</AccordionTrigger>
<AccordionPanel>
Base UI is a library of high-quality unstyled React components for
design systems and web apps.
</AccordionPanel>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>How do I get started?</AccordionTrigger>
<AccordionPanel>
Head to the "Quick start" guide in the docs. If you've used unstyled
libraries before, you'll feel at home.
</AccordionPanel>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Can I use it for my project?</AccordionTrigger>
<AccordionPanel>
Of course! Base UI is free and open source.
</AccordionPanel>
</AccordionItem>
</Accordion>
<div className="flex flex-col items-start gap-4">
<Button
onClick={() => setValue(["item-1", "item-2"])}
variant="outline"
>
Open First Two
</Button>
<p className="text-muted-foreground text-sm">
Open items: {value.length > 0 ? value.join(", ") : "None"}
</p>
</div>
</div>
);
}
Plus/Minus Indicators
Replace the default chevron with a + / − icon pair that clearly communicates the expand and collapse actions.
import { Minus, Plus } from "lucide-react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
const items = [
{
content:
"We use industry-standard AES-256 encryption to protect your sensitive information at rest and in transit.",
trigger: "Data Security",
value: "security",
},
{
content:
"Seamlessly connect with your favorite tools using our robust REST API and pre-built connectors.",
trigger: "API Integration",
value: "integration",
},
{
content:
"Invite team members, assign roles, and work together in real-time on shared projects and documents.",
trigger: "Team Collaboration",
value: "collaboration",
},
];
export default function Pattern() {
return (
<div className="mx-auto w-full max-w-sm">
<Accordion defaultValue={["security"]}>
{items.map((item) => (
<AccordionItem key={item.value} value={item.value}>
<AccordionTrigger
className="hover:no-underline"
icon={
<div className="flex h-7 w-7 items-center justify-center">
<Plus className="in-data-open:hidden" size={16} />
<Minus className="in-data-open:block hidden" size={16} />
</div>
}
>
<span>{item.trigger}</span>
</AccordionTrigger>
<AccordionContent>{item.content}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
}
Bordered
Adds individual item borders and rounded corners for a card-like appearance suited to settings or FAQ sections.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
const items = [
{
content:
"We offer monthly and annual subscription plans. Billing is charged at the beginning of each cycle, and you can cancel anytime. All plans include automatic backups, 24/7 support, and unlimited team members. There are no hidden fees or setup costs.",
trigger: "How does billing work?",
value: "billing",
},
{
content:
"Yes. We use end-to-end encryption, SOC 2 Type II compliance, and regular third-party security audits. All data is encrypted at rest and in transit using industry-standard protocols. We also offer optional two-factor authentication and single sign-on for enterprise customers.",
trigger: "Is my data secure?",
value: "security",
},
{
content: (
<>
<p>
We integrate with 500+ popular tools including Slack, Zapier,
Salesforce, HubSpot, and more. You can also build custom integrations
using our REST API and webhooks.{" "}
</p>
<p>
Our API documentation includes code examples in 10+ programming
languages.
</p>
</>
),
trigger: "What integrations do you support?",
value: "integration",
},
];
export function Pattern() {
return (
<div className="mx-auto mb-auto w-full max-w-lg">
<Accordion
className="space-y-2 border-0"
defaultValue={["billing"]}
multiple={false}
>
{items.map((item) => (
<AccordionItem
className="rounded-lg border border-border not-last:border-b px-3"
key={item.value}
value={item.value}
>
<AccordionTrigger className="items-center py-3 font-medium hover:no-underline">
{item.trigger}
</AccordionTrigger>
<AccordionContent className="pt-0 pb-4 text-muted-foreground">
{item.content}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
}
In Card
Wraps the accordion inside a Card component so it inherits the card's border, padding, and background.
Annual billing is available with a 20% discount. All plans include a 14-day free trial with no credit card required.
import { ArrowUpRightIcon } from "lucide-react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
const items = [
{
content: (
<>
<p>
<a className="text-primary hover:underline" href="#">
Annual billing is available
</a>{" "}
with a 20% discount. All plans include a 14-day free trial with no
credit card required.
</p>
<Button className="mt-4" size="sm">
View plans
<ArrowUpRightIcon className="size-4" />
</Button>
</>
),
trigger: "What subscription plans do you offer?",
value: "plans",
},
{
content: (
<>
<p>
Billing occurs automatically at the start of each billing cycle. We
accept all major credit cards, PayPal, and ACH transfers for
enterprise customers.
</p>
</>
),
trigger: "How does billing work?",
value: "billing",
},
{
content: (
<>
<p>
We take security seriously. All data is encrypted at rest using
AES-256 and in transit via TLS 1.3. We perform regular third-party
security audits and maintain SOC 2 Type II compliance.
</p>
<p>
You can also enable multi-factor authentication (MFA) and single
sign-on (SSO) for additional security.
</p>
</>
),
trigger: "Is my data secure?",
value: "security",
},
];
export function Pattern() {
return (
<div className="mx-auto mb-auto w-full max-w-lg">
<Card>
<CardHeader>
<CardTitle>Subscription & Billing</CardTitle>
<CardDescription>
Common questions about your account, plans, and payments
</CardDescription>
</CardHeader>
<CardContent>
<Accordion defaultValue={["plans"]} multiple>
{items.map((item) => (
<AccordionItem key={item.value} value={item.value}>
<AccordionTrigger>{item.trigger}</AccordionTrigger>
<AccordionContent>{item.content}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</CardContent>
</Card>
</div>
);
}
Nested
Demonstrates accordion items that contain a second-level accordion, each level visually indented and bordered.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
const nestedItems = [
{
content:
"Detailed technical specs including dimensions, weight, and power requirements.",
trigger: "Technical Specifications",
value: "sub-item-1",
},
{
content:
"List of supported devices and operating systems for this product.",
trigger: "Compatibility",
value: "sub-item-2",
},
];
const mainItems = [
{
content:
"This product is designed for high-performance enterprise environments requiring maximum reliability.",
trigger: "Product Overview",
value: "product-info",
},
{
isNested: true,
trigger: "Additional Details",
value: "details",
},
{
content:
"Free standard shipping on orders over $500. 30-day return policy applies.",
trigger: "Shipping & Returns",
value: "shipping",
},
];
export function Pattern() {
return (
<div className="mx-auto mb-auto w-full max-w-lg">
<Accordion
className="space-y-2 border-none"
defaultValue={["details"]}
multiple={false}
>
{mainItems.map((item) => (
<AccordionItem
className="rounded-lg border border-border bg-transparent px-4"
key={item.value}
value={item.value}
>
<AccordionTrigger className="items-center py-3 font-medium hover:no-underline">
{item.trigger}
</AccordionTrigger>
<AccordionContent className="h-auto text-muted-foreground">
{item.isNested ? (
<Accordion
className="space-y-2 border-none"
defaultValue={["sub-item-1"]}
multiple={false}
>
{nestedItems.map((subItem) => (
<AccordionItem
className="rounded-lg border border-border bg-transparent px-3"
key={subItem.value}
value={subItem.value}
>
<AccordionTrigger className="items-center py-3 font-medium text-foreground hover:no-underline">
{subItem.trigger}
</AccordionTrigger>
<AccordionContent className="text-sm">
{subItem.content}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
) : (
item.content
)}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
}
With User List
Each item header shows a user avatar, name, and role badge. Expanding the item reveals additional profile details.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Frame, FramePanel } from "@/components/ui/frame";
const users = [
{
avatar:
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
content:
"Alex has full administrative access to the platform, including billing management, user provisioning, and security configurations.",
email: "alex@apple.com",
id: "1",
initials: "AJ",
name: "Alex Johnson",
role: "Admin",
},
{
avatar:
"https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
content:
"Sarah has read-only access to projects and reports. She cannot modify settings or invite new members.",
email: "sarah@openai.com",
id: "2",
initials: "SC",
name: "Sarah Chen",
role: "Viewer",
},
{
avatar:
"https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
content:
"Michael is part of the design team and has permissions to edit projects, manage assets, and update design system components.",
email: "michael@meta.com",
id: "3",
initials: "MR",
name: "Michael Rodriguez",
role: "Editor",
},
];
export function Pattern() {
return (
<div className="mx-auto mb-auto w-full max-w-lg">
<Frame>
{users.map((user) => (
<FramePanel key={user.id}>
<Accordion
className="border-none"
defaultValue={["1"]}
multiple={false}
>
<AccordionItem
className="border-none bg-transparent p-0 **:data-[slot=accordion-content]:p-0!"
value={user.id}
>
<AccordionTrigger className="items-center px-1 py-1 hover:no-underline">
<div className="flex items-center gap-2">
<Avatar className="size-8 border">
<AvatarImage alt={user.name} src={user.avatar} />
<AvatarFallback className="text-xs">
{user.initials}
</AvatarFallback>
</Avatar>
<div className="inline-flex items-center gap-2">
<span className="font-semibold text-foreground/90 tracking-tight">
{user.name}
</span>
<Badge
size="sm"
variant={
user.role === "Admin" ? "success" : "secondary"
}
>
{user.role}
</Badge>
</div>
</div>
</AccordionTrigger>
<AccordionContent className="py-0 pl-11 text-muted-foreground">
{user.content}
</AccordionContent>
</AccordionItem>
</Accordion>
</FramePanel>
))}
</Frame>
</div>
);
}
Changelog
Versioned release entries with a monospace version string, "Latest" and "Breaking" badges inline in the trigger, and a bullet list of color-coded change types in the panel.
Changelog
4 releases- newIntroduced real-time collaboration with live cursors
- newAdded webhook support for all resource events
- improvedDashboard load time reduced by 60% with incremental rendering
- fixedResolved token refresh race condition on simultaneous requests
//biome-ignore-all lint/suspicious/noArrayIndexKey: <>
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge";
type ChangeType = "new" | "improved" | "fixed" | "breaking";
const changeVariant: Record<
ChangeType,
"default" | "info" | "success" | "destructive"
> = {
breaking: "destructive",
fixed: "success",
improved: "info",
new: "default",
};
const releases = [
{
changes: [
{
label: "new",
text: "Introduced real-time collaboration with live cursors",
},
{ label: "new", text: "Added webhook support for all resource events" },
{
label: "improved",
text: "Dashboard load time reduced by 60% with incremental rendering",
},
{
label: "fixed",
text: "Resolved token refresh race condition on simultaneous requests",
},
],
date: "May 22, 2025",
isLatest: true,
value: "v3-2-0",
version: "v3.2.0",
},
{
changes: [
{
label: "improved",
text: "Overhauled settings UI for better discoverability",
},
{
label: "improved",
text: "CSV export now supports custom column ordering",
},
{
label: "fixed",
text: "Fixed pagination offset bug on filtered table views",
},
{
label: "fixed",
text: "Corrected timezone handling in scheduled reports",
},
],
date: "Apr 10, 2025",
isLatest: false,
value: "v3-1-2",
version: "v3.1.2",
},
{
changes: [
{
label: "breaking",
text: "Removed legacy /v1/users endpoint — migrate to /v2/users",
},
{
label: "new",
text: "Role-based access control with custom permission sets",
},
{
label: "new",
text: "Two-factor authentication via TOTP and hardware keys",
},
{
label: "improved",
text: "API rate limit headers now returned on every response",
},
],
date: "Feb 28, 2025",
isLatest: false,
value: "v3-0-0",
version: "v3.0.0",
},
{
changes: [
{
label: "fixed",
text: "File uploads no longer fail silently on network timeout",
},
{
label: "fixed",
text: "Resolved mobile layout overflow on narrow viewports",
},
],
date: "Jan 15, 2025",
isLatest: false,
value: "v2-9-4",
version: "v2.9.4",
},
];
export function Pattern() {
return (
<div className="mx-auto w-full max-w-lg">
<div className="mb-4 flex items-center justify-between">
<h2 className="font-semibold text-sm">Changelog</h2>
<span className="text-muted-foreground text-xs">
{releases.length} releases
</span>
</div>
<Accordion defaultValue={["v3-2-0"]} multiple>
{releases.map((release) => (
<AccordionItem key={release.value} value={release.value}>
<AccordionTrigger className="py-3 hover:no-underline">
<div className="flex items-center gap-2.5">
<span className="font-mono font-semibold text-sm">
{release.version}
</span>
{release.isLatest && (
<Badge size="sm" variant="success">
Latest
</Badge>
)}
{release.changes.some((c) => c.label === "breaking") && (
<Badge size="sm" variant="destructive">
Breaking
</Badge>
)}
<span className="font-normal text-muted-foreground text-xs">
{release.date}
</span>
</div>
</AccordionTrigger>
<AccordionContent className="pb-4">
<ul className="space-y-2">
{release.changes.map((change, i) => (
<li className="flex items-start gap-2.5" key={i}>
<Badge
className="mt-px shrink-0 capitalize"
size="sm"
variant={changeVariant[change.label as ChangeType]}
>
{change.label}
</Badge>
<span className="text-sm leading-snug">{change.text}</span>
</li>
))}
</ul>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
}
Searchable FAQ
A live keyword filter input sits above the accordion. Typing narrows items by matching against both the question and the answer text, with an empty state when no results match.
Frequently Asked Questions
Search or browse answers below.
"use client";
import { SearchIcon } from "lucide-react";
import { useState } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Input } from "@/components/ui/input";
const faqs = [
{
content:
'Go to the login page and click "Forgot password". You\'ll receive a reset link within a few minutes. The link expires after 24 hours.',
trigger: "How do I reset my password?",
value: "q1",
},
{
content:
"Yes, you can upgrade or downgrade at any time from account settings. Changes take effect at the start of your next billing cycle.",
trigger: "Can I change my subscription plan?",
value: "q2",
},
{
content:
"All new accounts start with a 14-day free trial on the Pro plan. No credit card is required to start.",
trigger: "Is there a free trial available?",
value: "q3",
},
{
content:
"Navigate to Settings > Data & Privacy > Export. You can export in CSV or JSON format. Large exports may take a few minutes.",
trigger: "How do I export my data?",
value: "q4",
},
{
content:
"We accept all major credit cards (Visa, Mastercard, AmEx), PayPal, and bank transfers for annual enterprise plans.",
trigger: "What payment methods do you accept?",
value: "q5",
},
{
content:
'From your dashboard, go to Settings > Team, then click "Invite Member". Enter their email and select a role. They\'ll receive an invite within minutes.',
trigger: "How do I invite team members?",
value: "q6",
},
];
export function Pattern() {
const [query, setQuery] = useState("");
const filtered = faqs.filter(
(f) =>
f.trigger.toLowerCase().includes(query.toLowerCase()) ||
f.content.toLowerCase().includes(query.toLowerCase()),
);
return (
<div className="mx-auto w-full max-w-lg">
<div className="mb-4 flex flex-col gap-1">
<h2 className="font-semibold text-lg">Frequently Asked Questions</h2>
<p className="text-muted-foreground text-sm">
Search or browse answers below.
</p>
</div>
<div className="relative mb-4">
<SearchIcon className="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
className="pl-9"
onChange={(e) => setQuery(e.target.value)}
placeholder="Search questions…"
value={query}
/>
</div>
{filtered.length === 0 ? (
<p className="py-8 text-center text-muted-foreground text-sm">
No results for "{query}"
</p>
) : (
<Accordion className="w-full" multiple>
{filtered.map((faq) => (
<AccordionItem key={faq.value} value={faq.value}>
<AccordionTrigger>{faq.trigger}</AccordionTrigger>
<AccordionContent className="text-muted-foreground">
{faq.content}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
</div>
);
}
Settings Navigation
Accordion used as a collapsible sidebar navigation. Each section has an icon and label in the trigger, and expands to reveal indented links with a left border rail.
import { BellIcon, CreditCardIcon, ShieldIcon, UserIcon } from "lucide-react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
const sections = [
{
icon: UserIcon,
label: "Account",
links: ["Profile", "Display Name", "Email Address", "Language & Region"],
value: "account",
},
{
icon: ShieldIcon,
label: "Security",
links: ["Password", "Two-Factor Auth", "Active Sessions", "Login History"],
value: "security",
},
{
icon: BellIcon,
label: "Notifications",
links: [
"Email Alerts",
"Push Notifications",
"Digest Frequency",
"Do Not Disturb",
],
value: "notifications",
},
{
icon: CreditCardIcon,
label: "Billing",
links: [
"Subscription Plan",
"Payment Methods",
"Invoices",
"Usage & Limits",
],
value: "billing",
},
];
export function Pattern() {
return (
<div className="mx-auto w-full max-w-xs">
<p className="mb-3 font-semibold text-muted-foreground text-xs uppercase tracking-widest">
Settings
</p>
<Accordion defaultValue={["account"]} multiple>
{sections.map(({ icon: Icon, label, value, links }) => (
<AccordionItem className="border-none" key={value} value={value}>
<AccordionTrigger className="rounded-md px-2 py-2 hover:bg-accent hover:no-underline">
<div className="flex items-center gap-2.5">
<Icon className="size-4 text-muted-foreground" />
<span className="font-medium text-sm">{label}</span>
</div>
</AccordionTrigger>
<AccordionContent className="pb-1">
<ul className="ml-6 flex flex-col gap-0.5 border-l pl-3">
{links.map((link) => (
<li key={link}>
<a
className="block rounded-sm py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground"
href="#"
>
{link}
</a>
</li>
))}
</ul>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
}
Job Board Listings
Each item shows a job title, department, location, and employment-type badge in the trigger. Expanding reveals a description, requirements list, salary range, and an Apply Now button.
Open Positions
3 rolesimport { BriefcaseIcon, MapPinIcon } from "lucide-react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
const jobs = [
{
department: "Engineering",
description:
"Lead the development of our design system and core UI components, working closely with product and design to ship scalable, accessible interfaces.",
location: "Remote · Worldwide",
requirements: [
"5+ years React experience",
"TypeScript proficiency",
"Experience with design systems",
],
salary: "$140k – $180k",
title: "Senior Frontend Engineer",
type: "Full-time",
value: "job-1",
},
{
department: "Design",
description:
"Shape the end-to-end experience of our core product. Own flows from research to high-fidelity prototypes and collaborate with engineers on implementation.",
location: "San Francisco, CA",
requirements: [
"3+ years product design",
"Figma proficiency",
"Strong portfolio",
],
salary: "$120k – $155k",
title: "Product Designer",
type: "Full-time",
value: "job-2",
},
{
department: "Marketing",
description:
"Be the bridge between our product and the developer community. Write docs, create tutorials, speak at conferences, and gather feedback to improve DX.",
location: "Remote · US",
requirements: [
"Public speaking experience",
"Technical writing skills",
"Developer community background",
],
salary: "$100k – $130k",
title: "Developer Advocate",
type: "Full-time",
value: "job-3",
},
];
export function Pattern() {
return (
<div className="mx-auto w-full max-w-2xl">
<div className="mb-4 flex items-center justify-between">
<h2 className="font-semibold text-base">Open Positions</h2>
<Badge variant="secondary">{jobs.length} roles</Badge>
</div>
<Accordion className="w-full space-y-2 border-none" multiple>
{jobs.map((job) => (
<AccordionItem
className="rounded-lg border px-4 last:border-b"
key={job.value}
value={job.value}
>
<AccordionTrigger className="hover:no-underline">
<div className="flex flex-col items-start gap-1 text-left">
<span className="font-semibold text-sm">{job.title}</span>
<div className="flex flex-wrap items-center gap-2 text-muted-foreground text-xs">
<span className="flex items-center gap-1">
<BriefcaseIcon className="size-3" />
{job.department}
</span>
<span className="flex items-center gap-1">
<MapPinIcon className="size-3" />
{job.location}
</span>
<Badge size="sm" variant="secondary">
{job.type}
</Badge>
</div>
</div>
</AccordionTrigger>
<AccordionContent className="pb-4">
<p className="mb-3 text-muted-foreground text-sm">
{job.description}
</p>
<ul className="mb-4 list-disc space-y-1 pl-4 text-muted-foreground text-sm">
{job.requirements.map((r) => (
<li key={r}>{r}</li>
))}
</ul>
<div className="flex items-center justify-between">
<span className="font-medium text-sm">{job.salary}</span>
<Button size="sm">Apply Now</Button>
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
}
Onboarding Step Tracker
A controlled accordion that tracks completed steps. Finished steps display a green numbered circle with a check mark and a tinted background; clicking "Mark as complete" advances to the next step.
Getting started
1 of 4 steps completed
Add teammates by email. They'll receive an invite link and can join immediately.
"use client";
import { CheckIcon } from "lucide-react";
import { useState } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
const steps = [
{
description:
"Give your workspace a name and upload a logo. This is how your team will identify the space.",
title: "Create your workspace",
value: "step-1",
},
{
description:
"Add teammates by email. They'll receive an invite link and can join immediately.",
title: "Invite your team",
value: "step-2",
},
{
description:
"Integrate with Slack, GitHub, Jira, and 50+ other tools. Connections can be updated anytime.",
title: "Connect your tools",
value: "step-3",
},
{
description:
"Add a payment method to keep access after your 14-day trial ends. Cancel anytime.",
title: "Set up billing",
value: "step-4",
},
];
export function Pattern() {
const [completed, setCompleted] = useState<string[]>(["step-1"]);
const [opened, setOpened] = useState<string[]>(["step-2"]);
const markDone = (value: string, index: number) => {
setCompleted((prev) => [...prev, value]);
const next = steps[index + 1];
if (next) setOpened([next.value]);
};
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-4">
<h2 className="font-semibold text-base">Getting started</h2>
<p className="text-muted-foreground text-sm">
{completed.length} of {steps.length} steps completed
</p>
</div>
<Accordion
className="w-full space-y-2 border-none"
multiple
onValueChange={setOpened}
value={opened}
>
{steps.map((step, i) => {
const done = completed.includes(step.value);
return (
<AccordionItem
className={`rounded-lg border px-4 last:border-b ${
done
? "border-emerald-200 bg-emerald-50/50 dark:border-emerald-900 dark:bg-emerald-950/30"
: ""
}`}
key={step.value}
value={step.value}
>
<AccordionTrigger className="hover:no-underline">
<div className="flex items-center gap-3">
<div
className={`flex size-6 shrink-0 items-center justify-center rounded-full font-semibold text-xs ${
done
? "bg-emerald-500 text-white"
: "border-2 bg-background text-muted-foreground"
}`}
>
{done ? <CheckIcon className="size-3.5" /> : i + 1}
</div>
<span className="font-medium text-sm">{step.title}</span>
</div>
</AccordionTrigger>
<AccordionContent className="pb-4 pl-9">
<p className="mb-3 text-muted-foreground text-sm">
{step.description}
</p>
{!done && (
<Button onClick={() => markDone(step.value, i)} size="sm">
Mark as complete
</Button>
)}
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</div>
);
}
Product Specifications
Specification rows are grouped into collapsible categories (Display, Performance, Battery, Connectivity). Each panel renders a definition list with label and value columns.
Technical Specifications
MacBook Pro 14″- Screen Size
- 14.2-inch Liquid Retina XDR
- Resolution
- 3024 × 1964 at 254 ppi
- Brightness
- 1000 nits sustained (full-screen)
- Refresh Rate
- ProMotion adaptive 24Hz to 120Hz
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge";
const specs = [
{
category: "Display",
rows: [
["Screen Size", "14.2-inch Liquid Retina XDR"],
["Resolution", "3024 × 1964 at 254 ppi"],
["Brightness", "1000 nits sustained (full-screen)"],
["Refresh Rate", "ProMotion adaptive 24Hz to 120Hz"],
],
value: "display",
},
{
category: "Performance",
rows: [
["Chip", "Apple M4 Pro"],
["CPU Cores", "14-core (10 performance, 4 efficiency)"],
["GPU Cores", "20-core"],
["Memory", "24 GB unified memory"],
["Storage", "512 GB SSD"],
],
value: "performance",
},
{
category: "Battery & Power",
rows: [
["Battery Life", "Up to 22 hours video playback"],
["Battery Capacity", "72.4-watt-hour lithium-polymer"],
["Fast Charge", "0–50% in ~30 min with 96W+ adapter"],
["Charger", "96W USB-C Power Adapter (included)"],
],
value: "battery",
},
{
category: "Connectivity",
rows: [
["Wi-Fi", "Wi-Fi 6E (802.11ax)"],
["Bluetooth", "5.3"],
["Ports", "3× Thunderbolt 4, HDMI, SD card, MagSafe 3"],
],
value: "connectivity",
},
];
export function Pattern() {
return (
<div className="mx-auto w-full max-w-lg">
<div className="mb-4 flex items-center gap-2">
<h2 className="font-semibold text-base">Technical Specifications</h2>
<Badge size="sm" variant="secondary">
MacBook Pro 14″
</Badge>
</div>
<Accordion className="w-full" defaultValue={["display"]} multiple>
{specs.map((spec) => (
<AccordionItem key={spec.value} value={spec.value}>
<AccordionTrigger className="font-medium">
{spec.category}
</AccordionTrigger>
<AccordionContent>
<dl className="divide-y text-sm">
{spec.rows.map(([label, value]) => (
<div className="flex items-baseline gap-4 py-2.5" key={label}>
<dt className="w-36 shrink-0 text-muted-foreground">
{label}
</dt>
<dd className="text-foreground">{value}</dd>
</div>
))}
</dl>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
}

