Editorial Accordion
Big type, fine rules, and room to breathe. A more considered way to tell the story.
THE ART OF THE DETAILS
Small details.
Big difference.
Copy the source. Change every detail. Make something only you could make.
Considered interactions, with respect for your visitors’ motion preferences.
Typed React components, ordinary CSS, and no animation dependencies.
Live component. Go ahead, interact.
REACT / TYPESCRIPT
'use client'
import { useId, useState } from 'react'
import type { ReactNode } from 'react'
import './editorial-accordion.css'
export interface EditorialAccordionItem {
/** A unique, stable identifier within this accordion. */
id: string
title: string
content: ReactNode
}
export interface EditorialAccordionProps {
items: EditorialAccordionItem[]
defaultValue?: string
className?: string
}
export function EditorialAccordion({
items,
defaultValue,
className = '',
}: EditorialAccordionProps) {
const [openId, setOpenId] = useState<string | undefined>(defaultValue)
const instanceId = useId()
return (
<div className={`ac-editorial-accordion ${className}`.trim()}>
{items.map((item, index) => {
const isOpen = item.id === openId
const buttonId = `${instanceId}-trigger-${index}`
const panelId = `${instanceId}-panel-${index}`
return (
<section className="ac-editorial-accordion-item" data-open={isOpen} key={item.id}>
<h3 className="ac-editorial-accordion-heading">
<button
id={buttonId}
className="ac-editorial-accordion-trigger"
type="button"
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => setOpenId(isOpen ? undefined : item.id)}
>
<span className="ac-editorial-accordion-number" aria-hidden="true">
{String(index + 1).padStart(2, '0')}
</span>
<span className="ac-editorial-accordion-title">{item.title}</span>
<span className="ac-editorial-accordion-icon" aria-hidden="true" />
</button>
</h3>
<div
id={panelId}
className="ac-editorial-accordion-panel"
role="region"
aria-labelledby={buttonId}
hidden={!isOpen}
>
<div className="ac-editorial-accordion-content">{item.content}</div>
</div>
</section>
)
})}
</div>
)
}
01
Installation
Run this in your shadcn-configured React project. The component and stylesheet are added together.
TERMINAL
npx shadcn@latest add http://localhost:5173/r/editorial-accordion.jsonThis command uses the current site address. For localhost, keep this preview running on the same computer. Files follow your configured components alias.
02
Usage
Import the component and make the details your own.
TypeScript
import { EditorialAccordion } from '@/components/anticraft/EditorialAccordion';
export function Example() {
return (
<EditorialAccordion
defaultValue="source"
items={[
{ id: 'source', title: 'Make it your own', content: 'The source is yours to change.' },
{ id: 'license', title: 'Free to ship', content: 'Use it in personal and commercial projects.' },
]}
/>
);
}03
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | EditorialAccordionItem[] | Required | Items with a unique id, string title, and ReactNode content. |
defaultValue | string | undefined | The id of the item that starts open. At most one item is open at a time. |
className | string | '' | Additional classes for the accordion. |
04
Motion & accessibility
Considered by default.
Uses native buttons with expanded state and linked content regions. Reduced motion removes the icon and text transitions.
Use semantic content and meaningful labels in your composition. Check contrast and keyboard behavior with your own styles and content.