shadimage
Remocn
componentaccordionsource: ok · refreshed

UI Accordion

A disclosure whose opened/closed state is a pure function of the timeline; the panel reveal, chevron, and background are keyframed presets.

npx shadcn add "https://www.remocn.dev/r/accordion.json"
free

Where it goes

stats

Visual style

playful

Interaction

animated-counterscroll-revealaccordion

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

Dependencies

Bundle weight · light
remotion

Registry dependencies

@remocn/remocn-ui

Files2 · source cached 10/7/2026, 10:00:22 PM

registry/remocn-ui/accordion/index.tsx
"use client";

import { mixOklch, type RemocnTheme, useRemocnTheme } from "@/lib/remocn-ui";

export type AccordionState = "opened" | "closed";

type AccordionVariant = "default" | "ghost";

export interface AccordionProps {
  state?: AccordionState;
  style?: AccordionStyle;
  title?: string;
  content?: string;
  contentHeight?: number;
  variant?: AccordionVariant;
  theme?: Partial<RemocnTheme>;
  className?: string;
}

const CARD_WIDTH = 440;

interface VariantTokens {
  bordered: boolean;
  closedBg: string;
  openBg: string;
}

function variantTokens(
  variant: AccordionVariant,
  theme: RemocnTheme,
): VariantTokens {
  const variants = {
    ghost: {
      bordered: false,
      closedBg: theme.background,
      openBg: mixOklch(theme.background, theme.muted, 0.25),
    },
    default: {
      bordered: true,
      closedBg: theme.background,
      openBg: mixOklch(theme.background, theme.muted, 0.5),
    },
  };

  return variants[variant] ?? variants.default;
}

export interface AccordionStyle {
  panelHeight: number;
  panelOpacity: number;
  chevronRotation: number;
  background: string;
}

export interface AccordionStyleContext {
  bordered: boolean;
  closedBg: string;
  openBg: string;
  border: string;
  foreground: string;
  mutedForeground: string;
}

export function accordionStyleContext(
  variant: AccordionVariant,
  theme: RemocnTheme,
): AccordionStyleContext {
  const tokens = variantTokens(variant, theme);
  return {
    bordered: tokens.bordered,
    closedBg: tokens.closedBg,
    openBg: tokens.openBg,
    border: theme.border,
    foreground: theme.foreground,
    mutedForeground: theme.mutedForeground,
  };
}

export function accordionStyle(
  state: AccordionState,
  ctx: AccordionStyleContext,
): AccordionStyle {
  switch (state) {
    case "opened":
      return {
        panelHeight: 1,
        panelOpacity: 1,
        chevronRotation: 180,
        background: ctx.openBg,
      };
    default:
      return {
        panelHeight: 0,
        panelOpacity: 0,
        chevronRotation: 0,
        background: ctx.closedBg,
      };
  }
}

export function Accordion({
  state = "closed",
  style,
  title = "Is it accessible?",
  content = "Yes. It adheres to the WAI-ARIA design pattern.",
  contentHeight = 64,
  variant = "default",
  theme: themeOverride,
  className,
}: AccordionProps) {
  const theme = useRemocnTheme(themeOverride, "light");

  const ctx = accordionStyleContext(variant, theme);
  const v = style ?? accordionStyle(state, ctx);

  return (
    <div
      style={{
        position: "absolute",
        inset: 0,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        background: "transparent",
        fontFamily:
          "var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif",
      }}
    >
      <div
        className={className}
        style={{
          width: CARD_WIDTH,
          background: v.background,
          border: ctx.bordered
            ? `1px solid ${ctx.border}`
            : "1px solid transparent",
          borderRadius: theme.radius,
          overflow: "hidden",
        }}
      >
        {}
        <div
          style={{
            display: "flex",
            alignItems: "center",
            justifyContent: "space-between",
            gap: 24,
            padding: 16,
            fontSize: 15,
            fontWeight: 500,
            letterSpacing: "-0.01em",
            color: ctx.foreground,
          }}
        >
          <span>{title}</span>
          <svg
            width={16}
            height={16}
            viewBox="0 0 24 24"
            fill="none"
            style={{
              flexShrink: 0,
              transform: `rotate(${v.chevronRotation}deg)`,
            }}
          >
            <path
              d="M6 9l6 6 6-6"
              stroke={ctx.mutedForeground}
              strokeWidth="2.2"
              strokeLinecap="round"
              strokeLinejoin="round"
            />
          </svg>
        </div>
        {}
        <div
          style={{
            height: contentHeight * v.panelHeight,
            opacity: v.panelOpacity,
            overflow: "hidden",
          }}
        >
          <div
            style={{
              padding: "0 16px 16px",
              fontSize: 14,
              lineHeight: 1.5,
              color: ctx.mutedForeground,
            }}
          >
            {content}
          </div>
        </div>
      </div>
    </div>
  );
}
registry/remocn-ui/accordion/use-accordion-transition.ts
"use client";

import {
  type AccordionState,
  type AccordionStyle,
  accordionStyle,
  accordionStyleContext,
} from "@/components/remocn/accordion";
import {
  easings,
  mixOklch,
  type RemocnTheme,
  type Step,
  useRemocnTheme,
  useStateTransition,
} from "@/lib/remocn-ui";

export const DEFAULT_DURATION = 14;

export function tweenAccordionStyle(
  a: AccordionStyle,
  b: AccordionStyle,
  t: number,
): AccordionStyle {
  return {
    panelHeight: a.panelHeight + (b.panelHeight - a.panelHeight) * t,
    panelOpacity: a.panelOpacity + (b.panelOpacity - a.panelOpacity) * t,
    chevronRotation:
      a.chevronRotation + (b.chevronRotation - a.chevronRotation) * t,
    background: mixOklch(a.background, b.background, t),
  };
}

export interface AccordionTransitionOptions {
  variant?: "default" | "ghost";
  theme?: Partial<RemocnTheme>;
  mode?: "light" | "dark";
  speed?: number;
  defaultDuration?: number;
}

export function useAccordionTransition(
  steps: Step<AccordionState>[],
  opts: AccordionTransitionOptions = {},
): AccordionStyle {
  const {
    variant = "default",
    theme: themeOverride,
    mode,
    speed = 1,
    defaultDuration = DEFAULT_DURATION,
  } = opts;
  const theme = useRemocnTheme(themeOverride, mode);
  const ctx = accordionStyleContext(variant, theme);
  const { from, to, progress } = useStateTransition(
    steps,
    "closed",
    speed,
    defaultDuration,
  );
  const t = easings.out(progress);
  return tweenAccordionStyle(
    accordionStyle(from, ctx),
    accordionStyle(to, ctx),
    t,
  );
}

More from Remocn

Browse all components in this library →