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.
Button Group
Row of related buttons joined by shared borders. It can also work as a single-choice control, with an accent pill that glides to whichever option you pick.
Preview
import * as React from "react";
import { forwardRef, Children, isValidElement, cloneElement } from "react";
import { Text, View, type LayoutChangeEvent, type ViewProps } from "react-native";
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
} from "react-native-reanimated";
import { cn, ease, spring, timing, useReducedMotion } from "@lumioui/core";
/**
* ButtonGroup — a horizontal row of connected Buttons sharing borders.
* <ButtonGroup>
* <Button variant="outline">Left</Button>
* <Button variant="outline">Right</Button>
* </ButtonGroup>
* Pass `attached={false}` to render children with a gap instead.
*
* Selection: pass `value` / `defaultValue` (each child's React `key` is its
* value) and the group becomes one control — a single rounded track with
* short dividers between the answers, and an inset pill that glides to the
* chosen one on the indicator spring while the label ink follows its
* coverage on the UI thread. Dividers beside the pill dim away so the pill
* never sits against a straight line.
* <ButtonGroup value={rsvp} onValueChange={setRsvp}>
* <Button key="yes" variant="outline">Going</Button> …
* </ButtonGroup>
*/
export interface ButtonGroupProps extends ViewProps {
/** Attach buttons together (shared borders, merged radius). Default true. */
attached?: boolean;
/** Orientation of the group. Default "horizontal". */
orientation?: "horizontal" | "vertical";
/** Disable every child in the group. */
disabled?: boolean;
className?: string;
/** Extra classes merged into each child element that accepts className. */
itemClassName?: string;
/** Selected child (matched against each child's React `key`). Controlled. */
value?: string | null;
/** Initial selection when uncontrolled. */
defaultValue?: string | null;
/** Fires when a child is pressed in selection mode, after its own onPress. */
onValueChange?: (value: string) => void;
children?: React.ReactNode;
}
type ChildShape = {
className?: string;
disabled?: boolean;
pressedScale?: number;
variant?: string;
size?: "sm" | "md" | "lg" | "icon";
textClassName?: string;
children?: React.ReactNode;
onPress?: (...args: never[]) => unknown;
onLayout?: (e: LayoutChangeEvent) => void;
accessibilityState?: Record<string, unknown>;
};
/** Label sizes mirror Button's own label scale so the lit copy sits exactly on it. */
const LABEL_SIZE = { sm: "text-sm", md: "text-base", lg: "text-[17px]", icon: "text-base" } as const;
/** Inset between the track's edge and the gliding pill (p-1). */
const TRACK_INSET = 4;
/** Divider height between answers — short, so it reads as a seam, not a wall. */
const DIVIDER_H = 18;
/** A seam between two answers; it dims away when either neighbour is chosen. */
function Divider({ x, hidden }: { x: number; hidden: boolean }) {
const on = useSharedValue(hidden ? 0 : 1);
React.useEffect(() => {
on.value = withTiming(hidden ? 0 : 1, hidden ? { ...timing.recede, easing: Easing.bezier(...ease.in) } : { ...timing.fast, easing: Easing.bezier(...ease.out) });
}, [hidden, on]);
const style = useAnimatedStyle(() => ({ opacity: on.value }));
return (
<Animated.View
pointerEvents="none"
className="absolute bg-border"
style={[{ left: x, width: 1, height: DIVIDER_H, top: "50%", marginTop: -DIVIDER_H / 2 }, style]}
/>
);
}
/** Children.toArray prefixes keys (".$yes") — the value is the author's key. */
const valueOf = (key: React.Key | null) => (key == null ? null : String(key).replace(/^\.\$/, ""));
type Rect = { x: number; w: number };
export const ButtonGroup = forwardRef<React.ElementRef<typeof View>, ButtonGroupProps>(
function ButtonGroup(
{
attached = true,
orientation = "horizontal",
disabled,
className,
itemClassName,
value: controlled,
defaultValue,
onValueChange,
children,
accessibilityLabel,
"aria-label": ariaLabel,
...props
},
ref
) {
const reduce = useReducedMotion();
const horizontal = orientation === "horizontal";
const items = Children.toArray(children).filter(isValidElement);
const count = items.length;
const selecting = controlled !== undefined || defaultValue !== undefined || onValueChange != null;
const [internal, setInternal] = React.useState<string | null>(defaultValue ?? null);
const value = controlled !== undefined ? controlled : internal;
const select = (v: string) => {
if (controlled === undefined) setInternal(v);
onValueChange?.(v);
};
// The gliding fill needs every label as text and a horizontal weld; other
// shapes fall back to the selected child switching to the filled variant.
const labels = items.map((c) => (c.props as ChildShape).children);
const glides =
selecting && attached && horizontal && labels.every((l) => typeof l === "string" || typeof l === "number");
const [rects, setRects] = React.useState<(Rect | undefined)[]>([]);
const measure = (i: number) => (e: LayoutChangeEvent) => {
const { x, width } = e.nativeEvent.layout;
setRects((prev) => {
const cur = prev[i];
if (cur && cur.x === x && cur.w === width) return prev;
const next = prev.slice();
next[i] = { x, w: width };
return next;
});
};
const selIndex = value == null ? -1 : items.findIndex((c) => valueOf(c.key) === value);
const target = selIndex >= 0 ? rects[selIndex] : undefined;
// Indicator — the light glides; label ink follows the fill because the lit
// labels live inside the fill's clipping window.
const x = useSharedValue(0);
const w = useSharedValue(0);
const on = useSharedValue(0);
const placed = React.useRef(false);
React.useEffect(() => {
if (!glides) return;
if (!target) {
on.value = withTiming(0, { ...timing.recede, easing: Easing.bezier(...ease.in) });
placed.current = false;
return;
}
if (!placed.current || reduce) {
x.value = target.x;
w.value = target.w;
} else {
x.value = withSpring(target.x, spring.indicator);
w.value = withSpring(target.w, spring.indicator);
}
placed.current = true;
on.value = withTiming(1, { ...timing.fast, easing: Easing.bezier(...ease.out) });
}, [glides, target, reduce, x, w, on]);
const windowStyle = useAnimatedStyle(() => ({
opacity: on.value,
width: w.value,
transform: [{ translateX: x.value }],
}));
const innerStyle = useAnimatedStyle(() => ({ transform: [{ translateX: -x.value }] }));
// Group-level disabled propagates down; a child's own `disabled` is
// respected when the group prop is unset.
const childDisabled = (childProps: ChildShape) =>
disabled === true ? true : childProps.disabled;
const selectionProps = (child: React.ReactElement, i: number): Partial<ChildShape> => {
if (!selecting) return {};
const childProps = child.props as ChildShape;
const v = valueOf(child.key);
const selected = v != null && v === value;
return {
onPress: ((...args: never[]) => {
childProps.onPress?.(...args);
if (v != null) select(v);
}) as ChildShape["onPress"],
onLayout: (e: LayoutChangeEvent) => {
measure(i)(e);
childProps.onLayout?.(e);
},
accessibilityState: { ...childProps.accessibilityState, selected },
// Without the glide the picked child carries the fill itself.
...(glides ? null : { variant: selected ? "default" : childProps.variant }),
};
};
const content = glides
? // One track: the answers are transparent pills inside it; the track
// draws the outline and the seams, the gliding pill draws the choice.
items.map((child, i) => {
const childProps = child.props as ChildShape;
return cloneElement(child as React.ReactElement<ChildShape>, {
className: cn(childProps.className, "rounded-full", itemClassName),
disabled: childDisabled(childProps),
pressedScale: 1,
...selectionProps(child, i),
variant: "ghost",
});
})
: attached
? items.map((child, i) => {
const isFirst = i === 0;
const isLast = i === count - 1;
const positional = horizontal
? cn(
"rounded-none",
isFirst && "rounded-l-full",
isLast && "rounded-r-full",
!isFirst && "-ml-px"
)
: cn(
"rounded-none",
isFirst && "rounded-t-full",
isLast && "rounded-b-full",
!isFirst && "-mt-px"
);
const childProps = child.props as ChildShape;
return cloneElement(child as React.ReactElement<ChildShape>, {
className: cn(positional, childProps.className, itemClassName),
disabled: childDisabled(childProps),
// A scaled segment would open a gap in the weld — attached
// children press via ink/sheen only.
pressedScale: 1,
...selectionProps(child, i),
});
})
: itemClassName || disabled != null || selecting
? items.map((child, i) => {
const childProps = child.props as ChildShape;
// cloneElement preserves the existing key — only classes merge.
return cloneElement(child as React.ReactElement<ChildShape>, {
className: cn(childProps.className, itemClassName),
disabled: childDisabled(childProps),
...selectionProps(child, i),
});
})
: items;
return (
<View
ref={ref}
className={cn(
horizontal ? "flex-row" : "flex-col",
!attached && "gap-2",
glides && "p-1",
className
)}
// A related-action cluster is a group, not a toolbar — toolbars
// imply arrow-key navigation semantics this control doesn't provide.
// `role` carries the full ARIA union (accessibilityRole has no "group").
role={selecting ? "radiogroup" : "group"}
accessibilityLabel={accessibilityLabel ?? ariaLabel}
aria-label={ariaLabel ?? accessibilityLabel}
accessibilityState={{ disabled: disabled ?? false }}
{...props}
>
{glides ? (
<View pointerEvents="none" className="absolute inset-0 rounded-full border border-border" />
) : null}
{glides
? rects.slice(1).map((r, k) =>
r ? <Divider key={k} x={r.x} hidden={selIndex === k || selIndex === k + 1} /> : null
)
: null}
{content}
{glides ? (
<Animated.View
pointerEvents="none"
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
className="absolute left-0 overflow-hidden rounded-full bg-primary"
style={[{ top: TRACK_INSET, bottom: TRACK_INSET }, windowStyle]}
>
<Animated.View className="absolute left-0" style={[{ top: -TRACK_INSET, bottom: -TRACK_INSET }, innerStyle]}>
{items.map((child, i) => {
const r = rects[i];
if (!r) return null;
const cp = child.props as ChildShape;
return (
<View
key={i}
className="absolute bottom-0 top-0 items-center justify-center"
style={{ left: r.x, width: r.w }}
>
<Text
className={cn(
"font-semibold text-primary-foreground",
LABEL_SIZE[cp.size ?? "md"],
cp.textClassName
)}
numberOfLines={1}
>
{labels[i] as string}
</Text>
</View>
);
})}
</Animated.View>
</Animated.View>
) : null}
</View>
);
}
);
ButtonGroup.displayName = "ButtonGroup"; import { ButtonGroup } from "@/components/ui/button-group/button-group";
export function Example() {
return (
<ButtonGroup />
);
} Installation
Install the dependencies
npx expo install react-native-reanimatedCopy the source from the Code tab into your project
components/ui/button-group/button-group.tsxcomponents/ui/button-group/index.tsUpdate the import paths to match your project setup.
Usage
import { ButtonGroup } from "@/components/ui/button-group/button-group"; <ButtonGroup /> Examples
An attached RSVP group on a calendar invite.
import * as React from "react";
import { Image, View } from "react-native";
import Animated, { Easing, useAnimatedStyle, useSharedValue, withSpring, withTiming } from "react-native-reanimated";
import { Accent, ease, spring, timing, useReducedMotion } from "@lumioui/core";
import { ButtonGroup } from "../button-group";
import { Button } from "../../button/button";
import { Icon } from "../../icon/icon";
import { Text } from "../../text/text";
type Rsvp = "yes" | "maybe" | "no";
const ANSWERS: { key: Rsvp; label: string; note: string }[] = [
{ key: "yes", label: "Going", note: "Tomás will see you're going" },
{ key: "maybe", label: "Maybe", note: "Marked tentative on your calendar" },
{ key: "no", label: "Decline", note: "Removed from your calendar" },
];
const GUESTS = [14, 33, 51, 68];
/** Swapped lines ignite from this scale (swap grammar). */
const SWAP_FROM = 0.96;
/** The confirmation under the group swaps in place — the old line dims, the
* new one ignites — and then stays as the evidence of the answer. */
function SwapLine({ text }: { text: string }) {
const reduce = useReducedMotion();
const lit = useSharedValue(1);
const grow = useSharedValue(1);
const first = React.useRef(true);
React.useEffect(() => {
if (first.current) {
first.current = false;
return;
}
lit.value = 0;
lit.value = withTiming(1, { ...timing.fast, easing: Easing.bezier(...ease.out) });
if (reduce) return;
grow.value = SWAP_FROM;
grow.value = withSpring(1, spring.swap);
}, [text, reduce, lit, grow]);
const style = useAnimatedStyle(() => ({ opacity: lit.value, transform: [{ scale: grow.value }] }));
return (
<Animated.Text
className="text-center text-sm text-muted-foreground"
style={style}
accessibilityLiveRegion="polite"
>
{text}
</Animated.Text>
);
}
/** ButtonGroupActions — an attached RSVP group on a calendar invite. The
* three answers sit in one rounded track: an inset orange pill glides to
* the chosen answer with the label ink following it, and the line under it
* swaps in place to confirm. */
export function ButtonGroupActions() {
const [answer, setAnswer] = React.useState<Rsvp | null>(null);
const picked = ANSWERS.find((a) => a.key === answer);
return (
<Accent color="orange" className="w-full">
<View className="w-full gap-5 rounded-3xl bg-card p-5">
<View className="gap-1">
<Text variant="muted">Thu, Oct 16 · 2:30 – 3:15 PM</Text>
<Text className="text-2xl font-bold tracking-tight">Pricing page crit</Text>
</View>
<View className="gap-3">
<View className="flex-row items-center gap-3">
<Icon name="map-pin" size="sm" color="mutedForeground" />
<Text className="text-[15px]">Room 4B, Pier House</Text>
</View>
<View className="flex-row items-center gap-3">
<Icon name="users" size="sm" color="mutedForeground" />
<View className="flex-row">
{GUESTS.map((g, i) => (
<Image
key={g}
source={{ uri: `https://i.pravatar.cc/150?img=${g}` }}
className={`h-7 w-7 rounded-full border-2 border-card bg-muted ${i > 0 ? "-ml-2" : ""}`}
accessibilityIgnoresInvertColors
/>
))}
</View>
<Text variant="muted">Tomás Herrera + 5</Text>
</View>
</View>
<View className="gap-2">
<ButtonGroup
className="w-full"
accessibilityLabel="RSVP"
value={answer}
onValueChange={(v) => setAnswer(v as Rsvp)}
>
{ANSWERS.map((a) => (
<Button key={a.key} variant="outline" className="flex-1">
{a.label}
</Button>
))}
</ButtonGroup>
<SwapLine text={picked ? picked.note : "Reply by tomorrow, 5:00 PM"} />
</View>
</View>
</Accent>
);
} Detached groups as a barber's open slots.
import * as React from "react";
import { Image, View } from "react-native";
import { ButtonGroup } from "../button-group";
import { Button } from "../../button/button";
import { Icon } from "../../icon/icon";
import { Text } from "../../text/text";
const ROWS: string[][] = [
["12:30", "1:15", "2:00"],
["2:45", "3:30", "4:15"],
["5:00", "5:45", "6:30"],
];
const TAKEN = new Set(["1:15", "3:30", "5:45"]);
/** ButtonGroupSlots — detached groups as a barber's open slots. Each row is
* a ButtonGroup with gaps; taken slots are disabled, the picked one fills
* ink and the booking CTA carries the time and price. Once booked, the
* chosen slot and the confirmation stay at full strength — only the other
* slots dim, so the answer never reads as disabled. */
export function ButtonGroupSlots() {
const [slot, setSlot] = React.useState<string | null>("4:15");
const [booked, setBooked] = React.useState(false);
const book = async () => {
if (booked) return;
await new Promise((r) => setTimeout(r, 1200));
setBooked(true);
};
return (
<View className="w-full gap-5 rounded-3xl bg-card p-5">
<View className="flex-row items-center gap-3">
<Image
source={{ uri: "https://i.pravatar.cc/150?img=15" }}
className="h-12 w-12 rounded-full bg-muted"
accessibilityIgnoresInvertColors
/>
<View className="flex-1">
<Text className="text-[17px] font-semibold">Skin fade + beard trim</Text>
<Text variant="muted">with Marcus · 45 min · Fri afternoon</Text>
</View>
</View>
<View className="gap-2">
{ROWS.map((row) => (
<ButtonGroup key={row[0]} attached={false} className="w-full" itemClassName="flex-1">
{row.map((t) => (
<Button
key={t}
size="md"
variant={slot === t ? "default" : "secondary"}
disabled={TAKEN.has(t) || (booked && slot !== t)}
onPress={() => {
if (!booked) setSlot(t);
}}
textClassName="tabular-nums"
accessibilityState={{ selected: slot === t }}
>
{t}
</Button>
))}
</ButtonGroup>
))}
</View>
<Button
size="lg"
className="w-full"
disabled={!slot}
onPress={book}
icon={booked ? <Icon name="check" size="sm" color="primaryForeground" /> : undefined}
accessibilityState={{ disabled: booked }}
>
{booked ? `Booked for ${slot} PM` : slot ? `Book ${slot} PM · $38` : "Pick a time"}
</Button>
</View>
);
} API reference
attached boolean Attach buttons together (shared borders, merged radius). Default true.
true orientation "horizontal" | "vertical" Orientation of the group. Default "horizontal".
"horizontal" disabled boolean Disable every child in the group.
—className string — itemClassName string Extra classes merged into each child element that accepts className.
—value string | null Selected child (matched against each child's React key). Controlled.
defaultValue string | null Initial selection when uncontrolled.
—onValueChange (value: string) => void Fires when a child is pressed in selection mode, after its own onPress.
—children React.ReactNode —