Expokit
AboutDocsComponentsChartsBlocksPricing
Sign inGet started
All components

Inputs

CalendarNewDate PickerNewFloating InputOTP InputPassword InputSearch BarSelectSliderStepperTag InputTextareaTime PickerNew

Selection

CheckboxChip GroupColor SwatchProRadio GroupRatingProSegmented ControlSwitch

Buttons

ButtonFloating Action ButtonProLike ButtonProMorph ButtonProShimmer ButtonProSlide to ConfirmProStateful Button

Feedback

AlertProgressSkeletonSpinnerToast

Overlays

Action SheetNewBottom SheetDialogDrawerNew

Display

AccordionAvatarBadgeCardCarouselProList ItemNewPagination DotsNewSwipeable RowProTimelineNew

Navigation

Bottom NavNewProgress StepsNewTabs

Motion

Number TickerProText Animation
Accordion
Sponsor ExpoKit

Free stays free. Tip once — keep shipping.

Support
About
Docs
IntroductionWhat ExpoKit is and how it worksInstallationAdd ExpoKit to an Expo appThemingColours, radius and dark modeCLIinit, add, login and updateAI assistants (MCP)Cursor, Claude, Antigravity and moreRun on your phoneTry components in Expo GoChangelogWhat shipped recently
npx @expokit/cli@latest initQuick start
Components
Liquid Glass8 iOS 26 glass pieces3DNew14 tilt, flip and stack
Inputs 12
  • CalendarNew
  • Date PickerNew
  • Floating Input
  • OTP Input
  • Password Input
  • +7 more
Selection 7
  • Checkbox
  • Chip Group
  • Color SwatchPro
  • Radio Group
  • RatingPro
  • +2 more
Buttons 7
  • Button
  • Floating Action ButtonPro
  • Like ButtonPro
  • Morph ButtonPro
  • Shimmer ButtonPro
  • +2 more
Feedback 5
  • Alert
  • Progress
  • Skeleton
  • Spinner
  • Toast
Overlays 4
  • Action SheetNew
  • Bottom Sheet
  • Dialog
  • DrawerNew
Display 9
  • Accordion
  • Avatar
  • Badge
  • Card
  • CarouselPro
  • +4 more
Navigation 3
  • Bottom NavNew
  • Progress StepsNew
  • Tabs
Motion 2
  • Number TickerPro
  • Text Animation
70 components · 347 live variantsAll components
Charts

Cartesian

  • Area Chart
  • Bar Chart
  • Line Chart
  • Combo Chart

Circular

  • Pie & Donut
  • Radial Chart
  • Radar Chart
  • Polar AreaPro

3D & Pixel

  • Isometric BarsPro
  • Pixel BarsPro

Specialty

  • TreemapPro
  • WaterfallPro
  • BulletPro
  • TimelinePro
  • Gauge
  • Candlestick
  • Heatmap
  • Funnel

Classic

  • Sparkline
  • Progress Rings
  • MultiLine (classic)
  • StackedBar (classic)
  • Ranking Bars (classic)

Guides

  • Usage & config
  • Patterns & effects
  • Show / hide anything
  • Chart theming
  • Interactions
23 charts, built on the open-source expo-charts engineAll charts
Blocks

Screens

ChatProAn AI assistant chat screenCheckoutProA premium checkoutDashboardProA finance / analytics home screenInboxProA messaging inboxOnboardingProA minimal swipeable welcome flowProfileProAccount profile for any appSearch ResultsProA clean, monochrome search screenSettingsProAn iOS-style grouped settings screenSign InProOne configurable sign-in flow modelled on top mobile apps
9 complete screens — drop one in, wire your dataSee them on devices All blocks
Pricing
DocsComponentsDisplayAccordion

Accordion

Scan with Expo Go

Accordion with all its variants, native in Expo Go. No account needed.

No Expo Go? Open in your browser

Sections spring open to their measured height while the chevron turns.

<Accordion />4 variantsiOS · Android · Expo Go
9:41

Sunday 18 October

9:41

Installation#

One command copies the source into your app — you own the code and can change anything.

Usage#

Colours, radius and light / dark come from useKit(). Wrap your app in <KitProvider accent="#6366f1" radius={14}> to re-skin every component at once.

Props#

PropTypeDefault
type

One open item at a time, or any number.

"single" | "multiple""single"
value / defaultValue

Open item values (controlled / uncontrolled).

string[][]
onValueChange

Called when items open or close.

(value: string[]) => void—
collapsible

Single mode: allow closing the open item.

booleantrue
variant

Divided list, one bordered box, or separate cards.

"default" | "bordered" | "separated""default"
accent

Chevron colour of open items.

stringtheme accent
AccordionItem.value

Identifies the item in the Accordion's value array.

string—
AccordionItem.title / subtitle

Header text.

ReactNode / string—
AccordionItem.icon

Leading icon; content indents to line up with the title.

ReactNode—
AccordionItem.disabled

Can't be toggled.

booleanfalse
style

Style for the Accordion's root view (the card in the bordered variant); AccordionItem takes its own.

StyleProp<ViewStyle>—

Behaviour & accessibility#

Touch
Tap a header to open or close its section. In single mode, opening one closes the other; disabled items don't respond.
Motion
Content height springs to its measured size, so any content animates smoothly. The chevron turns 180° on the native driver.
Screen readers
Each header is a button with expanded and disabled state. Content of closed sections is hidden from assistive tech.

Source#

PreviousDrawerNextAvatar

Did you like the content?

Coding agents

Chat apps

Other

9:41

Default

Accordion

Manual installation
  1. Install dependencies

    terminal
    npx expo install react-native-svg
  2. Copy the files

    Create each file at the same path and paste its code. @/ points at your src/ folder (the default in new Expo apps). Skip any file you already have, like lib/kit.tsx.

    • src/components/ui/accordion.tsx234 lines
    • src/lib/kit.tsx220 lines

    Want to read it first? Every file is in Source below.

  3. Use it

    App.tsx
    import { Accordion } from "@/components/ui/accordion";

    See Usage for a full example.

Example.tsx
import { Accordion, AccordionItem } from '@/components/ui/accordion'; <Accordion defaultValue={['shipping']}>  <AccordionItem value="shipping" title="How long does shipping take?">    Orders leave our warehouse within 24 hours.  </AccordionItem>  <AccordionItem value="returns" title="Can I return an item?">    You have 30 days to send anything back for a full refund.  </AccordionItem></Accordion>

Every variant above has a complete, runnable example in its Code tab.

src/components/ui/accordion.tsx
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';import {  Animated,  Pressable,  StyleSheet,  Text,  View,  type LayoutChangeEvent,  type StyleProp,  type ViewStyle,} from 'react-native';import Svg, { Path } from 'react-native-svg'; import { alpha, isTextNode, motion, useKit } from '@/lib/kit'; export type AccordionVariant = 'default' | 'bordered' | 'separated'; export interface AccordionProps {  children: React.ReactNode;  /** 'single' keeps one item open at a time; 'multiple' lets any number stay open. */  type?: 'single' | 'multiple';  /** Open item values (controlled). */  value?: string[];  defaultValue?: string[];  onValueChange?: (value: string[]) => void;  /** In 'single' mode, allow closing the open item (default true). */  collapsible?: boolean;  variant?: AccordionVariant;  /** Chevron colour of open items (default: theme accent). */  accent?: string;  style?: StyleProp<ViewStyle>;} interface Ctx {  open: string[];  toggle: (v: string) => void;  variant: AccordionVariant;  tint: string;}const AccordionContext = createContext<Ctx | null>(null);const PositionContext = createContext({ first: true, last: true }); /** Expanding sections with measured-height spring motion and a rotating chevron. */export function Accordion({  children,  type = 'single',  value,  defaultValue = [],  onValueChange,  collapsible = true,  variant = 'default',  accent,  style,}: AccordionProps) {  const { colors, radius } = useKit();  const [inner, setInner] = useState(defaultValue);  const open = value ?? inner;  const openRef = useRef(open);  openRef.current = open;   const toggle = useCallback(    (v: string) => {      const cur = openRef.current;      const isOpen = cur.includes(v);      let next: string[];      if (type === 'multiple') next = isOpen ? cur.filter((x) => x !== v) : [...cur, v];      else next = isOpen ? (collapsible ? [] : cur) : [v];      if (next === cur) return;      if (value === undefined) setInner(next);      onValueChange?.(next);    },    [type, collapsible, value, onValueChange]  );   const ctx = useMemo(() => ({ open, toggle, variant, tint: accent ?? colors.accent }), [open, toggle, variant, accent, colors.accent]);  const items = React.Children.toArray(children);   return (    <AccordionContext.Provider value={ctx}>      <View        style={[          variant === 'bordered' && {            borderWidth: StyleSheet.hairlineWidth * 2,            borderColor: colors.border,            borderRadius: radius.lg,            backgroundColor: colors.card,            overflow: 'hidden',          },          variant === 'separated' && styles.separated,          style,        ]}      >        {items.map((child, i) => (          <PositionContext.Provider key={(child as React.ReactElement).key ?? i} value={{ first: i === 0, last: i === items.length - 1 }}>            {child}          </PositionContext.Provider>        ))}      </View>    </AccordionContext.Provider>  );} export interface AccordionItemProps {  /** Unique id used by the Accordion's value. */  value: string;  title: React.ReactNode;  /** Secondary line under the title. */  subtitle?: string;  /** Leading icon. */  icon?: React.ReactNode;  children?: React.ReactNode;  disabled?: boolean;  style?: StyleProp<ViewStyle>;} /** One collapsible section. Content is measured, so any height animates smoothly. */export function AccordionItem({ value, title, subtitle, icon, children, disabled, style }: AccordionItemProps) {  const ctx = useContext(AccordionContext);  if (!ctx) throw new Error('AccordionItem must be used inside <Accordion>');  const { open: openValues, toggle, variant, tint } = ctx;  const { last } = useContext(PositionContext);  const { colors, radius, dark } = useKit();  const open = openValues.includes(value);   const [height, setHeight] = useState(0);  // `progress` drives height (layout → JS driver); `turn` rotates the chevron on the native driver.  const progress = useRef(new Animated.Value(open ? 1 : 0)).current;  const turn = useRef(new Animated.Value(open ? 1 : 0)).current;   useEffect(() => {    Animated.spring(progress, { toValue: open ? 1 : 0, useNativeDriver: false, ...motion.gentle, overshootClamping: true }).start();    Animated.spring(turn, { toValue: open ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();  }, [open, progress, turn]);   const onLayout = (e: LayoutChangeEvent) => {    const h = Math.ceil(e.nativeEvent.layout.height);    if (h !== height) setHeight(h);  };   const pad = variant === 'default' ? 0 : 16;  const container: ViewStyle =    variant === 'separated'      ? {          backgroundColor: colors.card,          borderRadius: radius.lg,          borderWidth: StyleSheet.hairlineWidth * 2,          borderColor: open ? alpha(tint, dark ? 0.45 : 0.35) : colors.border,          overflow: 'hidden',        }      : variant === 'default' || !last        ? { borderBottomWidth: StyleSheet.hairlineWidth * 2, borderBottomColor: colors.border }        : {};   return (    <View style={[container, style]}>      <Pressable        accessibilityRole="button"        accessibilityState={{ expanded: open, disabled: !!disabled }}        disabled={disabled}        onPress={() => toggle(value)}        style={({ pressed }) => [          styles.header,          { paddingHorizontal: pad, opacity: disabled ? 0.45 : 1 },          pressed && variant !== 'default' && { backgroundColor: colors.muted },          pressed && variant === 'default' && { opacity: 0.6 },        ]}      >        {icon ? <View style={styles.icon}>{icon}</View> : null}        <View style={styles.titles}>          {isTextNode(title) ? (            <Text style={[styles.title, { color: open && variant === 'default' ? tint : colors.foreground }]}>{title}</Text>          ) : (            title          )}          {subtitle ? <Text style={[styles.subtitle, { color: colors.mutedForeground }]}>{subtitle}</Text> : null}        </View>        <Animated.View          style={{ transform: [{ rotate: turn.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] }) }] }}        >          <Svg width={18} height={18} viewBox="0 0 24 24">            <Path              d="M6 9l6 6 6-6"              stroke={open ? tint : colors.mutedForeground}              strokeWidth={2.2}              strokeLinecap="round"              strokeLinejoin="round"              fill="none"            />          </Svg>        </Animated.View>      </Pressable>       <Animated.View        style={{ height: progress.interpolate({ inputRange: [0, 1], outputRange: [0, height] }), overflow: 'hidden' }}        accessibilityElementsHidden={!open}        importantForAccessibility={open ? 'auto' : 'no-hide-descendants'}      >        <Animated.View          onLayout={onLayout}          pointerEvents={open ? 'auto' : 'none'}          style={[            styles.body,            {              paddingHorizontal: pad,              paddingLeft: pad + (icon ? 36 : 0),              opacity: progress.interpolate({ inputRange: [0, 0.6, 1], outputRange: [0, 0.4, 1] }),              transform: [{ translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [-8, 0] }) }],            },          ]}        >          {isTextNode(children) ? (            <Text style={[styles.text, { color: colors.mutedForeground }]}>{children}</Text>          ) : (            children          )}        </Animated.View>      </Animated.View>    </View>  );} Accordion.Item = AccordionItem; const styles = StyleSheet.create({  separated: { gap: 10 },  header: { flexDirection: 'row', alignItems: 'center', minHeight: 56, paddingVertical: 14, gap: 12 },  icon: { width: 24, alignItems: 'center' },  titles: { flex: 1, gap: 2 },  title: { fontSize: 15.5, fontWeight: '400', letterSpacing: -0.1 },  subtitle: { fontSize: 13, lineHeight: 18 },  body: { position: 'absolute', top: 0, left: 0, right: 0, paddingBottom: 16 },  text: { fontSize: 14.5, lineHeight: 21 },});