Chip
Free · MITReact Native · ExpoFilter, choice and input chips: selecting one fills it with the accent while a check draws itself in, removing one shrinks it away, and a ChipGroup in single mode moves one elastic highlight from chip to chip.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/chip.jsonThen run npx expo install --fix so react-native-reanimated match your Expo SDK. Files land in components/goodui-native/chip.
Usage
import { useState } from "react";
import { ChipGroup } from "@/components/goodui-native/chip/chip";
type Slot = "09:00" | "10:30" | "13:00" | "15:30";
export function TimeSlots() {
const [slot, setSlot] = useState<Slot[]>(["10:30"]);
return (
<ChipGroup<Slot>
accessibilityLabel="Appointment time"
value={slot}
onValueChange={setSlot}
options={[
{ value: "09:00", label: "9:00" },
{ value: "10:30", label: "10:30" },
{ value: "13:00", label: "1:00 PM" },
{ value: "15:30", label: "3:30 PM", disabled: true },
]}
/>
);
}When to use
- Filters above a list or map: cuisine, price, distance, open now.
- Picking one or several short options in a form: time slots, sizes, interests.
- Showing entered values that can be removed: recipients, tags, applied filters.
- Suggestions the person can tap to act on (action chips with onPress).
- Switching between two to five views of the same content: use segmented-control.
- Read-only status: use badge.
- A long list of options or anything that needs search: use a picker or a bottom-sheet list.
- The main action of a screen: use button.
Examples
Multiple filters in a scrolling row, at most three
<ChipGroup
mode="multiple"
layout="scroll"
max={3}
defaultValue={["open"]}
accessibilityLabel="Filters"
contentContainerStyle={{ paddingHorizontal: 16 }}
options={[
{ value: "open", label: "Open now" },
{ value: "near", label: "Under 2 km" },
{ value: "cheap", label: "€" },
{ value: "vegan", label: "Vegan" },
{ value: "rated", label: "4.5+" },
]}
/>Removable input chips
{recipients.map((r) => (
<Chip key={r.id} label={r.name} onRemove={() => setRecipients((all) => all.filter((x) => x.id !== r.id))} />
))}A single toggle chip
<Chip label="Only refundable" selected={refundable} onSelectedChange={setRefundable} />API reference
Chip
| Prop | Type | Default |
|---|---|---|
| label* Short text, sentence case. | string | — |
| selected Controlled selected state. Passing it (or `defaultSelected` or `onSelectedChange`) makes the chip a toggle. | boolean | — |
| defaultSelected Uncontrolled starting state. | boolean | — |
| onSelectedChange Called with the new state when the chip is toggled. | (selected: boolean) => void | — |
| onPress For action chips that do not toggle, such as a suggestion. | () => void | — |
| onRemove Shows an × button. The chip shrinks away first, then this runs, so the list can drop it without a jump. | () => void | — |
| leading Icon or avatar before the label, 16 to 18 pt. When selected, the check takes its place. | ChipLeading | — |
| variant `outline` has a hairline border, `soft` a quiet fill. Selected chips fill with the accent. | "soft" | "outline" | "outline" |
| size 32 or 36 pt tall, with a 44 pt touch target. | "sm" | "md" | "md" |
| check Draw a check in when selected. Default `true`. | boolean | true |
| disabled Dims the control and ignores presses. | boolean | false |
| haptic Haptic on toggle or press. `false` turns it off. | HapticName | false | "select" |
| accessibilityLabel Overrides the label read by screen readers. | string | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<ChipLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
ChipGroup
| Prop | Type | Default |
|---|---|---|
| options* The chips, in order. | ChipOption<T>[] | — |
| mode `single` behaves like radio buttons with one highlight that springs between chips; `multiple` like checkboxes. | "single" | "multiple" | "single" |
| value Controlled selection. Always an array; in `single` mode it holds at most one value. | T[] | — |
| defaultValue Uncontrolled starting selection. | T[] | — |
| onValueChange Called with the new selection. | (value: T[]) => void | — |
| allowEmpty In `single` mode, tapping the selected chip clears the selection. Default `false`. | boolean | false |
| max In `multiple` mode, the most chips that can be selected. Past it the tap is refused with a warning haptic. | number | — |
| layout `wrap` flows onto new lines; `scroll` is one horizontal row that keeps the selected chip in view. | "wrap" | "scroll" | "wrap" |
| variant | "soft" | "outline" | "outline" |
| size | "sm" | "md" | "md" |
| haptic Haptic when the selection changes. | HapticName | false | "select" |
| accessibilityLabel Names the group for screen readers, for example "Cuisine". | string | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<ChipLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| contentContainerStyle Padding for the scrolling row, usually the screen gutter. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- Selecting fills the chip and recolors the label with a timing; the check slot opens on the snappy spring and the check draws itself in. With a leading icon, the check crossfades in its place.
- Single-mode ChipGroup: one accent highlight travels between chips. The edge in the direction of travel leads on the snappy spring and the trailing edge follows on smooth, so it stretches and settles; top and height follow on morph when it changes lines.
- Press scales the chip to 0.95 on the snappy spring from touch down.
- Remove: the chip's real width closes while it fades and scales to 0.8, then onRemove runs, so neighbors slide over.
- In the scroll layout the row scrolls the newly selected chip into view.
- Reduced motion: the highlight jumps, the check appears without drawing, removal is a fade, no press scale.
- Haptic: select when a chip toggles or the group selection changes (configurable with haptic)
- Haptic: warning when a selection is refused at max
- Haptic: tap on remove
- Colors, the highlight and removal run on the UI thread. Chip layouts are measured once per real change; the highlight only moves when the selection or the selected chip's box changes.
Accessibility
- A toggle Chip is a checkbox; in a ChipGroup chips are radios (single mode, inside a radiogroup) or checkboxes (multiple), with checked and disabled in accessibilityState.
- Action chips are buttons; chips with neither are read as text.
- The × is its own button labelled 'Remove <label>', and removal is announced.
- Hitting the max in multiple mode is refused with a warning haptic and announced ('Choose up to 3').
- Chips are 32 or 36 pt tall with a vertical hitSlop up to 44 pt.
Notes for AI
- ChipGroup value is always an array, also in single mode: value={[slot]} onValueChange={([v]) => setSlot(v)}. Single mode cannot be cleared unless allowEmpty is set.
- A Chip becomes a toggle when you pass selected, defaultSelected or onSelectedChange; otherwise pass onPress for an action chip or nothing for a static one.
- Keep a removed chip mounted until onRemove runs; then drop it from your list.
- Use layout='scroll' with contentContainerStyle={{ paddingHorizontal: 16 }} for a full-bleed filter row.
- Labels are short and sentence case. Put icons in leading (a function receives the label color).
Related
Two to five views of the same thing with an elastic highlight that stretches toward the new segment, can be dragged, and ticks at every boundary. Includes a billing toggle.
A count, dot or status badge that springs in when it appears, pops when its value changes, caps at 99+ and shrinks away at zero; BadgeAnchor pins it to an icon or avatar.
A sheet that rests at detents, follows the finger 1:1, settles with velocity and hands drags off to its scroll view.