Accordion
Vertically stacked, interactive headings that reveal content.
- Chromium
- Firefox
- WebKit
- WCAG 2.2 AA
Preview
"use client";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@moe-ui/registry/components/ui/accordion";
export const AccordionPreview = () => {
return (
<Accordion type="single" collapsible className="w-full max-w-sm">
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Is it styled?</AccordionTrigger>
<AccordionContent>
Yes. It comes with default styles that match the other components’
aesthetic.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Is it animated?</AccordionTrigger>
<AccordionContent>
Yes. It is animated by default, and can be customized.
</AccordionContent>
</AccordionItem>
</Accordion>
);
};
One command, your source
Installation
pnpm dlx @moe-ui/cli@beta add accordionUsage
The CLI installs editable source into your application. Import the component from your configured UI directory:
import { Accordion } from "@/components/ui/accordion";Variants and composition
Each additional variant has its own live preview and testable section.
Multiple
"use client";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@moe-ui/registry/components/ui/accordion";
export const AccordionMultipleTypePreview = () => {
return (
<Accordion
type="multiple"
defaultValue={["item-1"]}
className="w-full max-w-sm"
>
<AccordionItem value="item-1">
<AccordionTrigger>First section</AccordionTrigger>
<AccordionContent>
This accordion allows multiple sections to stay expanded.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Second section</AccordionTrigger>
<AccordionContent>
You can open this section without closing the first one.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Third section</AccordionTrigger>
<AccordionContent>
Use the multiple type when users need to compare content side by side.
</AccordionContent>
</AccordionItem>
</Accordion>
);
};
Compound parts and variants remain regular source in your project, so you can change them without wrapping a package API.
Public API
The canonical source panel below lists every public export, dependency, and the complete implementation shipped by the registry.
Keyboard and accessibility
Arrow-key and Tab behavior follows the underlying accessible primitive. Focus indicators remain visible in both themes.
Browser support
This beta is release-tested in Chromium, Firefox, and WebKit in light and dark themes. See the web compatibility matrix for the current gate.
Nothing hiddenCanonical source and public exports4 exports
AccordionAccordionContentAccordionItemAccordionTrigger"use client";
import { Icon } from "./icon";
import { TextClassContext } from "./text";
import { cn } from "../../lib/utils";
import * as AccordionPrimitive from "@rn-primitives/accordion";
import { ChevronDown } from "lucide-react-native";
import { Platform, Pressable, View } from "react-native";
import Animated, {
FadeOutUp,
LayoutAnimationConfig,
LinearTransition,
useAnimatedStyle,
useDerivedValue,
withTiming,
} from "react-native-reanimated";
function Accordion({
children,
...props
}: Omit<AccordionPrimitive.RootProps, "asChild"> &
React.RefAttributes<AccordionPrimitive.RootRef>) {
return (
<LayoutAnimationConfig skipEntering>
<AccordionPrimitive.Root
{...(props as AccordionPrimitive.RootProps)}
asChild={Platform.OS !== "web"}
>
<Animated.View layout={LinearTransition.duration(200)}>
{children}
</Animated.View>
</AccordionPrimitive.Root>
</LayoutAnimationConfig>
);
}
function AccordionItem({
children,
className,
value,
...props
}: AccordionPrimitive.ItemProps &
React.RefAttributes<AccordionPrimitive.ItemRef>) {
return (
<AccordionPrimitive.Item
className={cn(
"border-border border-b",
Platform.select({ web: "last:border-b-0" }),
className,
)}
value={value}
asChild
{...props}
>
<Animated.View
className="native:overflow-hidden"
layout={Platform.select({ native: LinearTransition.duration(200) })}
>
{children}
</Animated.View>
</AccordionPrimitive.Item>
);
}
const Trigger = Platform.OS === "web" ? View : Pressable;
function AccordionTrigger({
className,
children,
...props
}: AccordionPrimitive.TriggerProps & {
children?: React.ReactNode;
} & React.RefAttributes<AccordionPrimitive.TriggerRef>) {
const { isExpanded } = AccordionPrimitive.useItemContext();
const progress = useDerivedValue(
() =>
isExpanded
? withTiming(1, { duration: 250 })
: withTiming(0, { duration: 200 }),
[isExpanded],
);
const chevronStyle = useAnimatedStyle(
() => ({
transform: [{ rotate: `${progress.value * 180}deg` }],
}),
[progress],
);
return (
<TextClassContext.Provider
value={cn(
"text-left text-sm font-medium",
Platform.select({ web: "group-hover:underline" }),
)}
>
<AccordionPrimitive.Header>
<AccordionPrimitive.Trigger {...props} asChild={Platform.OS !== "web"}>
<Trigger
className={cn(
"flex-row items-start justify-between gap-4 rounded-md py-4 disabled:opacity-50",
Platform.select({
web: "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 outline-none transition-all hover:underline focus-visible:ring-[3px] disabled:pointer-events-none [&[data-state=open]>svg]:rotate-180",
}),
className,
)}
>
<>{children}</>
<Animated.View style={chevronStyle}>
<Icon
as={ChevronDown}
size={16}
className={cn(
"text-muted-foreground shrink-0",
Platform.select({
web: "pointer-events-none translate-y-0.5 transition-transform duration-200",
}),
)}
/>
</Animated.View>
</Trigger>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
</TextClassContext.Provider>
);
}
function AccordionContent({
className,
children,
...props
}: AccordionPrimitive.ContentProps &
React.RefAttributes<AccordionPrimitive.ContentRef>) {
const { isExpanded } = AccordionPrimitive.useItemContext();
return (
<TextClassContext.Provider value="text-sm">
<AccordionPrimitive.Content
className={cn(
"overflow-hidden",
Platform.select({
web: isExpanded ? "animate-accordion-down" : "animate-accordion-up",
}),
)}
{...props}
>
<Animated.View
exiting={Platform.select({ native: FadeOutUp.duration(200) })}
className={cn("pb-4", className)}
>
{children}
</Animated.View>
</AccordionPrimitive.Content>
</TextClassContext.Provider>
);
}
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };