The Accordion is typically used for displaying collapsible content like FAQs, documentation sections, or grouped settings. It helps keep interfaces clean and focused by allowing users to toggle visibility without overwhelming them with information.
With Ark UI, you get unstyled but accessible building blocks. This means you have full control and can style the
accordion to match your design system by simply targeting the right data-scope
and data-part
attributes.
In this post, you’ll learn how to:
- Define Base Styles: Set the default and foundational styles for each part of the accordion.
- Add Size Variants: Support small, medium, and large accordions by adjusting padding, font size, and spacing to fit different layouts and use cases.
- Customize Visual Variants: Create variants like
outline
,subtle
, andenclosed
to match your design system’s look and feel. - Style Disabled States: Apply styles that visually indicate when specific accordion items are unclickable or inactive.
- Add an Action Button: Place actionable elements like buttons inside the accordion trigger for improved user interaction.
By the end, you’ll have a flexible, scalable pattern for styling accordions that fit into your design system.
Anatomy
Before we dive into specific styles, it's essential to understand the parts that make up the Ark UI Accordion component.
Each part supports the data-scope
and data-part
attributes, which you can target in both Vanilla CSS and Panda CSS.
root
– the wrapper that contains all accordion items.item
– represents a single section in the accordion.item-trigger
– The interactive header of each accordion item that toggles the accordion open or closed.item-indicator
– Typically an icon placed within the trigger that visually shows when the accordion is expanded or collapsed.item-content
– The section that expands or collapses to show or hide content.
Basic Usage
Here's a simple example of how to use the Accordion
component in your code:
import { Accordion } from '@ark-ui/react/accordion'
import { ChevronDownIcon } from 'lucide-react'
export const Basic = () => {
return (
<Accordion.Root defaultValue={['React']}>
{['React', 'Solid'].map((item) => (
<Accordion.Item key={item} value={item}>
<Accordion.ItemTrigger>
What is {item}?
<Accordion.ItemIndicator>
<ChevronDownIcon />
</Accordion.ItemIndicator>
</Accordion.ItemTrigger>
<Accordion.ItemContent>{item} is a JavaScript library for building user interfaces.</Accordion.ItemContent>
</Accordion.Item>
))}
</Accordion.Root>
)
}
From the above code, it is worth noting that:
- We have two accordion items (
React
andSolid
) - Only one item (
React
) is open by default
When rendered, the structure looks like this in the DOM:
<div data-scope="accordion" data-part="root">
<div data-part="item" data-state="open">
<button data-part="item-trigger">
What is React?
<span data-part="item-indicator">▼</span>
</button>
<div data-part="item-content">
React is a JavaScript library for building user interfaces.
</div>
</div>
<!-- Other accordion item here -->
</div>
Each part of the accordion component comes with data attributes like:
data-scope="accordion"
which scopes all accordion parts under theaccordion
namespace, ensuring styles don't clash with other components.data-part="..."
breaks the component down into its internal parts so you can style them individually such asdata-part="item”
,data-part="item-trigger”
and more.data-state="..."
tells you what state a component is in. For example,data-state="open"
means the item is currently expanded
Styling with Vanilla CSS
When styling the Accordion component in a design system using Vanilla CSS, it’s helpful to start thinking in recipes.
A recipe is a structured approach to defining styles for all parts of a component, including its base styles, size variants, and visual variants.
With a recipe for Accordion, you can:
- Set up base styles that apply to every accordion instance (like spacing, layout, and transitions).
- Add size variants (
sm
,md
,lg
) that adjust padding, font sizes, and spacing. - Define visual variants like
outline
,subtle
, orenclosed
, each with a different look and feel.
Base Styles
We’ll start by defining foundational styles for each part of the Accordion
component using Ark UI’s attributes like
data-scope="accordion"
and data-part="..."
.
Before defining the base styles for the Accordion, make sure your project includes the shared global styles we use across the design system, such as reset styles, tokens, keyframes, and base button styles.
If you haven’t set that up yet, head over to the Design System Intro to see how to configure your global styles directory.
Styling the Accordion Root and Items
Let's define the base styles of the accordion. Add the following styles to your accordion.css
stylesheet:
[data-scope='accordion'][data-part='root'] {
width: 100%;
border-radius: var(--accordion-radius);
}
[data-scope='accordion'][data-part='item'] {
overflow-anchor: none;
}
The CSS variable --accordion-radius
makes the root
flexible to customize across different sizes.
Applying overflow-anchor: none
to the item
prevents scroll anchoring issues, which can occur when an item is
expanded or collapsed.
Styling the Item Trigger
The item-trigger
is the clickable header that users interact with to expand or collapse content. To style it, add the
following styles to your accordion.css
stylesheet:
[data-scope='accordion'][data-part='item-trigger'] {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 0.75rem;
font-weight: 500;
text-align: start;
outline: 0;
border-radius: var(--accordion-radius);
padding: var(--accordion-padding-y) var(--accordion-padding-x);
background: transparent;
}
[data-scope='accordion'][data-part='item-trigger']:focus-visible {
outline: 2px solid var(--accordion-focus-ring, #3b82f6);
}
[data-scope='accordion'][data-part='item-trigger']:disabled {
opacity: 0.5;
cursor: not-allowed;
}
Let's break down the CSS properties applied:
- The trigger spans the
width: 100%
, has afont-weight
of500
and other responsive styles. These create a horizontally aligned layout. - The
:focus-visible
pseudo-class is an accessibility feature. Unlike:focus
(which applies on click and keyboard focus),:focus-visible
only triggers when the element is focused via keyboard navigation. - The
:disabled
pseudo-class allows you to apply styles that visually communicate when the accordion items are not clickable.
Styling the Item Content
The item-content
is the container that expands and collapses to reveal or hide the content of an accordion item.
Add the following styles to the stylesheet.
[data-scope='accordion'][data-part='item-content'] {
overflow: hidden;
border-radius: var(--accordion-radius);
padding-inline: var(--accordion-padding-x);
}
CSS variables make it easy to adjust spacing across size variants.
While the above styles define its static appearance, we need to add smooth transitions to the item-content
for
improved visual experience of your component.
Inside your global keyframes.css
stylesheet, define the animations:
@keyframes expand-height {
from {
height: 0;
}
to {
height: var(--height);
}
}
@keyframes collapse-height {
from {
height: var(--height);
}
to {
height: 0;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
Now, use the data-state='open'
and data-state='closed'
, to add beautiful entry and exit animations to the item
content.
[data-scope='accordion'][data-part='item-content'][data-state='open'] {
animation-name: expand-height, fade-in;
animation-duration: 200ms;
}
[data-scope='accordion'][data-part='item-content'][data-state='closed'] {
animation-name: collapse-height, fade-out;
animation-duration: 200ms;
}
Styling the Indicator
The indicator typically is an arrow or chevron icon that rotates when the accordion item is expanded or collapsed.
[data-scope='accordion'][data-part='item-indicator'] {
transition: rotate 0.2s;
transform-origin: center;
color: var(--accordion-text-subtle, #a1a1aa);
}
[data-scope='accordion'][data-part='item-indicator'][data-state='open'] {
transform: rotate(180deg);
}
[data-scope='accordion'][data-part='item-indicator'] svg {
width: 1.2em;
height: 1.2em;
}
These styles animate the indicator's rotation smoothly from its center and rotates the icon 180° when the accordion is open.
Styling the Item Body (Custom Part)
Although Ark UI doesn’t include an item-body
part by default, we’ll create this part ourselves when applying styles.
The item body serves as a wrapper around the content inside each accordion item. Let's style it:
[data-scope='accordion'][data-part='item-body'] {
padding-top: var(--accordion-padding-y);
padding-bottom: calc(var(--accordion-padding-y) * 2);
}
This custom item-body
helps you gain more control over vertical spacing inside your accordion content.
Visual Variants
A flexible design system supports different visual variants to fit design needs. We’ll define the styles for three
accordion variants: outline
, subtle
, and enclosed
.
Outline
The outline
variant adds a clean separator between items using a bottom border.

This works well for minimalist interfaces. In your accordion.css
stylesheet, add the following styles:
.accordion--variant-outline [data-scope='accordion'][data-part='item'] {
border-bottom: 1px solid var(--accordion-border, #e2e8f0);
}
Subtle
The subtle
variant uses a soft background and padding to distinguish accordion items.

.accordion--variant-subtle {
[data-scope='accordion'][data-part='item-trigger'],
[data-scope='accordion'][data-part='item-content'] {
padding-inline: var(--accordion-padding-x);
}
[data-scope='accordion'][data-part='item'] {
border-radius: var(--accordion-radius);
}
[data-scope='accordion'][data-part='item'][data-state='open'] {
background-color: var(--accordion-bg-subtle, #f4f4f5);
}
}
Enclosed
The enclosed
variant visually groups all accordion items inside a bordered box, giving card-like structure.

.accordion--variant-enclosed [data-scope='accordion'][data-part='root'] {
border: 1px solid #e2e8f0;
border-radius: var(--accordion-radius);
overflow: hidden;
}
.accordion--variant-enclosed {
[data-scope='accordion'][data-part='item-trigger'],
[data-scope='accordion'][data-part='item-content'] {
padding-inline: var(--accordion-padding-x);
}
[data-scope='accordion'][data-part='item'] ~ [data-part='item'] {
border-top: 1px solid #e2e8f0;
}
[data-scope='accordion'][data-part='item'][data-state='open'] {
background-color: #fafafa;
}
}
This
~
rule adds a top border to every item that follows another item (i.e., not the first one). It creates a visual separation between accordion items inside the enclosed box, without doubling borders.
Size Variants
To make your Accordion responsive to different layouts, we’ll add small, medium and large variants using the sm
, md
,
and lg
classes.

Each size sets its own padding, border radius, and font size using CSS variables:
.accordion--size-sm {
--accordion-padding-x: 0.75rem;
--accordion-padding-y: 0.5rem;
--accordion-radius: 0.25rem;
}
.accordion--size-md {
--accordion-padding-x: 1rem;
--accordion-padding-y: 0.5rem;
--accordion-radius: 0.375rem;
}
.accordion--size-lg {
--accordion-padding-x: 1.5rem;
--accordion-padding-y: 0.75rem;
--accordion-radius: 0.5rem;
}
You can also customize the font size for the item trigger in each size variant:
.accordion--size-sm [data-part='item-trigger'] {
font-size: 0.875rem;
}
.accordion--size-md [data-part='item-trigger'] {
font-size: 1rem;
}
.accordion--size-lg [data-part='item-trigger'] {
font-size: 1.125rem;
}
Putting It All Together
We've covered how to define base styles using data-scope
and data-part
attributes, create scalable variants using
CSS custom properties, apply smooth animations, and even introduce custom parts like item-body
.
Here's what the complete accordion.css
stylesheet will look like:
/* Base Styles */
[data-scope='accordion'][data-part='root'] {
width: 100%;
border-radius: var(--accordion-radius);
}
[data-scope='accordion'][data-part='item'] {
overflow-anchor: none;
}
[data-scope='accordion'][data-part='item-trigger'] {
display: flex;
align-items: center;
justify-content: space-between;
text-align: start;
width: 100%;
outline: 0;
gap: 0.75rem;
font-weight: 500;
border-radius: var(--accordion-radius);
padding: var(--accordion-padding-y) var(--accordion-padding-x);
background: transparent;
&:focus-visible {
outline: 2px solid var(--accordion-focus-ring, #3b82f6);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
[data-scope='accordion'][data-part='item-content'] {
overflow: hidden;
border-radius: var(--accordion-radius);
padding-inline: var(--accordion-padding-x);
&[data-state='open'] {
animation-name: expand-height, fade-in;
animation-duration: 200ms;
}
&[data-state='closed'] {
animation-name: collapse-height, fade-out;
animation-duration: 200ms;
}
}
[data-scope='accordion'][data-part='item-body'] {
padding-top: var(--accordion-padding-y);
padding-bottom: calc(var(--accordion-padding-y) * 2);
}
[data-scope='accordion'][data-part='item-indicator'] {
transition: rotate 0.2s;
transform-origin: center;
color: var(--accordion-text-subtle, #a1a1aa);
&[data-state='open'] {
transform: rotate(180deg);
}
& svg {
width: 1.2em;
height: 1.2em;
}
}
/* Variants */
.accordion--variant-outline [data-scope='accordion'][data-part='item'] {
border-bottom: 1px solid var(--accordion-border, #e2e8f0);
}
.accordion--variant-subtle {
[data-scope='accordion'][data-part='item-trigger'],
[data-scope='accordion'][data-part='item-content'] {
padding-inline: var(--accordion-padding-x);
}
[data-scope='accordion'][data-part='item'] {
border-radius: var(--accordion-radius);
&[data-state='open'] {
background-color: var(--accordion-bg-subtle, #f4f4f5);
}
}
}
.accordion--variant-enclosed {
&[data-scope='accordion'][data-part='root'] {
border: 1px solid #e2e8f0;
border-radius: var(--accordion-radius);
overflow: hidden;
}
[data-scope='accordion'][data-part='item-trigger'],
[data-scope='accordion'][data-part='item-content'] {
padding-inline: var(--accordion-padding-x);
}
[data-scope='accordion'][data-part='item'] {
& ~ & {
border-top: 1px solid #e2e8f0;
}
&[data-state='open'] {
background-color: #fafafa;
}
}
}
/* Sizes */
.accordion--size-sm {
--accordion-padding-x: 0.75rem;
--accordion-padding-y: 0.5rem;
--accordion-radius: 0.25rem;
[data-scope='accordion'][data-part='item-trigger'] {
font-size: 0.875rem;
}
}
.accordion--size-md {
--accordion-padding-x: 1rem;
--accordion-padding-y: 0.5rem;
--accordion-radius: 0.375rem;
[data-scope='accordion'][data-part='item-trigger'] {
font-size: 1rem;
}
}
.accordion--size-lg {
--accordion-padding-x: 1.5rem;
--accordion-padding-y: 0.75rem;
--accordion-radius: 0.5rem;
[data-scope='accordion'][data-part='item-trigger'] {
font-size: 1.125rem;
}
}
Using The Vanilla CSS Classes in Your Component
Once you've defined your accordion styles in accordion.css
, you can apply them in your components by adding the
appropriate utility classes.
Let’s say you want a medium-sized, subtle accordion. You’d apply both classes to the root element:
import { Accordion } from '@ark-ui/react'
import { ChevronDownIcon } from 'lucide-react'
// Define the custom item body part
const AccordionItemBody = ({ children }: { children: React.ReactNode }) => (
<div data-scope="accordion" data-part="item-body">
{children}
</div>
)
// Define the full accordion component
export const VanillaCSSAccordion = () => {
return (
<Accordion.Root className="accordion--size-md accordion--variant-subtle" defaultValue="react">
<Accordion.Item value="react">
<Accordion.ItemTrigger>
What is React?
<Accordion.ItemIndicator>
<ChevronDownIcon />
</Accordion.ItemIndicator>
</Accordion.ItemTrigger>
<Accordion.ItemContent>
<AccordionItemBody>React is a JavaScript library for building UIs.</AccordionItemBody>
</Accordion.ItemContent>
</Accordion.Item>
</Accordion.Root>
)
}
To explore all the styles defined and see them in action, check out Storybook.
Styling with Panda CSS
If you're using Panda CSS, you can follow the same design system approach, but with a few key differences. Panda provides a recipe-based styling system that allows you to define styles for each part of a component in a structured way.
For the accordion, we’ll:
- Define base styles for the root, item, trigger, content, indicator, and custom parts like
item-body
- Apply interaction states such as
open
,disabled
, andfocus-visible
- Set up variants (
outline
,subtle
andenclosed
) - Set up size variants (
sm
,md
,lg
)
Creating the Accordion Recipe
To style each part of the Accordion component with Panda CSS, we'll define a
slot recipe using Panda’s sva()
utility. This allows us to scope
styles to all the individual parts of the Accordion like root
, item
, item-trigger
, item-content
,
item-indicator
, and more.
We also use accordionAnatomy.keys()
from Ark UI to automatically pull in all the part names for the Accordion
component.
Let’s start by setting up base styles for each slot. In your project, create an accordion.ts
file to hold the
accordion recipe.
Styling the Accordion Root and Item
The accordion root is the container that holds all items. Define the base styles in your Panda CSS recipe.
// src/recipes/accordion.ts
import { sva } from '../../styled-system/css'
import { accordionAnatomy } from '@ark-ui/react/accordion'
export const accordionRecipe = sva({
slots: accordionAnatomy(),
className: 'accordion',
base: {
root: {
width: 'full',
borderRadius: 'var(--accordion-radius)',
},
item: {
overflowAnchor: 'none',
},
},
})
The --accordion-radius
CSS variable ensures the component can easily scale across size variants.
Styling the Item Trigger
The itemTrigger
is the clickable header that users interact with to expand or collapse content.
itemTrigger: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
textAlign: 'start',
width: 'full',
outline: 0,
gap: '3',
fontWeight: 'medium',
borderRadius: 'var(--accordion-radius)',
padding: 'var(--accordion-padding-y) var(--accordion-padding-x)',
background: 'transparent',
_focusVisible: {
outline: '2px solid var(--accordion-focus-ring, #3b82f6)',
},
_disabled: {
opacity: 0.5,
cursor: 'not-allowed',
},
},
Let’s break down what each of these styles does:
- The trigger spans a full width, has a font weight of medium and contains other styles. These styles create a responsive and horizontally aligned layout.
_focusVisible
: Targets keyboard navigation focus only, adding a clear, accessible outline using the-accordion-focus-ring
variable or a fallback color._disabled
: Reduces opacity and disables pointer interaction when the trigger is in a disabled state.
Styling the Item Content
The itemContent
is the container that expands and collapses to reveal or hide the content of an accordion item.
itemContent: {
overflow: 'hidden',
borderRadius: 'var(--accordion-radius)',
paddingInline: 'var(--accordion-padding-x)',
},
The CSS variables make it easy to adjust spacing across size variants.
The core styles define its static appearance, however, we need to add smooth transitions for better maintaining the visual integrity of your component.
Inside your panda.config.ts
file, add some more keyframes to define the animations for the item content:
theme: {
extend: {
semanticTokens: {},
keyframes: {
...
'expand-height': {
'0%': { height: '0' },
'100%': { height: 'var(--height)' },
},
'collapse-height': {
'0%': { height: 'var(--height)' },
'100%': { height: '0' },
},
'fade-in': {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
'fade-out': {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
},
},
},
Now, use the _open
and _close
selectors, to add beautiful entry and exit animations to the item content.
itemContent: {
overflow: 'hidden',
borderRadius: 'var(--accordion-radius)',
paddingInline: 'var(--accordion-padding-x)',
_open: {
animationName: 'expand-height, fade-in',
animationDuration: '200ms',
},
_closed: {
animationName: 'collapse-height, fade-out',
animationDuration: '200ms',
},
},
Styling the Indicator
The indicator typically is an arrow or chevron icon that rotates based on whether the accordion item is expanded or collapsed. Add these styles to your accordion recipe:
itemIndicator: {
transition: 'rotate 0.2s',
transformOrigin: 'center',
color: 'fg.subtle',
_open: {
transform: 'rotate(180deg)',
},
_icon: {
width: '1.2em',
height: '1.2em',
},
},
The _icon
key ensures the SVG maintains consistent dimensions.
Styling the Item Body (Custom Part)
Although not provided by default in Ark UI, we’ll add a custom itemBody
part to manage spacing within item content. To
style this custom part, we’ll need to extend our accordion recipe.
Extending the Accordion Anatomy for Custom Parts
Panda CSS's recipe system is quite flexible. While accordionAnatomy.keys()
gives us all the standard parts exposed
by Ark UI, we can easily add our own by introducing a custom slot named itemBody
.
This explicitly tells Panda CSS to expect and generate styles for this new part, even though it's not part of Ark UI's default structure.
Add 'itemBody'
to the array of slots when defining our accordionRecipe
:
import { sva } from '../../styled-system/css'
import { accordionAnatomy } from '@ark-ui/react/accordion'
export const accordionRecipe = sva({
slots: [...accordionAnatomy.keys(), 'itemBody'],
className: 'accordion',
...
})
Now, add the base styles for the itemBody
import { sva } from '../../styled-system/css'
import { accordionAnatomy } from '@ark-ui/react/accordion'
export const accordionRecipe = sva({
slots: [...accordionAnatomy.keys(), 'itemBody'],
className: 'accordion',
base: {
itemBody: {
paddingTop: 'var(--accordion-padding-y)',
paddingBottom: 'calc(var(--accordion-padding-y) * 2)',
},
},
})
This custom itemBody
helps you gain more control over vertical spacing inside your accordion content.This helps
maintain consistent spacing between the trigger and content body.
Visual Variants
A flexible design system supports different visual variants to fit design needs. We’ll support the outline
, subtle
,
and enclosed
visual variants to align with different UI patterns.

Within the variants
key of your recipe, define the visual variants:
variants: {
variant: {
outline: {
item: {
borderBottom: '1px solid',
borderColor: 'border',
},
},
subtle: {
itemTrigger: {
paddingInline: 'var(--accordion-padding-x)',
},
itemContent: {
paddingInline: 'var(--accordion-padding-x)',
},
item: {
borderRadius: 'var(--accordion-radius)',
_open: {
background: 'bg.muted',
},
},
},
enclosed: {
root: {
border: '1px solid',
borderColor: 'border',
borderRadius: 'var(--accordion-radius)',
overflow: 'hidden',
},
itemTrigger: {
paddingInline: 'var(--accordion-padding-x)',
},
itemContent: {
paddingInline: 'var(--accordion-padding-x)',
},
item: {
'& + &': {
borderTop: '1px solid',
borderColor: 'border',
},
_open: {
background: 'bg.subtle',
},
},
},
},
- The
outline
variant adds a clean separator between items using a bottom border. This works well for minimalist interfaces. - The
subtle
variant uses a soft background and padding to distinguish accordion items. - The
enclosed
variant visually groups all accordion items inside a bordered box, giving card-like structure.
The
& + &
selector is a combinator that applies styles only to accordion items that are immediately preceded by another accordion item.
Size Variants
We also support sm
, md
, and lg
size variants. These define CSS variables that affect padding, border radius, and
font size.

Within the size
key of your recipe, define the size variants:
size: {
sm: {
root: {
'--accordion-padding-x': '0.75rem',
'--accordion-padding-y': '0.5rem',
'--accordion-radius': '0.25rem',
},
itemTrigger: {
fontSize: 'sm',
},
},
md: {
root: {
'--accordion-padding-x': '1rem',
'--accordion-padding-y': '0.5rem',
'--accordion-radius': '0.375rem',
},
itemTrigger: {
fontSize: 'md',
},
},
lg: {
root: {
'--accordion-padding-x': '1.5rem',
'--accordion-padding-y': '0.75rem',
'--accordion-radius': '0.5rem',
},
itemTrigger: {
fontSize: 'lg',
},
},
},
These variables cascade throughout the component, keeping spacing and text scale consistent.
Putting It All Together
We've explored how to define base styles, create flexible size variants using CSS variables, handle interactive states,
and integrate custom parts like the itemBody
.
Now let's bring everything together. Here's what the complete accordion recipe will look like:
import { sva } from '../../styled-system/css'
import { accordionAnatomy } from '@ark-ui/react/accordion'
export const accordionRecipe = sva({
slots: [...accordionAnatomy.keys(), 'itemBody'],
className: 'accordion',
base: {
root: {
width: 'full',
borderRadius: 'var(--accordion-radius)',
},
item: {
overflowAnchor: 'none',
},
itemTrigger: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
textAlign: 'start',
width: 'full',
outline: 0,
gap: '3',
fontWeight: 'medium',
borderRadius: 'var(--accordion-radius)',
padding: 'var(--accordion-padding-y) var(--accordion-padding-x)',
background: 'transparent',
_focusVisible: {
outline: '2px solid var(--accordion-focus-ring, #3b82f6)',
},
_disabled: {
opacity: 0.5,
cursor: 'not-allowed',
},
},
itemContent: {
overflow: 'hidden',
borderRadius: 'var(--accordion-radius)',
paddingInline: 'var(--accordion-padding-x)',
_open: {
animationName: 'expand-height, fade-in',
animationDuration: '200ms',
},
_closed: {
animationName: 'collapse-height, fade-out',
animationDuration: '200ms',
},
},
itemBody: {
paddingTop: 'var(--accordion-padding-y)',
paddingBottom: 'calc(var(--accordion-padding-y) * 2)',
},
itemIndicator: {
transition: 'rotate 0.2s',
transformOrigin: 'center',
color: 'fg.subtle',
_open: {
transform: 'rotate(180deg)',
},
_icon: {
width: '1.2em',
height: '1.2em',
},
},
},
variants: {
variant: {
outline: {
item: {
borderBottom: '1px solid',
borderColor: 'border',
},
},
subtle: {
itemTrigger: {
paddingInline: 'var(--accordion-padding-x)',
},
itemContent: {
paddingInline: 'var(--accordion-padding-x)',
},
item: {
borderRadius: 'var(--accordion-radius)',
_open: {
background: 'bg.muted',
},
},
},
enclosed: {
root: {
border: '1px solid',
borderColor: 'border',
borderRadius: 'var(--accordion-radius)',
overflow: 'hidden',
},
itemTrigger: {
paddingInline: 'var(--accordion-padding-x)',
},
itemContent: {
paddingInline: 'var(--accordion-padding-x)',
},
item: {
'& + &': {
borderTop: '1px solid',
borderColor: 'border',
},
_open: {
background: 'bg.subtle',
},
},
},
},
size: {
sm: {
root: {
'--accordion-padding-x': '0.75rem',
'--accordion-padding-y': '0.5rem',
'--accordion-radius': '0.25rem',
},
itemTrigger: {
fontSize: 'sm',
},
},
md: {
root: {
'--accordion-padding-x': '1rem',
'--accordion-padding-y': '0.5rem',
'--accordion-radius': '0.375rem',
},
itemTrigger: {
fontSize: 'md',
},
},
lg: {
root: {
'--accordion-padding-x': '1.5rem',
'--accordion-padding-y': '0.75rem',
'--accordion-radius': '0.5rem',
},
itemTrigger: {
fontSize: 'lg',
},
},
},
},
defaultVariants: {
size: 'sm',
variant: 'outline',
},
})
Using Your Accordion Recipe in a React Component
With your accordionRecipe
fully defined, let’s put it to work in a React component.
1. Import Your Accordion Recipe
Start by importing the accordionRecipe
into your component file.
// accordion.tsx
import { accordionRecipe } from './recipes/accordion'
2. Generate Component Classes
Next, generate the class names needed for each part of your Accordion. Calling accordionRecipe()
returns an object
where each key matches a slot name (e.g., root
, itemTrigger
, itemContent
) and its value is the generated class
string.
You can also pass size
and variant
values as recipe options. For example:
// Generate default styles (uses defaultVariants)
const accordionClasses = accordionRecipe()
// Generate styles for a medium-sized, subtle accordion
const accordionClasses = accordionRecipe({
size: 'md',
variant: 'subtle',
})
3. Apply Classes to Ark UI Parts
Now apply the generated class names to each corresponding Ark UI component part using the className
prop.
Here’s a complete example using all styled parts:
import { Accordion } from '@ark-ui/react'
import { ChevronDownIcon } from 'lucide-react'
import { accordionRecipe } from './recipes/accordion'
const AccordionItemBody = ({ children }: { children: React.ReactNode }) => (
<div data-scope="accordion" data-part="itemBody">
{children}
</div>
)
export const PandaStyledAccordion = () => {
const accordionClasses = accordionRecipe({ size: 'md', variant: 'subtle' })
return (
<Accordion.Root className={accordionClasses.root}>
<Accordion.Item className={accordionClasses.item} value="react">
<Accordion.ItemTrigger className={accordionClasses.itemTrigger}>
What is React?
<Accordion.ItemIndicator className={accordionClasses.itemIndicator}>
<ChevronDownIcon />
</Accordion.ItemIndicator>
</Accordion.ItemTrigger>
<Accordion.ItemContent className={accordionClasses.itemContent}>
<AccordionItemBody className={accordionClasses.itemBody}>
React is a JavaScript library for building UIs.
</AccordionItemBody>
</Accordion.ItemContent>
</Accordion.Item>
<Accordion.Item className={accordionClasses.item} value="vue">
<Accordion.ItemTrigger className={accordionClasses.itemTrigger}>
What is Vue?
<Accordion.ItemIndicator className={accordionClasses.itemIndicator}>
<ChevronDownIcon />
</Accordion.ItemIndicator>
</Accordion.ItemTrigger>
<Accordion.ItemContent className={accordionClasses.itemContent}>
<AccordionItemBody className={accordionClasses.itemBody}>
Vue is a progressive JavaScript framework.
</AccordionItemBody>
</Accordion.ItemContent>
</Accordion.Item>
</Accordion.Root>
)
}
To explore all of the accordion styles and their variations, be sure to check out the Storybook examples.
Conclusion
You’ve just built a fully custom-styled Accordion component using Ark UI, with support for Vanilla CSS or Panda CSS.
We walked through the base styles, size and visual variants, and added a custom part (item-body
) to give you more
layout control.
A headless library like Ark UI means that whichever style pattern you decide to use, you're in control of your design system and can scale as your team grows. and keep things clean.
Feel free to explore the GitHub repo or Storybook examples.