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 · lightremotion
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,
);
}