Accordion
A vertically stacked set of interactive headings that each reveal a section of content.
Our flagship product combines cutting-edge technology with sleek design. Built with premium materials, it offers unparalleled performance and reliability.
Key features include advanced processing capabilities, and an intuitive user interface designed for both beginners and experts.
We offer worldwide shipping through trusted courier partners. Standard delivery takes 3-5 business days, while express shipping ensures delivery within 1-2 business days.
All orders are carefully packaged and fully insured. Track your shipment in real-time through our dedicated tracking portal.
We stand behind our products with a comprehensive 30-day return policy. If you're not completely satisfied, simply return the item in its original condition.
Our hassle-free return process includes free return shipping and full refunds processed within 48 hours of receiving the returned item.
import { css } from 'remix/component'
import {
Accordion,
AccordionContent,
AccordionItem,import { css } from 'remix/component'
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '#app/ui/accordion.tsx'
const contentStyles = css({
display: 'flex',
flexDirection: 'column',
gap: '1rem',
textWrap: 'balance',
})
export function AccordionDemo() {
return () => (
<Accordion type="single" collapsible mix={css({ width: '100%' })} defaultValue="item-1">
<AccordionItem value="item-1">
<AccordionTrigger>Product Information</AccordionTrigger>
<AccordionContent mix={contentStyles}>
<p>
Our flagship product combines cutting-edge technology with sleek design. Built with
premium materials, it offers unparalleled performance and reliability.
</p>
<p>
Key features include advanced processing capabilities, and an intuitive user interface
designed for both beginners and experts.
</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Shipping Details</AccordionTrigger>
<AccordionContent mix={contentStyles}>
<p>
We offer worldwide shipping through trusted courier partners. Standard delivery takes
3-5 business days, while express shipping ensures delivery within 1-2 business days.
</p>
<p>
All orders are carefully packaged and fully insured. Track your shipment in real-time
through our dedicated tracking portal.
</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Return Policy</AccordionTrigger>
<AccordionContent mix={contentStyles}>
<p>
We stand behind our products with a comprehensive 30-day return policy. If you're
not completely satisfied, simply return the item in its original condition.
</p>
<p>
Our hassle-free return process includes free return shipping and full refunds processed
within 48 hours of receiving the returned item.
</p>
</AccordionContent>
</AccordionItem>
</Accordion>
)
}Installation
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/accordion.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/accordion.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/accordion.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/accordion.jsonInstall the following dependencies:
$ bun add remix @remix-run/ui$ npm install remix @remix-run/ui$ pnpm add remix @remix-run/ui$ yarn add remix @remix-run/uiCopy and paste the following code into your project.
import * as accordion from '@remix-run/ui/accordion'
import { css, ref, type Handle, type Props } from 'remix/component'
import { ChevronDownIcon } from './icons.tsx'
interface AccordionBaseProps extends Omit<Props<'div'>, 'defaultValue'> {
disabled?: boolean
}
export interface AccordionSingleProps extends AccordionBaseProps {
type: 'single'
/** The controlled open item; `""` when none is open. */
value?: string
defaultValue?: string
onValueChange?: (value: string) => void
/** Allow closing the open item by clicking its trigger. */
collapsible?: boolean
}
export interface AccordionMultipleProps extends AccordionBaseProps {
type: 'multiple'
value?: string[]
defaultValue?: string[]
onValueChange?: (value: string[]) => void
}
export type AccordionProps = AccordionSingleProps | AccordionMultipleProps
export interface AccordionItemProps extends Props<'div'> {
value: string
disabled?: boolean
}
const accordionItemStyles = css({
borderBottomWidth: '1px',
'&:last-child': { borderBottomWidth: 0 },
})
const accordionHeaderStyles = css({ display: 'flex' })
const accordionTriggerStyles = css({
display: 'flex',
flex: '1 1 0%',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: '1rem',
borderRadius: 'calc(var(--radius) - 2px)',
paddingBlock: '1rem',
textAlign: 'left',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
fontWeight: 500,
transitionProperty: 'all',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms',
outlineStyle: 'none',
boxShadow: 'var(--tw-ring-shadow), var(--tw-shadow)',
'&:hover': { textDecorationLine: 'underline' },
'&:focus-visible': {
borderColor: 'var(--ring)',
'--tw-ring-shadow':
'0 0 0 3px var(--tw-ring-color, color-mix(in oklab, var(--ring) 50%, transparent))',
},
'&:disabled': { pointerEvents: 'none', opacity: 0.5 },
'&[data-state="open"] > svg': { rotate: '180deg' },
})
const accordionChevronStyles = css({
pointerEvents: 'none',
width: '1rem',
height: '1rem',
flexShrink: 0,
translate: '0 0.125rem',
color: 'var(--muted-foreground)',
transitionProperty: 'transform, translate, scale, rotate',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '200ms',
})
const accordionContentStyles = css({
overflow: 'hidden',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
'&[data-state="closed"]': {
height: 0,
animation: 'shadcn-accordion-up 200ms ease-out',
},
'&[data-state="open"]': { animation: 'shadcn-accordion-down 200ms ease-out' },
'@keyframes shadcn-accordion-down': {
from: { height: 0 },
to: { height: 'var(--radix-accordion-content-height, auto)' },
},
'@keyframes shadcn-accordion-up': {
from: { height: 'var(--radix-accordion-content-height, auto)' },
to: { height: 0 },
},
})
const accordionContentInnerStyles = css({ paddingTop: 0, paddingBottom: '1rem' })
export function Accordion(handle: Handle<AccordionProps>) {
return () => {
let props = handle.props
let content = (
<div
data-slot="accordion"
data-orientation="vertical"
{...omitAccordionProps(props)}
mix={[accordion.root(), props.mix]}
/>
)
if (props.type === 'multiple') {
return (
<accordion.Context
type="multiple"
value={props.value}
defaultValue={props.defaultValue}
onValueChange={props.onValueChange}
disabled={props.disabled}
>
{content}
</accordion.Context>
)
}
let { value, defaultValue, onValueChange } = props
return (
<accordion.Context
type="single"
value={value === undefined ? undefined : value || null}
defaultValue={defaultValue || null}
onValueChange={onValueChange && ((next) => onValueChange(next ?? ''))}
collapsible={props.collapsible ?? false}
disabled={props.disabled}
>
{content}
</accordion.Context>
)
}
}
function omitAccordionProps(props: AccordionProps): Props<'div'> {
let { type, value, defaultValue, onValueChange, disabled, mix, ...rest } = props as AccordionSingleProps
let { collapsible, ...domProps } = rest
return domProps
}
export function AccordionItem(handle: Handle<AccordionItemProps>) {
return () => {
let { value, disabled, mix, ...props } = handle.props
return (
<accordion.ItemContext value={value} disabled={disabled}>
<div
data-slot="accordion-item"
data-orientation="vertical"
{...props}
mix={[accordionItemStyles, accordion.item(), mix]}
/>
</accordion.ItemContext>
)
}
}
export function AccordionTrigger(handle: Handle<Props<'button'>>) {
let item = handle.context.get(accordion.ItemContext)
return () => {
let { children, mix, ...props } = handle.props
return (
<h3
data-orientation="vertical"
data-state={item.open ? 'open' : 'closed'}
data-disabled={item.disabled ? '' : undefined}
mix={accordionHeaderStyles}
>
<button
type="button"
data-slot="accordion-trigger"
data-orientation="vertical"
data-disabled={item.disabled ? '' : undefined}
{...props}
mix={[accordionTriggerStyles, accordion.trigger(), mix]}
>
{children}
<ChevronDownIcon mix={accordionChevronStyles} />
</button>
</h3>
)
}
}
export function AccordionContent(handle: Handle<Props<'div'>>) {
let item = handle.context.get(accordion.ItemContext)
let node: HTMLDivElement | undefined
let wasOpen: boolean | undefined
// Like Radix, the open/close animation is suppressed until the item is first toggled, so items
// that render open (or closed) don't animate on page load.
let animate = false
return () => {
let { children, mix, style, ...props } = handle.props
if (wasOpen !== undefined && wasOpen !== item.open) {
animate = true
handle.queueTask(() => {
node?.style.setProperty('--radix-accordion-content-height', `${node.scrollHeight}px`)
})
}
wasOpen = item.open
return (
<div
data-slot="accordion-content"
role="region"
data-orientation="vertical"
{...props}
style={animate || typeof style === 'string' ? style : { ...style, animationName: 'none' }}
mix={[accordionContentStyles, accordion.content(), ref((el: HTMLDivElement) => (node = el))]}
>
<div mix={[accordionContentInnerStyles, mix]}>
{children}
</div>
</div>
)
}
}Update the import paths to match your project setup.
Usage
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '#app/ui/accordion.tsx'<Accordion type="single" collapsible defaultValue="item-1">
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
</Accordion>Composition
Use the following composition to build an Accordion:
Accordion
└── AccordionItem
├── AccordionTrigger
└── AccordionContentAPI Reference
Accordion
Root <div>. Arrow Up/Down, Home and End move focus between triggers.
| Prop | Type | Default |
|---|---|---|
type | "single" | "multiple" | - |
value | string | string[] | - |
defaultValue | string | string[] | - |
onValueChange | (value: string | string[]) => void | - |
collapsible | boolean | false |
disabled | boolean | false |
mix | MixValue | - |
AccordionItem
Item wrapper <div> with data-state="open" | "closed".
| Prop | Type | Default |
|---|---|---|
value | string | - |
disabled | boolean | false |
mix | MixValue | - |
AccordionTrigger
A <button> inside an <h3>, with a chevron that rotates when open.
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
AccordionContent
The collapsible region. Animates its height when toggled. mix styles the inner wrapper.
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |