shadimage
liquefy-ui
uiliquid-accordionsource: ok · refreshed

LiquidAccordion

Stacked disclosure panels with real headings and a height-measured open animation.

npx shadcn add "https://liquefy-ui.com/r/liquid-accordion.json"
free

Interaction

accordion

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@base-ui/react@1.8.0

Registry dependencies

https://liquefy-ui.com/r/liquefy-ui.jsonhttps://liquefy-ui.com/r/internal-glyphs.jsonhttps://liquefy-ui.com/r/liquid-surface.jsonhttps://liquefy-ui.com/r/styles-prop.json

Files1 · source cached 10/8/2026, 6:17:01 AM

registry/liquid-accordion.tsx
'use client'

import { Accordion } from '@base-ui/react/accordion'
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
import { ChevronDownGlyph } from '@/lib/internal-glyphs'
import { LiquidSurface, type LiquidSurfaceProps } from '@/components/ui/liquid-surface'
import { useLiquidStyles, type LiquidStyleProps } from '@/lib/styles-prop'

// Base UI owns the roving focus across headers, the aria-controls wiring and the
// panel height measurement (exposed as --accordion-panel-height). liquefy-ui keeps
// the glass surface, the chevron and the jelly open animation.

export type LiquidAccordionProps = Omit<LiquidSurfaceProps, 'children' | 'defaultValue'> & {
  children: ReactNode
  defaultValue?: string[]
  multiple?: boolean
  onValueChange?: (value: string[]) => void
  value?: string[]
}

export const LiquidAccordion = forwardRef<HTMLDivElement, LiquidAccordionProps>(({
  children,
  className,
  defaultValue = [],
  multiple = false,
  onValueChange,
  value,
  ...props
}, ref) => (
  <Accordion.Root
    defaultValue={defaultValue}
    multiple={multiple}
    onValueChange={(next) => onValueChange?.(next.map(String))}
    render={(
      <LiquidSurface
        className={['lq-accordion', className].filter(Boolean).join(' ')}
        interactive={false}
        ref={ref}
        {...props}
      />
    )}
    value={value}
  >
    {children}
  </Accordion.Root>
))

LiquidAccordion.displayName = 'LiquidAccordion'

// `title` is omitted because HTMLAttributes declares it as the tooltip attribute,
// i.e. a `string`, and intersecting that with `ReactNode` leaves `string` — the
// header below could never be given an element.
export type LiquidAccordionItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'title'> & LiquidStyleProps & {
  children: ReactNode
  subtitle?: ReactNode
  title: ReactNode
  value: string
}

export const LiquidAccordionItem = forwardRef<HTMLDivElement, LiquidAccordionItemProps>(({
  children,
  className,
  style,
  styles,
  subtitle,
  title,
  value,
  ...props
}, ref) => {
  const root = useLiquidStyles('lq-accordion__item', { className, style, styles })

  return (
    <Accordion.Item className={root.className} ref={ref} style={root.style} value={value} {...props}>
      {/* A real heading, so screen-reader users can jump between sections. */}
      <Accordion.Header className="lq-accordion__header">
        <Accordion.Trigger className="lq-accordion__trigger">
          <span className="lq-accordion__heading">
            <span className="lq-accordion__title">{title}</span>
            {subtitle && <span className="lq-accordion__subtitle">{subtitle}</span>}
          </span>
          <span className="lq-accordion__chevron"><ChevronDownGlyph size={15} /></span>
        </Accordion.Trigger>
      </Accordion.Header>
      <Accordion.Panel className="lq-accordion__content">
        <div className="lq-accordion__content-inner">{children}</div>
      </Accordion.Panel>
    </Accordion.Item>
  )
})

LiquidAccordionItem.displayName = 'LiquidAccordionItem'

More from liquefy-ui

Browse all components in this library →