Accordion
Expandable content sections with animated height transitions.
Yes. It uses accessibilityRole and accessibilityState.
Yes. It uses react-native-reanimated for smooth animations.
Yes. Use className to override any styles.
import { Accordion, AccordionItem } from "@/components/ui/accordion";
export function MyScreen() {
return (
<Accordion defaultValue="item-1">
<AccordionItem value="item-1" trigger="Is it accessible?">
<Text>Yes. It uses accessibilityRole and accessibilityState.</Text>
</AccordionItem>
<AccordionItem value="item-2" trigger="Is it animated?">
<Text>Yes. It uses react-native-reanimated for smooth animations.</Text>
</AccordionItem>
<AccordionItem value="item-3" trigger="Can I customize it?">
<Text>Yes. Use className to override any styles.</Text>
</AccordionItem>
</Accordion>
);
}Installation
npx @aniui/cli add accordionThis component requires react-native-reanimated for animations.
Usage
app/index.tsx
import { Accordion, AccordionItem } from "@/components/ui/accordion";
export function MyScreen() {
return (
<Accordion defaultValue="item-1">
<AccordionItem value="item-1" trigger="Is it accessible?">
<Text>Yes. It uses accessibilityRole and accessibilityState.</Text>
</AccordionItem>
<AccordionItem value="item-2" trigger="Is it animated?">
<Text>Yes. It uses react-native-reanimated for smooth animations.</Text>
</AccordionItem>
<AccordionItem value="item-3" trigger="Can I customize it?">
<Text>Yes. Use className to override any styles.</Text>
</AccordionItem>
</Accordion>
);
}Compound Components
ComponentDescription
AccordionRoot container that manages expanded state
AccordionItemIndividual collapsible section with trigger text
Props
Accordion
PropTypeDefault
defaultValuestring—classNamestring—AccordionItem
PropTypeDefault
valuestringrequiredtriggerstringrequiredclassNamestring—Source
components/ui/accordion.tsx
import React, { createContext, useContext, useState } from "react";
import { View, Pressable, Text } from "react-native";
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
import { cn } from "@/lib/utils";
const AccordionContext = createContext<{
expanded: string | null;
toggle: (value: string) => void;
}>({ expanded: null, toggle: () => {} });
export interface AccordionProps extends React.ComponentPropsWithoutRef<typeof View> {
className?: string;
defaultValue?: string;
children?: React.ReactNode;
}
export function Accordion({ className, defaultValue, children, ...props }: AccordionProps) {
const [expanded, setExpanded] = useState<string | null>(defaultValue ?? null);
const toggle = (value: string) => setExpanded((prev) => (prev === value ? null : value));
return (
<AccordionContext.Provider value={{ expanded, toggle }}>
<View className={cn("", className)} {...props}>{children}</View>
</AccordionContext.Provider>
);
}
export interface AccordionItemProps extends React.ComponentPropsWithoutRef<typeof View> {
className?: string;
value: string;
trigger: string;
children?: React.ReactNode;
}
export function AccordionItem({ value, trigger, className, children, ...props }: AccordionItemProps) {
const { expanded, toggle } = useContext(AccordionContext);
const isOpen = expanded === value;
const progress = useSharedValue(0);
React.useEffect(() => {
progress.value = withTiming(isOpen ? 1 : 0, { duration: 250 });
}, [isOpen, progress]);
const animatedStyle = useAnimatedStyle(() => ({
height: progress.value === 0 ? 0 : undefined,
opacity: progress.value,
overflow: "hidden" as const,
}));
return (
<View className={cn("border-b border-border", className)} {...props}>
<Pressable
className="flex-row items-center justify-between py-4 min-h-12"
onPress={() => toggle(value)}
accessible={true}
accessibilityRole="button"
accessibilityState={{ expanded: isOpen }}
>
<Text className="text-base font-medium text-foreground flex-1">{trigger}</Text>
<Text className="text-muted-foreground text-lg">{isOpen ? "−" : "+"}</Text>
</Pressable>
<Animated.View style={animatedStyle}>
<View className="pb-4">{children}</View>
</Animated.View>
</View>
);
}