Primitives
Animated Number Avatar Badge Button Button Group Toggle Toggle Group Checkbox Chip Countdown Divider Fab Grid Icon Image Input Search Input Textarea Label Loader Marquee Masked Input Message Bubble Notification Banner Offline Banner Progress Progress Ring Radio Group Range Slider Rating Separator Skeleton Slider Stack Stepper Switch Text Theme Toggle Ticker Typing IndicatorComposed
Accordion Achievement Badge Action Bar Action Sheet Action Swap Activity Rings Add to Cart Button Address Card Agenda List Alert Amount Keypad Animated Icons App Bar Audio Message Audio Recorder Autocomplete Avatar Group Balance Display Bar List Barcode Before After Biometric Gate Bloom Menu Bottom Sheet Paywall Sheet Side Sheet Breadcrumb Calendar Calendar Strip Card Carousel Cart Item Character Charts Chat Composer Chat List Item Choice Card Circle Reveal Coach Marks Collapsible Collapsible Header Color Picker Combobox Command Palette Comment Thread Confetti Consent Sheet Context Menu Copy Field Country Picker Coupon Input Currency Input Data Table Date Range Picker Day Schedule Dialog Dock Donut Chart Drawer Driver Card Dropdown Menu Drum Picker Emoji Picker Empty State Error State Event Card Expandable Control Fader Feature Gate File Picker Filter Bar Follow Button Form Gauge Glass Glass Cluster Glass Dock Morph Glass Fab Morph Glass Lab Glass Orb Glass Search Morph Glass Sheet Edge Gradient Button Headline New Heatmap Image Cropper Image Viewer Kanban Card Keyboard Avoiding Keyboard Toolbar Leaderboard Level Progress Lightbox Like Button Line Chart List Reorderable List Swipeable Row Location Card Location Search Map Sheet Map View Markdown View Media Controls Media Picker Media Tile Mention Input Meter Bar Mini Player Mood Slider Morphing Modal Multi Select Navigation Rail Notification Item Number Input Onboarding Pager Order Card Order Summary OTP Input Pagination Paper Clip New Parallax Header Pass Card Password Strength Payment Button Payment Card Payment Method Permission Primer Phone Input PIN Pad Polaroid New Poll Popover Post Card Postage Stamp New Price Tag Pricing Table Product Card Product Gallery Profile Header Promo Banner Prompt Input Property Card Pull To Refresh QR Code Quick Actions Quick Replies Rating Summary Reaction Picker Resend Timer Review Card Review Prompt Ride Option Save Button Scan Frame Scroll Spy Tabs Seat Map Section Header Section Index Segmented Control Select Share Button Share Sheet Shutter Button Signature Pad Slide to Confirm Slot Picker Smart Banner Social Login Button Sparkline Splash Screen Split Button Stat Card Stateful Button Status Dot Step Flow Steps Sticker New Stopwatch Stories Progress Stories Viewer Story Ring Streak Counter Streaming Text Success Seal Swipe Deck Tabs Tag Input Tape New Text Reveal Thinking Indicator Time Picker Timeline Timer Ring Tip Selector Toast Toolbar Tooltip Track Row Transaction Row Trip Progress Variant Picker Video Controls Waveform Weather Card Weekday Picker Zoom TransitionScreens
Account Switcher Biometric Setup Delete Account Forgot Password Grow Auth Login Login — Dark Stage Login — Dense Work Login — Editorial Login — Illustrated Login — Minimal Ink Login — Social Login — Video Loop Food Onboarding Onboarding Onboarding Dark Objects Onboarding Editorial Onboarding Illustrated Onboarding Personalize Onboarding Photo Carousel OTP Verification Passkey — Create Password Reset Permission — Notifications Phone Verification Onboarding Quiz Session Devices Signup Sign-up Chat Sign-up Photo Sheet Social Auth Split Auth Verify Identity Welcome AI Chat AI Assistant Channels List Chat Info Chat List Chat Media Chat Thread Create Group Group Info Message Requests New Message Kept Messages Accessibility Activity Log Appearance Authenticator Blocked Accounts Export Your Data Downloads Edit Profile Edit Profile — Rows Emergency Call Empty States Set Error State Send Feedback Device Locator Help Center Home — Bento Home — Dark Home — Editorial Home Feed Invite Friends Language & Region Linked Accounts Not Found (404) Notification Settings Notifications Password Vault Paywall Paywall — Comparison Paywall — Dark Stage Paywall — Editorial Paywall — Photo Paywall — Trial Timeline Privacy Profile Profile — Gamified Profile — Minimal Profile — Photo Hero QR Scanner Report Issue Saved Items Phone Time Search Search — Command Palette Search — Visual Account Health Settings Settings — Dark Device Settings — Search Storage & Data Terms & Privacy Dating Chat Dating Filters Profile Discover Its A Match Into You Matches Profile Detail Profile Editor Courier Matching Delivery History Delivery Support Package Schedule Parcel Tracking Proof Of Delivery Quick Aisle Quick Home Send Package Substitutions Add Payment Address Book Cart Categories Checkout Checkout — Steps Checkout Success Delivery Filters & Sort Flash Sale Gift Cards Loyalty Points Order Cancel Order Detail Orders Pay Later Checkout Payment Methods Product Detail Product Detail — Hero Product Grid Product Reviews Promo Codes Recently Viewed Return Items Search Results Shop Assistant Store Home Tip & Rate Courier Order Tracking Wishlist Write a Review Event Checkin Event Hero Events Discover My Tickets Seat Selection Ticket Checkout Add Card Bank Accounts Dashboard — Bento Bill Pay Budgets Card Details Crypto Portfolio Finance Dashboard Dashboard — Dark Exchange Currency Expense List Invoice Detail Manage Cards Plan Management Savings Goals Send Money Spending Analytics Split Bill Subscriptions Token Detail Token Swap Transactions Wallet Home Wallet Movers Courier Tracking Food Cart Food Home Food Orders Grocery List Recipe Detail Restaurant — Dark Restaurant Detail Activity Rings Fitness Home Habit Quest Habit Tracker Meal Planner Med Store Meditation Player Mood Check-in Nutrition Home Period Tracker Prior Auth Prior Auth Cases Quest Complete Run Tracking Rx Scan Sleep Tracker Telehealth Booking Walk Map Water Tracker Workout Complete Workout Detail Workout Session Applications Apply Company Interview Schedule Job Detail Job Feed Resume Saved Jobs Create Listing Item Chat Listing Detail Make Offer Market Checkout Market Home Market Search My Listings Offers Inbox Seller Profile Audiobook Player Camera Gallery Albums Game Store Mini Player Music Player Photo Editor Photo Gallery Playlist Detail Podcast Player Clips Feed Serene Home Sound Mixer Stories Video Call Video Player Article Reader Course Detail Event Detail Memory Onboarding Icon Lab Leaderboard Lesson Player Match Detail News Editorial News Home Plant Care Prediction Live Prediction Welcome Smart Home Standings Ticket Wallet Topics Follow Weather Detail Add Task Sheet Alarm Clock Calendar Day Calendar Month Doc Scanner File Manager Focus Timer Journal Kanban Board Mail Inbox Mail Thread Note Editor Notes List Reminders Task List Time Blocks Agent Profile Compare Homes Mortgage Calculator Property Detail Rental Application Saved Homes Schedule Tour Search List Search Map Complete Destination Driver Arriving Ev Charging History Home In Trip Matching Options Parking Receipt Safety Schedule Scooter Unlock Booking Confirmed Booking Review Booking Slots My Bookings Pro On The Way Provider Detail Provider List Rate Service Services Home Arc Create Arc Detail Arc Onboarding Avatar Creator Comments Sheet Create Post Explore Grid Followers List Hashtag Feed Mentions Post Detail Post Hero Social Profile Boarding Pass Booking Confirm Car Rental Trip Discover Flight Live Flight Preferences Flight Results Flight Search Hotel Detail Live Transit Trip — Map Day Trip — Rideshare Request Seat Select Travel Home Trip Itinerary Trip OnboardingNothing matches — try another word.
List
Settings-style list with grouped sections, separators and press feedback. Rows can brighten under your finger and appear one after another in reading order.
Preview
import * as React from "react";
import { forwardRef, createContext, useContext, Children } from "react";
import { View, Text, Pressable, StyleSheet, type ViewProps, type PressableProps, type GestureResponderEvent, } from "react-native";
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from "react-native-reanimated";
import { cn, useHaptic, usePressAnimation, a11yProps, AnimatedPressable, spring, timing, useReducedMotion, useTheme, BAND_STAGGER, rowDelay, SwitchOn, type A11yProps } from "@lumioui/core";
import { Icon } from "../icon";
import { Glass } from "../glass";
/**
* List — grouped list primitives for settings-style screens.
* <List inset>
* <List.Section title="General">
* <List.Item title="Wi-Fi" subtitle="Connected" showChevron />
* <List.Separator />
* <List.Item title="Bluetooth" />
* </List.Section>
* </List>
*
* Opt-in Lumio light: `lit` makes a held row brighten rather than shade;
* `switchOn` has rows start dim and switch on 40ms apart, sections a band
* (120ms) apart. Without them the list renders exactly as before.
*/
const ListInsetContext = createContext({ inset: false, glass: false, lit: false, switchOn: false });
/** Position of a section among its List's children — drives its switch-on band. */
const SectionOrderContext = createContext(0);
/** A row held under the finger brightens by this much — light, not shade. */
const LIT_WASH = 0.06;
export interface ListProps extends ViewProps {
/** Inset-grouped style: rounded card sections with horizontal margin. */
inset?: boolean;
/** Glass section material (pairs naturally with inset). */
glass?: boolean;
/**
* Lit press: a held row brightens (foreground light on dark, accent light
* on light) instead of taking the muted ink wash. Default false.
*/
lit?: boolean;
/**
* Switch on at mount: each section's rows start dim and switch on 40ms
* apart, sections one band (120ms) apart. Default false.
*/
switchOn?: boolean;
className?: string;
}
const ListRoot = forwardRef<React.ElementRef<typeof View>, ListProps>(
function List({ inset = false, glass = false, lit = false, switchOn = false, className, children, ...props }, ref) {
return (
<ListInsetContext.Provider value={{ inset, glass, lit, switchOn }}>
<View ref={ref} className={cn("w-full", className)} {...props}>
{switchOn
? Children.toArray(children).map((child, i) => (
<SectionOrderContext.Provider key={React.isValidElement(child) && child.key != null ? child.key : i} value={i}>
{child}
</SectionOrderContext.Provider>
))
: children}
</View>
</ListInsetContext.Provider>
);
}
);
export interface ListSectionProps extends ViewProps {
/** Optional header text rendered above the group. */
title?: string;
/** Extra classes for the header text. */
titleClassName?: string;
/** Optional footnote under the group — a short explainer line. */
footer?: string;
className?: string;
}
const ListSection = forwardRef<React.ElementRef<typeof View>, ListSectionProps>(
function ListSection({ title, titleClassName, footer, className, children, ...props }, ref) {
const { inset, glass, switchOn } = useContext(ListInsetContext);
const order = useContext(SectionOrderContext);
const band = order * BAND_STAGGER;
const header =
title != null ? (
<Text
className={cn(
"px-4 pb-1.5 text-[13px] text-muted-foreground",
titleClassName
)}
accessibilityRole="header"
>
{title}
</Text>
) : null;
if (switchOn) {
// Container, then content: the group surface is there at once, its
// header and rows switch on inside it in reading order.
return (
<View ref={ref} className={cn("mb-6", inset && "mx-4", className)} {...props}>
{header != null ? <SwitchOn delay={band}>{header}</SwitchOn> : null}
<View
className={cn("overflow-hidden", !glass && "bg-card", (inset || glass) && "rounded-2xl")}
>
{glass ? (
<Glass
effect="regular"
sheen={0.35}
className="absolute inset-0"
style={{ borderRadius: 16 }}
pointerEvents="none"
/>
) : null}
{Children.toArray(children).map((child, i) => (
<SwitchOn key={React.isValidElement(child) && child.key != null ? child.key : i} delay={band + rowDelay(i + 1)}>
{child}
</SwitchOn>
))}
</View>
{footer != null && (
<SwitchOn delay={band + rowDelay(Children.count(children) + 1)}>
<Text className="px-4 pt-2 text-[13px] leading-[18px] text-muted-foreground">
{footer}
</Text>
</SwitchOn>
)}
</View>
);
}
return (
<View ref={ref} className={cn("mb-6", inset && "mx-4", className)} {...props}>
{title != null && (
<Text
className={cn(
"px-4 pb-1.5 text-[13px] text-muted-foreground",
titleClassName
)}
accessibilityRole="header"
>
{title}
</Text>
)}
<View
className={cn("overflow-hidden", !glass && "bg-card", (inset || glass) && "rounded-2xl")}
>
{glass ? (
<Glass
effect="regular"
sheen={0.35}
className="absolute inset-0"
style={{ borderRadius: 16 }}
pointerEvents="none"
/>
) : null}
{children}
</View>
{footer != null && (
<Text className="px-4 pt-2 text-[13px] leading-[18px] text-muted-foreground">
{footer}
</Text>
)}
</View>
);
}
);
export interface ListItemProps extends Omit<PressableProps, "style" | "children">, A11yProps {
/** Primary label. */
title: string;
/** Secondary label under the title. */
subtitle?: string;
/** Slot rendered before the text block (icon, avatar, etc.). */
leading?: React.ReactNode;
/** Slot rendered after the text block, before the chevron. */
trailing?: React.ReactNode;
/** Show a disclosure chevron on the trailing edge. */
showChevron?: boolean;
/** Haptic on press. Default "selection"; false to disable. */
haptic?: "light" | "medium" | "heavy" | "selection" | false;
className?: string;
titleClassName?: string;
subtitleClassName?: string;
/** Vertical rhythm: "default" for standard rows, "compact" for dense
* rails (media pickers, followers) where more rows must fit the fold. */
density?: "default" | "compact";
/** Title line clamp. Default 1 — event/news rows that must not lose
* their tail should pass 2. */
titleNumberOfLines?: number;
/** Subtitle line clamp. Default 2. */
subtitleNumberOfLines?: number;
children?: React.ReactNode;
}
const ListItem = forwardRef<React.ElementRef<typeof Pressable>, ListItemProps>(
function ListItem(
{
title,
subtitle,
leading,
trailing,
showChevron = false,
haptic = "selection",
className,
titleClassName,
subtitleClassName,
density = "default",
titleNumberOfLines = 1,
subtitleNumberOfLines = 2,
children,
disabled,
onPress,
onPressIn,
onPressOut,
accessibilityLabel,
accessibilityHint,
accessibilityRole,
accessibilityState,
...props
},
ref
) {
const haptics = useHaptic();
const press = usePressAnimation({ pressedScale: 0.98, fast: true });
const reduce = useReducedMotion();
// Ink wash — a fast fade of the muted-foreground token while the row is
// held. Low opacity keeps it a wash on both schemes.
const ink = useSharedValue(0);
// Lit lists brighten the held row instead: foreground light on dark,
// the accent's light on a white light-scheme card.
const { lit } = useContext(ListInsetContext);
const { scheme } = useTheme();
const washPeak = lit ? LIT_WASH : 0.14;
const inkStyle = useAnimatedStyle(() => ({
opacity: ink.value * washPeak,
}));
const washClass = lit
? scheme === "dark"
? "bg-foreground"
: "bg-primary"
: "bg-muted-foreground";
// Disclosure nudge — the chevron leans 2px right while the row is held,
// a whisper of "this goes somewhere" before the navigation fires.
const chevronX = useSharedValue(0);
const chevronStyle = useAnimatedStyle(() => ({
transform: [{ translateX: chevronX.value }],
}));
const handlePress = (e: GestureResponderEvent) => {
// Static rows have no onPress — never vibrate without an action.
if (haptic && onPress) haptics[haptic]();
onPress?.(e);
};
return (
<AnimatedPressable
ref={ref}
disabled={disabled}
onPress={handlePress}
onPressIn={(e) => {
press.onPressIn();
ink.value = withTiming(1, timing.instant);
if (showChevron && !reduce) chevronX.value = withTiming(2, timing.fast);
onPressIn?.(e);
}}
onPressOut={(e) => {
press.onPressOut();
ink.value = withTiming(0, timing.normal);
if (showChevron && !reduce) chevronX.value = withSpring(0, spring.press);
onPressOut?.(e);
}}
style={press.animatedStyle}
className={cn(
// relative: the absolute ink overlay below clips to this row.
density === "compact"
? "relative min-h-9 w-full flex-row items-center gap-2.5 px-4 py-1.5"
: "relative min-h-11 w-full flex-row items-center gap-3 px-4 py-2.5",
disabled && "opacity-50",
className
)}
{...a11yProps(
{ accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityState },
// Static rows (no onPress) are not interactive — never claim button.
onPress ? "button" : "none"
)}
accessibilityLabel={accessibilityLabel ?? title}
accessibilityState={{ disabled: !!disabled, ...accessibilityState }}
// RNW drops the accessibilityState object — mirror selected/disabled
// as aria-* so web screen readers announce row state (switch pattern).
aria-disabled={!!disabled}
aria-selected={accessibilityState?.selected}
hitSlop={6}
{...props}
>
<Animated.View
pointerEvents="none"
style={inkStyle}
className={cn("absolute inset-0", washClass)}
/>
{leading}
<View className="flex-1 gap-0.5">
<Text
className={cn(
density === "compact" ? "text-sm text-foreground" : "text-base text-foreground",
titleClassName
)}
numberOfLines={titleNumberOfLines}
>
{title}
</Text>
{subtitle != null && (
<Text
className={cn(
density === "compact" ? "text-xs text-muted-foreground" : "text-sm text-muted-foreground",
subtitleClassName
)}
numberOfLines={subtitleNumberOfLines}
>
{subtitle}
</Text>
)}
{children}
</View>
{trailing}
{showChevron && (
<Animated.View
style={chevronStyle}
accessibilityElementsHidden
importantForAccessibility="no"
aria-hidden
>
<Icon name="chevron-right" size="sm" color="mutedForeground" />
</Animated.View>
)}
</AnimatedPressable>
);
}
);
export interface ListSeparatorProps extends ViewProps {
/**
* Left inset preset:
* - omitted (default): 16px — under the text start of a row without a
* leading slot (hairlines never run
* full-bleed inside a section).
* - `"none"`/false: full-bleed.
* - `"icon"`/true: starts under the leading slot edge (16px — the row's
* px-4). Legacy `inset` maps here for pixel parity.
* - `"text"`: aligns under the title text, past a standard 24px leading
* icon (16px padding + 24px icon + 12px gap = 52px).
* - number: exact px inset — match your leading slot's width.
*/
inset?: boolean | "none" | "icon" | "text" | number;
className?: string;
}
const ListSeparator = forwardRef<React.ElementRef<typeof View>, ListSeparatorProps>(
function ListSeparator({ inset, className, style, ...props }, ref) {
const marginClass =
inset === "text"
? // 16px row padding + 24px icon slot + 12px gap.
"ml-[52px]"
: inset === undefined || inset === true || inset === "icon"
? "ml-4"
: inset === false || inset === "none"
? "mx-0"
: undefined;
return (
<View
ref={ref}
className={cn("bg-border", marginClass, className)}
// Numeric inset goes through style — arbitrary px can't be a class.
// True hairline (0.33pt on 3x screens), not a 1pt rule.
style={[{ height: StyleSheet.hairlineWidth }, typeof inset === "number" && { marginLeft: inset }, style]}
accessibilityElementsHidden
importantForAccessibility="no"
aria-hidden
{...props}
/>
);
}
);
export const List = Object.assign(ListRoot, {
Section: ListSection,
Item: ListItem,
Separator: ListSeparator,
});
List.displayName = "List";
ListSection.displayName = "List.Section";
ListItem.displayName = "List.Item";
ListSeparator.displayName = "List.Separator"; import { List } from "@/components/ui/list/list";
export function Example() {
return (
<List />
);
} Installation
Install the dependencies
npx expo install react-native-reanimatedAdd the components it builds on
Copy the source from the Code tab into your project
components/ui/list/list.tsxcomponents/ui/list/index.tsIcons it renders
Update the import paths to match your project setup.
Usage
import { List } from "@/components/ui/list/list"; <List /> Examples
A share-sheet recipient picker on a lit list: rows switch on 40ms apart and brighten under the finger.
import * as React from "react";
import { View } from "react-native";
import Animated, { Easing, useAnimatedProps, useAnimatedStyle, useSharedValue, withSequence, withSpring, withTiming } from "react-native-reanimated";
import Svg from "react-native-svg";
import { Accent, AnimatedPath, ease, spring, timing, useHaptic, useReducedMotion, useTheme } from "@lumioui/core";
import { List } from "../list";
import { Avatar } from "../../avatar/avatar";
import { Button } from "../../button/button";
import { ActionSwapText } from "../../action-swap/action-swap";
import { Text } from "../../text/text";
const PEOPLE = [
{ id: "noor", name: "Noor Haddad", sub: "Mobile · +1 415 555 0182", img: 5, initials: "NH" },
{ id: "kenji", name: "Kenji Watanabe", sub: "[email protected]", img: 13, initials: "KW" },
{ id: "sofia", name: "Sofia Marchetti", sub: "Mobile · +39 347 210 4410", img: 44, initials: "SM" },
{ id: "mateo", name: "Mateo Alvarez", sub: "Last shared Sunday", img: 59, initials: "MA" },
];
/** Check path in a 24 box and its length — the stroke draws along it. */
const CHECK = "M7 12.5 L10.5 16 L17 9";
const CHECK_LEN = 14;
/** The fill ignites over 120ms while the check draws over 240ms (fast + instant). */
const FILL_ON = { duration: timing.recede.duration, easing: Easing.bezier(...ease.out) };
const DRAW = { duration: timing.fast.duration + timing.instant.duration, easing: Easing.bezier(...ease.out) };
const DIM = { ...timing.recede, easing: Easing.bezier(...ease.in) };
/** The Send pill's enabled state crossfades — CTA state is continuous. */
const SHIFT = { ...timing.normal, easing: Easing.bezier(...ease.inOut) };
const SEND_IDLE = 0.5;
/** A refused Send kicks this far, then flickers home. */
const FLICKER_KICK = 4;
/** Pick circle: picking ignites the fill while the check draws on; clearing
* dims it — the check is never un-drawn. */
function PickCircle({ on }: { on: boolean }) {
const { colors } = useTheme();
const fill = useSharedValue(on ? 1 : 0);
const draw = useSharedValue(on ? 1 : 0);
const first = React.useRef(true);
React.useEffect(() => {
if (first.current) {
first.current = false;
return;
}
if (on) {
draw.value = 0;
fill.value = withTiming(1, FILL_ON);
draw.value = withTiming(1, DRAW);
} else {
fill.value = withTiming(0, DIM);
}
}, [on, fill, draw]);
const fillStyle = useAnimatedStyle(() => ({ opacity: fill.value }));
const checkProps = useAnimatedProps(() => ({ strokeDashoffset: CHECK_LEN * (1 - draw.value) }));
return (
<View className="h-6 w-6">
<View className="absolute inset-0 rounded-full border-2 border-border" />
<Animated.View style={fillStyle} className="absolute inset-0 items-center justify-center rounded-full bg-primary">
<Svg width={24} height={24} viewBox="0 0 24 24">
<AnimatedPath
d={CHECK}
stroke={colors.primaryForeground}
strokeWidth={2.6}
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
strokeDasharray={CHECK_LEN}
animatedProps={checkProps}
/>
</Svg>
</Animated.View>
</View>
);
}
/** ListContacts — a share-sheet recipient picker on a lit list: rows switch
* on 40ms apart and brighten under the finger. Picking someone ignites
* their circle while the check draws on; the Send pill's count swaps in
* place with a short rise or fall, and its enabled state crossfades. A
* send with nobody picked flickers. */
export function ListContacts() {
const haptics = useHaptic();
const reduce = useReducedMotion();
const [picked, setPicked] = React.useState<string[]>(["kenji"]);
const toggle = (id: string) =>
setPicked((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id]));
const ready = picked.length > 0;
const lit = useSharedValue(ready ? 1 : SEND_IDLE);
const kick = useSharedValue(0);
React.useEffect(() => {
lit.value = withTiming(ready ? 1 : SEND_IDLE, SHIFT);
}, [ready, lit]);
const sendStyle = useAnimatedStyle(() => ({ opacity: lit.value, transform: [{ translateX: kick.value }] }));
const refuse = () => {
haptics.warning();
if (reduce) {
lit.value = withSequence(withTiming(SEND_IDLE * 0.6, timing.instant), withTiming(SEND_IDLE, SHIFT));
return;
}
kick.value = FLICKER_KICK;
kick.value = withSpring(0, spring.shake);
};
return (
<Accent color="coral" className="w-full">
<View className="w-full overflow-hidden rounded-3xl bg-background pb-4 pt-5">
<View className="gap-1 px-5 pb-4">
<Text className="text-2xl font-bold tracking-tight">Share video</Text>
<Text variant="muted">Spring recital.mov · 2:48 · 184 MB</Text>
</View>
<List inset lit switchOn>
<List.Section title="Suggested" footer="People you share with most often.">
{PEOPLE.map((p, i) => {
const on = picked.includes(p.id);
return (
<React.Fragment key={p.id}>
{i > 0 ? <List.Separator inset={68} /> : null}
<List.Item
title={p.name}
subtitle={p.sub}
subtitleNumberOfLines={1}
leading={
<Avatar
size="md"
source={{ uri: `https://i.pravatar.cc/150?img=${p.img}` }}
fallback={p.initials}
alt={p.name}
/>
}
trailing={<PickCircle on={on} />}
onPress={() => toggle(p.id)}
accessibilityRole="checkbox"
accessibilityState={{ checked: on }}
/>
</React.Fragment>
);
})}
</List.Section>
</List>
<Animated.View style={sendStyle} className="px-4">
<Button
size="lg"
className="w-full"
haptic={ready ? "light" : false}
onPress={() => {
if (!ready) refuse();
}}
accessibilityLabel={ready ? `Send to ${picked.length} ${picked.length === 1 ? "person" : "people"}` : "Choose people"}
accessibilityState={{ disabled: !ready }}
>
{/* The label swaps in place as the count changes. */}
<ActionSwapText value={String(picked.length)}>
<Text className="text-[17px] font-semibold text-primary-foreground">
{picked.length === 0 ? "Choose people" : `Send to ${picked.length} ${picked.length === 1 ? "person" : "people"}`}
</Text>
</ActionSwapText>
</Button>
</Animated.View>
</View>
</Accent>
);
} Playback settings on a glass section over the album art they belong to; the only place glass earns its keep is over content.
import * as React from "react";
import { Image, StyleSheet, View } from "react-native";
import { List } from "../list";
import { Switch } from "../../switch/switch";
import { Text } from "../../text/text";
const ART =
"https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=800&h=1000&fit=crop&q=70&auto=format";
/** ListGlass — playback settings on a glass section over the album art
* they belong to; the only place glass earns its keep is over content. */
export function ListGlass() {
const [lossless, setLossless] = React.useState(true);
const [autoplay, setAutoplay] = React.useState(false);
return (
<View className="relative h-[460px] w-full overflow-hidden rounded-3xl bg-muted">
<Image source={{ uri: ART }} style={StyleSheet.absoluteFill} resizeMode="cover" accessibilityIgnoresInvertColors />
<View className="absolute inset-x-0 bottom-2">
<List glass inset>
<List.Section>
<List.Item
title="Lossless audio"
subtitle={lossless ? "24-bit / 48 kHz on Wi-Fi" : "High efficiency"}
subtitleNumberOfLines={1}
trailing={<Switch checked={lossless} onCheckedChange={setLossless} accessibilityLabel="Lossless audio" />}
/>
<List.Separator />
<List.Item
title="Autoplay similar music"
trailing={<Switch checked={autoplay} onCheckedChange={setAutoplay} accessibilityLabel="Autoplay similar music" />}
/>
<List.Separator />
<List.Item
title="Crossfade"
trailing={<Text className="text-[15px] text-muted-foreground">6 seconds</Text>}
showChevron
onPress={() => {}}
/>
<List.Separator />
<List.Item
title="Downloads"
trailing={<Text className="text-[15px] text-muted-foreground tabular-nums">2.4 GB</Text>}
showChevron
onPress={() => {}}
/>
</List.Section>
</List>
</View>
</View>
);
} An order history screen: an in-transit group above past orders, store marks in colour, totals right-aligned in tabular figures.
import * as React from "react";
import { View } from "react-native";
import { List } from "../list";
import { Icon, type IconName } from "../../icon/icon";
import { Text } from "../../text/text";
type Order = {
id: string;
store: string;
items: string;
status: string;
total: string;
icon: IconName;
tile: string;
};
const ACTIVE: Order[] = [
{ id: "a1", store: "Fernway Coffee", items: "2 bags · Ethiopia Guji", status: "Arriving tomorrow", total: "$38.00", icon: "coffee", tile: "bg-tint-amber" },
];
const PAST: Order[] = [
{ id: "p1", store: "Field & Fern", items: "Monstera, ceramic pot", status: "Delivered Oct 2", total: "$64.50", icon: "leaf", tile: "bg-tint-green" },
{ id: "p2", store: "Paperline", items: "A5 dot notebook ×3", status: "Delivered Sep 24", total: "$27.00", icon: "book-open", tile: "bg-tint-orange" },
{ id: "p3", store: "Halden Running", items: "Trail socks, 3 pack", status: "Returned · refund sent", total: "$18.00", icon: "footprints", tile: "bg-tint-red" },
];
function StoreTile({ icon, tile }: { icon: IconName; tile: string }) {
return (
<View className={`h-11 w-11 items-center justify-center rounded-xl ${tile}`}>
<Icon name={icon} size="md" color="#FFFFFF" />
</View>
);
}
/** ListOrders — an order history screen: an in-transit group above past
* orders, store marks in colour, totals right-aligned in tabular figures.
* The groups switch on a band apart, their rows 40ms apart, and a held
* row brightens rather than shades. */
export function ListOrders() {
const [opened, setOpened] = React.useState<string | null>(null);
const row = (o: Order) => (
<List.Item
key={o.id}
title={o.store}
titleClassName="font-medium"
subtitle={`${o.items}\n${o.status}`}
leading={<StoreTile icon={o.icon} tile={o.tile} />}
trailing={
<Text className="text-[15px] font-medium tabular-nums">{o.total}</Text>
}
showChevron
onPress={() => setOpened(o.id)}
accessibilityState={{ selected: opened === o.id }}
/>
);
return (
<View className="w-full overflow-hidden rounded-3xl bg-background pt-5">
<Text className="px-5 pb-4 text-3xl font-bold tracking-tight">Orders</Text>
<List inset lit switchOn>
<List.Section title="On the way">
{ACTIVE.map(row)}
</List.Section>
<List.Section title="Past orders">
{PAST.map((o, i) => (
<React.Fragment key={o.id}>
{i > 0 ? <List.Separator inset={72} /> : null}
{row(o)}
</React.Fragment>
))}
</List.Section>
</List>
</View>
);
} API reference
List
inset boolean Inset-grouped style: rounded card sections with horizontal margin.
—glass boolean Glass section material (pairs naturally with inset).
—lit boolean Lit press: a held row brightens (foreground light on dark, accent light on light) instead of taking the muted ink wash. Default false.
—switchOn boolean Switch on at mount: each section's rows start dim and switch on 40ms apart, sections one band (120ms) apart. Default false.
—className string — ListSection
title string Optional header text rendered above the group.
—titleClassName string Extra classes for the header text.
—footer string Optional footnote under the group — a short explainer line.
—className string — ListItem
title* string Primary label.
—subtitle string Secondary label under the title.
—leading React.ReactNode Slot rendered before the text block (icon, avatar, etc.).
—trailing React.ReactNode Slot rendered after the text block, before the chevron.
—showChevron boolean Show a disclosure chevron on the trailing edge.
—haptic "light" | "medium" | "heavy" +2
"light" | "medium" | "heavy" | "selection" | false Haptic on press. Default "selection"; false to disable.
—className string — titleClassName string — subtitleClassName string — density "default" | "compact" Vertical rhythm: "default" for standard rows, "compact" for dense rails (media pickers, followers) where more rows must fit the fold.
—titleNumberOfLines number Title line clamp. Default 1 — event/news rows that must not lose their tail should pass 2.
—subtitleNumberOfLines number Subtitle line clamp. Default 2.
—children React.ReactNode — ListSeparator
inset boolean | "none" | "icon" | "text" | number Left inset preset: - omitted (default): 16px — under the text start of a row without a leading slot (hairlines never run full-bleed inside a section). - "none"/false: full-bleed. - "icon"/true: starts under the leading slot edge (16px — the row's px-4). Legacy inset maps here for pixel parity. - "text": aligns under the title text, past a standard 24px leading icon (16px padding + 24px icon + 12px gap = 52px). - number: exact px inset — match your leading slot's width.
className string —