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
Chip Group
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
DocsComponentsSelectionChip Group

Chip Group

Scan with Expo Go

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

No Expo Go? Open in your browser

Filter chips that widen to make room as a check mark draws itself in.

<ChipGroup />5 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
options

Plain strings or objects with label, icon, count and per-chip disabled.

(string | { value, label?, icon?, count?, disabled? })[]—
multiple

Allow several selected chips.

booleanfalse
value / onValueChange

Controlled selection (or use defaultValue). Single mode reports [] or [value].

string[]—
defaultValue

Initial selection when uncontrolled.

string[][]
deselectable

Single mode: tapping the selected chip clears it.

booleantrue
variant

Selected look: filled accent, accent border with a faint tint, or soft accent tint.

"solid" | "outline" | "soft""solid"
size

30 / 36 / 42pt tall.

"sm" | "md" | "lg""md"
scrollable

One horizontally scrolling row instead of wrapping.

booleanfalse
contentInset

Scroll-row edge padding that bleeds into your screen gutter.

number0
showCheck

Morph a check in front of the label when selected.

booleantrue
accent

Selected colour.

stringtheme accent
disabled

Dims every chip and blocks presses (or set disabled per option).

booleanfalse
style

Style for the wrapping row (the ScrollView when scrollable).

StyleProp<ViewStyle>—

Behaviour & accessibility#

Touch
Tap to select. In single mode, tapping the selected chip clears it unless deselectable is false. Short chips pad their hit area to 44pt.
Motion
Selection springs the check, width and colours together, and chips squish while pressed.
Screen readers
Chips are radios in single mode and checkboxes in multiple mode, with checked and disabled state.

Source#

PreviousCheckboxNextColor Swatch

Did you like the content?

Coding agents

Chat apps

Other

9:41

Solid

Chip Group

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/chip-group.tsx270 lines
    • src/lib/kit.tsx220 lines

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

  3. Use it

    App.tsx
    import { ChipGroup } from "@/components/ui/chip-group";

    See Usage for a full example.

Example.tsx
import { ChipGroup } from '@/components/ui/chip-group'; <ChipGroup  multiple  defaultValue={['Vegan']}  options={['Vegan', 'Vegetarian', 'Gluten-free', 'Halal']}/>

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

src/components/ui/chip-group.tsx
import React, { useEffect, useRef, useState } from 'react';import {  Animated,  Pressable,  ScrollView,  StyleSheet,  View,  type StyleProp,  type ViewStyle,} from 'react-native';import Svg, { Path } from 'react-native-svg'; import { alpha, motion, readableOn, useKit } from '@/lib/kit'; const AnimatedPath = Animated.createAnimatedComponent(Path);const CHECK = 'M5 12.5l4.5 4.5L19 7.5';const CHECK_LEN = 22; export type ChipGroupVariant = 'solid' | 'outline' | 'soft';export type ChipGroupSize = 'sm' | 'md' | 'lg'; export interface ChipOption {  value: string;  label?: string;  /** Leading icon or emoji. */  icon?: React.ReactNode;  /** Small number after the label, e.g. result count. */  count?: number;  disabled?: boolean;} export interface ChipGroupProps {  /** Chips — plain strings or `{ value, label, icon, count }` objects. */  options: (string | ChipOption)[];  /** Allow several chips at once. Single mode still reports an array (`[]` or `[value]`). */  multiple?: boolean;  /** Controlled selection. */  value?: string[];  /** Initial selection when uncontrolled. */  defaultValue?: string[];  onValueChange?: (value: string[]) => void;  /** Single mode: tapping the selected chip clears it (default true). */  deselectable?: boolean;  variant?: ChipGroupVariant;  size?: ChipGroupSize;  /** One horizontally scrolling row instead of wrapping lines. */  scrollable?: boolean;  /** Morph a check in front of the label when selected (default true). */  showCheck?: boolean;  /** Selected colour (default: theme accent). */  accent?: string;  disabled?: boolean;  accessibilityLabel?: string;  /** Padding at the ends of the scroll row, so chips line up with your screen gutter. */  contentInset?: number;  style?: StyleProp<ViewStyle>;} const SIZES: Record<ChipGroupSize, { h: number; px: number; font: number; check: number }> = {  sm: { h: 30, px: 12, font: 13, check: 14 },  md: { h: 36, px: 14, font: 14.5, check: 16 },  lg: { h: 42, px: 16, font: 15.5, check: 18 },}; /** * Filter chips for single or multiple choice. Selecting one morphs a check mark in — the chip * widens smoothly to make room while its colours cross-fade. */export function ChipGroup({  options,  multiple = false,  value,  defaultValue = [],  onValueChange,  deselectable = true,  variant = 'solid',  size = 'md',  scrollable = false,  showCheck = true,  accent,  disabled,  accessibilityLabel,  contentInset = 0,  style,}: ChipGroupProps) {  const { colors } = useKit();  const tint = accent ?? colors.accent;  const items: ChipOption[] = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));  const [inner, setInner] = useState<string[]>(defaultValue);  const selected = value ?? inner;   const toggle = (v: string) => {    const has = selected.includes(v);    let next: string[];    if (multiple) next = has ? selected.filter((x) => x !== v) : [...selected, v];    else if (has) {      if (!deselectable) return;      next = [];    } else next = [v];    if (value === undefined) setInner(next);    onValueChange?.(next);  };   const s = SIZES[size];  const chips = items.map((o) => (    <Chip      key={o.value}      option={o}      active={selected.includes(o.value)}      onPress={() => toggle(o.value)}      variant={variant}      size={s}      tint={tint}      showCheck={showCheck}      multiple={multiple}      disabled={disabled || o.disabled}    />  ));   if (scrollable) {    return (      <ScrollView        horizontal        showsHorizontalScrollIndicator={false}        accessibilityLabel={accessibilityLabel}        style={[{ flexGrow: 0, marginHorizontal: -contentInset }, style]}        contentContainerStyle={[styles.row, { paddingHorizontal: contentInset, paddingVertical: 2 }]}      >        {chips}      </ScrollView>    );  }   return (    <View accessibilityLabel={accessibilityLabel} style={[styles.row, styles.wrap, style]}>      {chips}    </View>  );} interface ChipProps {  option: ChipOption;  active: boolean;  onPress: () => void;  variant: ChipGroupVariant;  size: { h: number; px: number; font: number; check: number };  tint: string;  showCheck: boolean;  multiple: boolean;  disabled?: boolean;} function Chip({ option, active, onPress, variant, size, tint, showCheck, multiple, disabled }: ChipProps) {  const { colors, radius } = useKit();  // p: colours, check width + stroke (JS driver). press: squish (native, on the outer view).  const p = useRef(new Animated.Value(active ? 1 : 0)).current;  const press = useRef(new Animated.Value(1)).current;   useEffect(() => {    Animated.spring(p, { toValue: active ? 1 : 0, useNativeDriver: false, ...motion.snappy }).start();  }, [active, p]);   const pressTo = (to: number) =>    Animated.spring(press, { toValue: to, useNativeDriver: true, ...(to < 1 ? motion.snappy : motion.bouncy) }).start();   const look: Record<ChipGroupVariant, { bg: [string, string]; border: [string, string]; fg: [string, string] }> = {    solid: {      bg: [colors.muted, tint],      border: [colors.muted, tint],      fg: [colors.foreground, readableOn(tint)],    },    outline: {      bg: [alpha(tint, 0), alpha(tint, 0.08)],      border: [colors.border, tint],      fg: [colors.foreground, tint],    },    soft: {      bg: [alpha(colors.mutedForeground, 0.1), alpha(tint, 0.16)],      border: [alpha(colors.mutedForeground, 0), alpha(tint, 0)],      fg: [colors.foreground, tint],    },  };  const l = look[variant];  const range = { inputRange: [0, 1], extrapolate: 'clamp' as const };  const checkBox = size.check + 4; // check + gap   return (    <Animated.View style={{ opacity: disabled ? 0.45 : 1, transform: [{ scale: press }] }}>      <Pressable        accessibilityRole={multiple ? 'checkbox' : 'radio'}        accessibilityState={{ checked: active, disabled: !!disabled }}        accessibilityLabel={option.label}        disabled={disabled}        onPress={onPress}        onPressIn={() => pressTo(0.94)}        onPressOut={() => pressTo(1)}        hitSlop={size.h < 44 ? { top: (44 - size.h) / 2, bottom: (44 - size.h) / 2 } : undefined}      >        <Animated.View          style={[            styles.chip,            {              height: size.h,              borderRadius: radius.capsule(size.h),              paddingHorizontal: size.px - 1,              backgroundColor: p.interpolate({ ...range, outputRange: l.bg }),              borderColor: p.interpolate({ ...range, outputRange: l.border }),            },          ]}        >          {showCheck ? (            <Animated.View              style={[                styles.checkWrap,                {                  width: p.interpolate({ ...range, outputRange: [0, checkBox], extrapolate: 'clamp' }),                  marginLeft: p.interpolate({ ...range, outputRange: [0, -2], extrapolate: 'clamp' }),                  opacity: p.interpolate({ inputRange: [0, 0.4, 1], outputRange: [0, 0.6, 1], extrapolate: 'clamp' }),                },              ]}            >              <Svg width={size.check} height={size.check} viewBox="0 0 24 24">                <AnimatedPath                  d={CHECK}                  stroke={l.fg[1]}                  strokeWidth={3}                  strokeLinecap="round"                  strokeLinejoin="round"                  fill="none"                  strokeDasharray={CHECK_LEN}                  strokeDashoffset={p.interpolate({ inputRange: [0.2, 1], outputRange: [CHECK_LEN, 0], extrapolate: 'clamp' }) as unknown as number}                />              </Svg>            </Animated.View>          ) : null}          {option.icon ? <View style={styles.icon}>{option.icon}</View> : null}          {option.label ? (            <Animated.Text              numberOfLines={1}              style={[styles.label, { fontSize: size.font, color: p.interpolate({ ...range, outputRange: l.fg }) }]}            >              {option.label}            </Animated.Text>          ) : null}          {option.count !== undefined ? (            <Animated.Text              style={[                styles.count,                { fontSize: size.font - 1.5, color: p.interpolate({ ...range, outputRange: [colors.mutedForeground, l.fg[1]] }) },              ]}            >              {option.count}            </Animated.Text>          ) : null}        </Animated.View>      </Pressable>    </Animated.View>  );} const styles = StyleSheet.create({  row: { flexDirection: 'row', alignItems: 'center', gap: 8 },  wrap: { flexWrap: 'wrap' },  chip: { flexDirection: 'row', alignItems: 'center', borderWidth: 1 },  checkWrap: { overflow: 'hidden', flexDirection: 'row', alignItems: 'center' },  icon: { marginRight: 6 },  label: { fontWeight: '400', letterSpacing: -0.1 },  count: { marginLeft: 6, fontWeight: '400', fontVariant: ['tabular-nums'], opacity: 0.85 },});