Component recipe · Actions & controls
Icon Button
44px icon-only action; an accessible label is required.
Web · liveiOS · native pathAndroid · native path
This is the real BeeUI Web Showcase, lazy-loaded in place. The code tab displays the exact typechecked runtime fixture selected for this preview.
apps/showcase/component-gallery/component-gallery.tsx
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
AlertBanner,
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogTrigger,
AppHeader,
Avatar,
Badge,
BeeThemeScope,
BottomActionBar,
Box,
Breadcrumb,
BreadcrumbItem,
Button,
Card,
Checkbox,
Chip,
ChipGroup,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
DescriptionItem,
DescriptionList,
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
DialogTrigger,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
EmptyState,
ErrorState,
Field,
FormGroup,
HStack,
IconButton,
Input,
Link,
ListGroup,
ListGroupHeader,
ListItem,
OTPInput,
Pagination,
PaginationItem,
PasswordInput,
Popover,
PopoverClose,
PopoverContent,
PopoverDescription,
PopoverTitle,
PopoverTrigger,
Progress,
Radio,
RadioGroup,
SafeArea,
Screen,
SearchInput,
Section,
SegmentedControl,
SegmentedControlItem,
Separator,
SettingsItem,
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetTitle,
SheetTrigger,
Skeleton,
Spinner,
Stack,
Stat,
StatHelpText,
StatLabel,
StatValue,
Stepper,
StepperItem,
Switch,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Text,
Textarea,
Timeline,
TimelineItem,
Tooltip,
TooltipContent,
TooltipTrigger,
useToast,
VStack,
} from '@beemvp/beeui-ui';
import * as React from 'react';
import { ScrollView, StatusBar } from 'react-native';
import { Uniwind, useUniwind } from 'uniwind';
import { DatePickerShowcase } from './date-picker-showcase';
import { DateTimePickerShowcase } from './date-time-picker-showcase';
import { SelectShowcase } from './select-showcase';
import { TableShowcase } from './table-showcase';
function ThemeToggle() {
const { hasAdaptiveThemes, theme } = useUniwind();
const activeTheme = hasAdaptiveThemes ? 'system' : theme;
const nextTheme = activeTheme === 'system' ? 'light' : activeTheme === 'light' ? 'dark' : 'system';
return (
<Button
accessibilityLabel={`Theme ${activeTheme}. Switch to ${nextTheme}`}
onPress={() => Uniwind.setTheme(nextTheme)}
size="sm"
testID="component-gallery-theme-toggle"
variant="outline"
>
{`Theme: ${activeTheme}`}
</Button>
);
}
function PlaygroundHeading({ children, description }: { children: string; description: string }) {
return (
<VStack gap="xs">
<Text variant="heading">{children}</Text>
<Text tone="muted">{description}</Text>
</VStack>
);
}
function PlacementPopover({ placement }: { placement: 'top' | 'right' | 'bottom' | 'left' }) {
return (
<Popover>
<PopoverTrigger size="sm" variant="outline">
{placement}
</PopoverTrigger>
<PopoverContent placement={placement} testID={`popover-${placement}-content`}>
<PopoverTitle>{`${placement[0].toUpperCase()}${placement.slice(1)} placement`}</PopoverTitle>
<PopoverDescription>
This surface is positioned by the shared anchored-overlay geometry kernel.
</PopoverDescription>
{placement === 'bottom' ? (
<Field label="Note">
<Input accessibilityLabel="Popover note" testID="popover-demo-input" />
</Field>
) : null}
<PopoverClose size="sm" variant="ghost">
Close
</PopoverClose>
</PopoverContent>
</Popover>
);
}
// Consumer React context declared below BeeUIProvider. Overlay content must
// resolve the provided value, not the default — the web regression asserts this.
const OverlayConsumerContext = React.createContext('overlay-context-default');
function OverlayContextValue({ testID }: { testID: string }) {
return <Text testID={testID}>{`context: ${React.useContext(OverlayConsumerContext)}`}</Text>;
}
// CASE C — scope-ordering proof for real-browser Escape. A single control opens,
// with independent open states, a root-scope Popover AND a Dialog whose nested
// DropdownMenu is also open. They live in different overlay scopes, so their
// registration order is irrelevant by construction — Escape routes to the active
// modal scope (the menu), never the root Popover behind it. (Strict "root opened
// after menu" ordering with real timing is covered deterministically in the jest
// CASE B/D contract tests; a click inside the modal cannot be used here because
// the open menu's dismiss layer intercepts pointer events.)
function CaseCScopeOrdering() {
const [dialogOpen, setDialogOpen] = React.useState(false);
const [menuOpen, setMenuOpen] = React.useState(false);
const [rootOpen, setRootOpen] = React.useState(false);
return (
<>
<Button
onPress={() => {
setDialogOpen(true);
setMenuOpen(true);
setRootOpen(true);
}}
testID="overlay-context-casec-open"
variant="outline"
>
Open CASE C
</Button>
<Popover onOpenChange={setRootOpen} open={rootOpen}>
<PopoverTrigger testID="overlay-context-casec-root-trigger" variant="outline">
CASE C root
</PopoverTrigger>
<PopoverContent placement="bottom">
<OverlayContextValue testID="overlay-context-casec-root-value" />
</PopoverContent>
</Popover>
<Dialog onOpenChange={setDialogOpen} open={dialogOpen}>
<DialogTrigger testID="overlay-context-casec-dialog-trigger">CASE C dialog</DialogTrigger>
<DialogContent>
<DialogTitle>CASE C dialog</DialogTitle>
<DropdownMenu onOpenChange={setMenuOpen} open={menuOpen}>
<DropdownMenuTrigger testID="overlay-context-casec-menu-trigger" variant="outline">
CASE C menu
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>CASE C menu</DropdownMenuLabel>
<OverlayContextValue testID="overlay-context-casec-menu-value" />
</DropdownMenuContent>
</DropdownMenu>
</DialogContent>
</Dialog>
</>
);
}
function ConsumerContextOverlays() {
const [dialogMenuAction, setDialogMenuAction] = React.useState('none');
return (
<OverlayConsumerContext.Provider value="preserved">
<VStack gap="sm">
<Popover>
<PopoverTrigger testID="overlay-context-popover-trigger" variant="outline">
Popover context
</PopoverTrigger>
<PopoverContent placement="bottom">
<PopoverTitle>Popover consumer context</PopoverTitle>
<OverlayContextValue testID="overlay-context-popover-value" />
</PopoverContent>
</Popover>
<DropdownMenu>
<DropdownMenuTrigger testID="overlay-context-menu-trigger" variant="outline">
Menu context
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Menu consumer context</DropdownMenuLabel>
<OverlayContextValue testID="overlay-context-menu-value" />
</DropdownMenuContent>
</DropdownMenu>
<Tooltip>
<TooltipTrigger testID="overlay-context-tooltip-trigger" variant="outline">
Tooltip context
</TooltipTrigger>
<TooltipContent>
<OverlayContextValue testID="overlay-context-tooltip-value" />
</TooltipContent>
</Tooltip>
<Dialog>
<DialogTrigger testID="overlay-context-dialog-trigger">Dialog context</DialogTrigger>
<DialogContent>
<DialogTitle>Dialog with a nested overlay</DialogTitle>
<Popover>
<PopoverTrigger testID="overlay-context-dialog-popover-trigger" variant="outline">
Popover in dialog
</PopoverTrigger>
<PopoverContent placement="bottom">
<OverlayContextValue testID="overlay-context-dialog-popover-value" />
</PopoverContent>
</Popover>
<Tooltip>
<TooltipTrigger testID="overlay-context-dialog-tooltip-trigger" variant="outline">
Tooltip in dialog
</TooltipTrigger>
<TooltipContent>
<OverlayContextValue testID="overlay-context-dialog-tooltip-value" />
</TooltipContent>
</Tooltip>
<DropdownMenu>
<DropdownMenuTrigger testID="overlay-context-dialog-menu-trigger" variant="outline">
Menu in dialog
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Menu in dialog</DropdownMenuLabel>
<OverlayContextValue testID="overlay-context-dialog-menu-value" />
<DropdownMenuItem
onSelect={() => setDialogMenuAction('selected')}
testID="overlay-context-dialog-menu-item"
>
Select in dialog
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Text testID="overlay-context-dialog-menu-action">{`menu action: ${dialogMenuAction}`}</Text>
</DialogContent>
</Dialog>
<CaseCScopeOrdering />
</VStack>
</OverlayConsumerContext.Provider>
);
}
// #68 — real-browser proof that a BeeThemeScope declared around an overlay
// trigger is still resolved for that overlay's portaled content on web (the
// `ReactDOM.createPortal` transport). The Playwright regression
// (apps/visual-regression/tests/overlay-context.spec.ts) asserts these testIDs
// against the real Uniwind runtime, the same way issue #35 proves consumer
// context survives the same transport.
function ThemeScopeValue({ testID }: { testID: string }) {
const { theme } = useUniwind();
return <Text testID={testID}>{`theme: ${theme}`}</Text>;
}
function ThemeScopeOverlays() {
return (
<VStack gap="sm">
<BeeThemeScope appearance="dark" brand="violet">
<VStack gap="sm">
<ThemeScopeValue testID="theme-scope-root-value" />
<Popover>
<PopoverTrigger testID="theme-scope-popover-trigger" variant="outline">
Popover in scope
</PopoverTrigger>
<PopoverContent placement="bottom">
<PopoverTitle>Popover in a BeeThemeScope</PopoverTitle>
<ThemeScopeValue testID="theme-scope-popover-value" />
</PopoverContent>
</Popover>
<DropdownMenu>
<DropdownMenuTrigger testID="theme-scope-menu-trigger" variant="outline">
Menu in scope
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Menu in a BeeThemeScope</DropdownMenuLabel>
<ThemeScopeValue testID="theme-scope-menu-value" />
</DropdownMenuContent>
</DropdownMenu>
<Dialog>
<DialogTrigger testID="theme-scope-dialog-trigger">Dialog in scope</DialogTrigger>
<DialogContent>
<DialogTitle>Dialog inside a BeeThemeScope</DialogTitle>
<ThemeScopeValue testID="theme-scope-dialog-value" />
<DialogClose size="sm" variant="ghost">
Close
</DialogClose>
</DialogContent>
</Dialog>
<BeeThemeScope appearance="light" brand="bee">
<ThemeScopeValue testID="theme-scope-nested-value" />
</BeeThemeScope>
</VStack>
</BeeThemeScope>
<ThemeScopeValue testID="theme-scope-sibling-value" />
</VStack>
);
}
function ToastPlayground() {
const toast = useToast();
const [lastAction, setLastAction] = React.useState('No Toast action yet');
return (
<Card className="gap-4" variant="raised">
<Section
description="Provider-scoped transient notifications. Up to three are visible; overflow waits FIFO. Close visible items to inspect queue promotion."
title="Toast notifications"
>
<HStack gap="sm" wrap>
<Button
onPress={() => toast.show({ title: 'Saved', description: 'Default toast with a 5 second timeout.' })}
size="sm"
variant="outline"
>
Default
</Button>
<Button
onPress={() => toast.show({ title: 'Published', description: 'Changes are live.', variant: 'success' })}
size="sm"
variant="outline"
>
Success
</Button>
<Button
onPress={() => toast.show({ title: 'Check settings', description: 'One option needs attention.', variant: 'warning' })}
size="sm"
variant="outline"
>
Warning
</Button>
<Button
onPress={() => toast.show({ title: 'Save failed', description: 'Try the operation again.', variant: 'destructive' })}
size="sm"
variant="outline"
>
Error
</Button>
<Button
onPress={() => toast.show({
title: 'Item archived',
description: 'You can undo this action.',
variant: 'info',
action: { label: 'Undo', onPress: () => setLastAction('Undo pressed') },
})}
size="sm"
variant="outline"
>
Action
</Button>
<Button
onPress={() => toast.show({ title: 'Persistent notice', description: 'Dismiss this explicitly.', duration: 'persistent', variant: 'info' })}
size="sm"
variant="outline"
>
Persistent
</Button>
<Button
onPress={() => {
for (let index = 1; index <= 6; index += 1) {
toast.show({
title: `Queued toast ${index}`,
description: index <= 3 ? 'Initially visible.' : 'Promotes FIFO after a visible toast closes.',
duration: 'persistent',
variant: index % 2 === 0 ? 'info' : 'neutral',
});
}
}}
size="sm"
variant="secondary"
>
Queue stress
</Button>
<Button onPress={toast.dismissAll} size="sm" variant="ghost">Dismiss all</Button>
</HStack>
<Text tone="muted" variant="caption">{lastAction}</Text>
</Section>
</Card>
);
}
export function ComponentGallery({ onBack }: { onBack: () => void }) {
const { theme } = useUniwind();
const [accepted, setAccepted] = React.useState(false);
const [notifications, setNotifications] = React.useState(true);
const [plan, setPlan] = React.useState<'starter' | 'pro'>('starter');
const [tab, setTab] = React.useState('overview');
const [otp, setOtp] = React.useState('');
const [filters, setFilters] = React.useState<string[]>(['mobile']);
const [viewMode, setViewMode] = React.useState('list');
const [page, setPage] = React.useState(2);
const [step, setStep] = React.useState(3);
const [menuToolbar, setMenuToolbar] = React.useState(true);
const [menuDensity, setMenuDensity] = React.useState('comfortable');
const [menuAction, setMenuAction] = React.useState('No action yet');
return (
<Screen testID="component-gallery">
<StatusBar barStyle={theme === 'dark' ? 'light-content' : 'dark-content'} />
<SafeArea className="bg-surface" edges={['top', 'left', 'right']}>
<AppHeader
description="Interactive React Native component playground built entirely from the public BeeUI API."
testID="component-gallery-header"
leading={
<HStack gap="sm">
<Button
accessibilityLabel="Back to Showcase home"
testID="component-gallery-back"
onPress={onBack}
size="sm"
variant="ghost"
>
Back
</Button>
<Avatar accessibilityLabel="BeeUI" fallback="BU" />
</HStack>
}
title="Component Gallery"
trailing={<ThemeToggle />}
/>
</SafeArea>
{/*
#281 — `min-h-24` is a rem-scaled floor (matches the row/text
scaling contract, so it keeps pace at any zoom) that stops this
flex:1 scroll region from being squeezed toward zero when the
pinned AppHeader above and BottomActionBar below both grow at large
text / zoom. Without it, once combined header+bar height leaves
less room than a single row needs, `scrollIntoView` can only
partially reveal that row, and hit-testing at its (unclipped)
geometric center lands on the BottomActionBar's real on-screen
rect — reported as the bar "intercepting pointer events" for
content that's actually just been squeezed out of its own
viewport. The floor keeps the scroll region a real, usable size;
on the rare device/zoom combination where total content still
exceeds the viewport, the page falls back to ordinary
document-level scrolling instead of corrupting hit-testing for
in-view content.
*/}
<SafeArea className="flex-1 min-h-24" edges={['left', 'right']}>
<ScrollView contentContainerStyle={{ paddingBottom: 120 }}>
<Box className="mx-auto w-full max-w-3xl gap-10 px-5 py-8">
<AlertBanner
description="Everything below is rendered from the public @beemvp/beeui-ui API. Switch themes, open overlays, change form state, and resize the web window to exercise the same contracts used on native."
title="Hands-on playground"
variant="info"
/>
<PlaygroundHeading description="Buttons, semantic surfaces, feedback, and loading states.">
Foundation
</PlaygroundHeading>
<Card className="gap-4" variant="raised">
<Section
action={<IconButton accessibilityLabel="Add item" variant="outline">+</IconButton>}
description="Variants, disabled state, and loading behavior."
title="Actions"
>
<Box className="gap-3">
<Button>Primary action</Button>
<Button variant="secondary">Secondary action</Button>
<Button variant="outline">Outline action</Button>
<Button variant="ghost">Ghost action</Button>
<Button variant="destructive">Destructive action</Button>
<Button disabled>Disabled action</Button>
<Button loading>Loading action</Button>
</Box>
</Section>
</Card>
<Card className="gap-4" variant="muted">
<Text variant="heading">Status and feedback</Text>
<Box className="flex-row flex-wrap gap-2">
<Badge>Primary</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="destructive">Error</Badge>
<Badge variant="info">Info</Badge>
</Box>
<Separator />
<Box className="flex-row items-center gap-5">
<Spinner />
<Spinner tone="success" />
<Spinner tone="warning" />
<Spinner tone="destructive" />
</Box>
<Progress accessibilityLabel="Profile completion" value={72} />
</Card>
<ToastPlayground />
<Card className="gap-4">
<Text variant="heading">Loading and state surfaces</Text>
<Box className="flex-row items-center gap-3">
<Skeleton className="h-12 w-12" variant="circle" />
<Box className="flex-1 gap-2">
<Skeleton className="w-2/3" variant="text" />
<Skeleton className="w-full" variant="text" />
</Box>
</Box>
<Skeleton className="h-24 w-full" />
<Separator />
<EmptyState
action={<Button size="sm">Create record</Button>}
description="Create your first record to get started."
title="No records yet"
/>
<Separator />
<ErrorState
action={<Button size="sm" variant="outline">Try again</Button>}
description="The server could not load this section."
/>
</Card>
<PlaygroundHeading description="Text-entry composition plus explicit group semantics for related choices.">
Forms
</PlaygroundHeading>
<Card className="gap-4">
<Field description="Used only for account notifications." label="Email" required>
<Input autoCapitalize="none" placeholder="you@example.com" />
</Field>
<Field label="Search">
<SearchInput onSearch={() => undefined} placeholder="Search projects" />
</Field>
<Field label="Password">
<PasswordInput placeholder="Enter password" />
</Field>
<Field description="Six numeric digits." label="Verification code">
<OTPInput
accessibilityLabel="Verification code"
onValueChange={setOtp}
value={otp}
/>
</Field>
<Field error="Enter a valid project name." invalid label="Project name">
<Input placeholder="Invalid value" />
</Field>
<Field disabled label="Managed field">
<Input placeholder="Disabled by field context" />
</Field>
<Field description="Optional long-form content." label="Notes">
<Textarea placeholder="Long-form notes" />
</Field>
<Separator />
<Checkbox checked={accepted} label="Accept terms" onCheckedChange={setAccepted} />
<FormGroup
description="The group owns legend/guidance metadata while each radio stays independently discoverable."
legend="Subscription plan"
required
>
<RadioGroup
onValueChange={(value) => setPlan(value as 'starter' | 'pro')}
value={plan}
>
<Radio label="Starter plan" value="starter" />
<Radio label="Pro plan" value="pro" />
</RadioGroup>
</FormGroup>
<Box className="flex-row items-center justify-between gap-4">
<Text>Notifications</Text>
<Switch
accessibilityLabel="Notifications"
onValueChange={setNotifications}
value={notifications}
/>
</Box>
</Card>
<PlaygroundHeading description="Persistent value selection with anchored overlay transport, keyboard behavior on web, and modal-local nesting.">
Select
</PlaygroundHeading>
<SelectShowcase />
<PlaygroundHeading description="Composable primitives (no owned data/columns) — real HTML table semantics on web, keyboard-reachable sort, and caller-owned row selection.">
Table
</PlaygroundHeading>
<TableShowcase />
<PlaygroundHeading description="Field-integrated trigger; Web opens Calendar in a Popover, native delegates to the system picker.">
DatePicker
</PlaygroundHeading>
<DatePickerShowcase />
<PlaygroundHeading description="Field-integrated trigger; composes DatePicker's date part with an Input/SegmentedControl time control on Web, native delegates to the system picker (chained date+time steps on Android).">
DateTimePicker
</PlaygroundHeading>
<DateTimePickerShowcase />
<PlaygroundHeading description="Modal and anchored overlays now have real public APIs you can click through here.">
Overlay playground
</PlaygroundHeading>
<Card className="gap-5" variant="raised">
<Section
description="Centered modal-class overlays use React Native core Modal."
title="Dialog and AlertDialog"
>
<HStack gap="sm" wrap>
<Dialog>
<DialogTrigger>Open Dialog</DialogTrigger>
<DialogContent>
<DialogTitle>Project settings</DialogTitle>
<DialogDescription>
This is BeeUI's centered modal surface. Backdrop, accessibility escape, and native request-close follow the Dialog contract.
</DialogDescription>
<Field label="Project name">
<Input defaultValue="BeeUI" />
</Field>
<DialogFooter>
<DialogClose variant="outline">Cancel</DialogClose>
<DialogClose>Save changes</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog>
<AlertDialogTrigger variant="destructive">Delete project</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
<AlertDialogDescription>
The backdrop cannot dismiss this confirmation. Choose an explicit action instead.
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Delete permanently</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</HStack>
</Section>
<Separator />
<Section
description="Bottom-sheet surface (#159): BeeUI's own Web overlay/focus primitives — Escape, backdrop press, Tab focus-trap, and focus restoration — with no native Modal and no gorhom on Web (ADR-006)."
title="Sheet"
>
<Sheet>
<SheetTrigger testID="sheet-demo-trigger">Open Sheet</SheetTrigger>
<SheetContent overlayTestID="sheet-demo-overlay" testID="sheet-demo-content">
<SheetTitle>Filters</SheetTitle>
<SheetDescription>Refine results by category and price.</SheetDescription>
<Field label="Search">
<Input accessibilityLabel="Sheet search" testID="sheet-demo-input" />
</Field>
<SheetFooter>
<SheetClose testID="sheet-demo-close" variant="outline">
Close
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
</Section>
<Separator />
<Section
description="Popover uses the shared non-Modal host, window-coordinate measurement, collision handling, and topmost dismissal stack."
title="Popover placements"
>
<HStack gap="sm" wrap>
<PlacementPopover placement="top" />
<PlacementPopover placement="right" />
<PlacementPopover placement="bottom" />
<PlacementPopover placement="left" />
</HStack>
</Section>
<Separator />
<Section
description="Menu items reuse the anchored runtime with normal, checkbox, radio, disabled, and keyboard-selection contracts."
title="DropdownMenu"
>
<VStack gap="sm">
<DropdownMenu>
<DropdownMenuTrigger variant="outline">Workspace menu</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Workspace</DropdownMenuLabel>
<DropdownMenuItem onSelect={() => setMenuAction('Edit project')}>
Edit project
</DropdownMenuItem>
<DropdownMenuItem disabled>Archive unavailable</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
checked={menuToolbar}
onCheckedChange={setMenuToolbar}
>
Show toolbar
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>Density</DropdownMenuLabel>
<DropdownMenuRadioGroup
onValueChange={setMenuDensity}
value={menuDensity}
>
<DropdownMenuRadioItem value="compact">Compact</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="comfortable">
Comfortable
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<Text tone="muted">
{`Last action: ${menuAction} · toolbar ${menuToolbar ? 'on' : 'off'} · ${menuDensity}`}
</Text>
</VStack>
</Section>
<Separator />
<Section
description="Non-interactive contextual disclosure (#152/#153/#154): hover or focus reveals a short description after a delay; the trigger's own press action is never intercepted, Escape dismisses without moving focus, and content is never a Tab stop."
title="Tooltip"
>
<HStack gap="sm" wrap>
<Tooltip>
<TooltipTrigger testID="tooltip-demo-trigger" variant="outline">
Autosave
</TooltipTrigger>
<TooltipContent testID="tooltip-demo-content">
Saved automatically every 30 seconds
</TooltipContent>
</Tooltip>
</HStack>
</Section>
<Separator />
<Section
description="Open the parent, then the child. Outside press / Escape / accessibility escape dismisses child-first."
title="Nested Popover"
>
<Popover>
<PopoverTrigger variant="secondary">Open parent</PopoverTrigger>
<PopoverContent align="start" placement="bottom">
<PopoverTitle>Parent Popover</PopoverTitle>
<PopoverDescription>
The child registers above this overlay in the shared dismiss stack.
</PopoverDescription>
<Popover>
<PopoverTrigger size="sm" variant="outline">Open child</PopoverTrigger>
<PopoverContent align="start" placement="right">
<PopoverTitle>Child Popover</PopoverTitle>
<PopoverDescription>
Dismiss me first; the parent remains mounted underneath.
</PopoverDescription>
<PopoverClose size="sm">Done</PopoverClose>
</PopoverContent>
</Popover>
</PopoverContent>
</Popover>
</Section>
<Separator />
<Section
description="This trigger is aligned to the container edge. Shrink the web viewport or rotate a device to make flip/shift collision handling obvious."
title="Collision edge case"
>
<Box className="items-end">
<Popover>
<PopoverTrigger variant="outline">Near right edge</PopoverTrigger>
<PopoverContent align="start" placement="right" sideOffset={12}>
<PopoverTitle>Collision-aware placement</PopoverTitle>
<PopoverDescription>
Preferred placement is right; the resolver flips or shifts only when the viewport requires it.
</PopoverDescription>
<PopoverClose size="sm" variant="ghost">Close</PopoverClose>
</PopoverContent>
</Popover>
</Box>
</Section>
<Separator />
<Section
description="Consumer React context declared below BeeUIProvider resolves inside overlay content on web, native, and inside a Dialog."
title="Consumer context"
>
<ConsumerContextOverlays />
</Section>
<Separator />
<Section
description="BeeThemeScope (#68) is a thin typed wrapper over Uniwind's own ScopedTheme. The scoped brand/appearance below resolves for Popover, DropdownMenu, and Dialog content declared inside it, nests independently of an inner scope, and never leaks to the sibling value outside it."
title="Scoped theme (BeeThemeScope)"
>
<ThemeScopeOverlays />
</Section>
</Card>
<PlaygroundHeading description="Selections, tabs, disclosure, paging, and application-level composition stay router-neutral.">
Navigation and composition
</PlaygroundHeading>
<Card className="gap-4">
<Section description="Dependency-free filters, view selection, and paging." title="Selection and navigation">
<Text variant="label">Filters</Text>
<ChipGroup
onValueChange={(value) => setFilters(Array.isArray(value) ? value : [value])}
selectionMode="multiple"
value={filters}
>
<Chip value="mobile">Mobile</Chip>
<Chip value="web">Web</Chip>
<Chip value="design">Design</Chip>
</ChipGroup>
<Text variant="label">View</Text>
<SegmentedControl onValueChange={setViewMode} value={viewMode}>
<SegmentedControlItem value="list">List</SegmentedControlItem>
<SegmentedControlItem value="grid">Grid</SegmentedControlItem>
</SegmentedControl>
<Text variant="label">Page</Text>
<Pagination onPageChange={setPage} page={page} pageCount={4}>
<PaginationItem type="previous" />
<PaginationItem page={1} />
<PaginationItem page={2} />
<PaginationItem page={3} />
<PaginationItem page={4} />
<PaginationItem type="next" />
</Pagination>
</Section>
</Card>
<Card className="gap-4">
<Text variant="heading">Tabs and disclosure</Text>
<Tabs onValueChange={setTab} value={tab}>
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="details">Details</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<Text tone="muted">Overview content is mounted for the active tab.</Text>
</TabsContent>
<TabsContent value="details">
<Text tone="muted">Details content is mounted only when selected.</Text>
</TabsContent>
</Tabs>
<Separator />
<Collapsible>
<CollapsibleTrigger>Advanced options</CollapsibleTrigger>
<CollapsibleContent>
<Text tone="muted">Hidden until expanded.</Text>
</CollapsibleContent>
</Collapsible>
<Accordion defaultValue="account">
<AccordionItem value="account">
<AccordionTrigger>Account</AccordionTrigger>
<AccordionContent>
<Text tone="muted">Account preferences and identity.</Text>
</AccordionContent>
</AccordionItem>
<AccordionItem value="billing">
<AccordionTrigger>Billing</AccordionTrigger>
<AccordionContent>
<Text tone="muted">Invoices and payment settings.</Text>
</AccordionContent>
</AccordionItem>
</Accordion>
</Card>
<Card className="gap-5">
<Section
description="Layout, history, and application composition without router ownership."
title="Application composition"
>
<Breadcrumb accessibilityLabel="Project breadcrumb">
<BreadcrumbItem onPress={() => undefined}>Projects</BreadcrumbItem>
<BreadcrumbItem current>BeeUI</BreadcrumbItem>
</Breadcrumb>
<Stack gap="lg">
<HStack gap="lg" wrap>
<Stat className="min-w-32 flex-1">
<StatLabel>API surface</StatLabel>
<StatValue>Public</StatValue>
<StatHelpText>Only @beemvp/beeui-ui exports</StatHelpText>
</Stat>
<Stat className="min-w-32 flex-1">
<StatLabel>Verification</StatLabel>
<StatValue>Cross-platform</StatValue>
<StatHelpText>Web, Android, and iOS gates</StatHelpText>
</Stat>
</HStack>
<Stepper currentStep={step} onStepChange={setStep}>
<StepperItem step={1} title="Foundation" />
<StepperItem step={2} title="Application patterns" />
<StepperItem step={3} title="Overlays" />
</Stepper>
<ListGroup>
<ListGroupHeader
description="Composition reuses existing row behavior."
title="Workspace"
/>
<ListItem
description="Portable component system"
onPress={() => undefined}
title="BeeUI"
/>
</ListGroup>
<Timeline>
<TimelineItem
description="Core layout, form, navigation, and modal contracts established."
meta="v0.1"
status="success"
title="Foundation"
/>
<TimelineItem
description="Packed packages bundle in Expo and bare React Native; Android APK compiles in CI."
meta="CI verified"
status="success"
title="Native portability"
/>
<TimelineItem
description="Public Popover and DropdownMenu exercise the shared anchored overlay geometry/runtime kernels."
meta="Current"
status="primary"
title="Anchored overlays"
/>
</Timeline>
</Stack>
</Section>
</Card>
<Card className="gap-4">
<Section description="Read-only application information patterns." title="Metadata and rows">
<DescriptionList>
<DescriptionItem label="Runtime" value="React Native 0.86.2" />
<DescriptionItem label="Styling" value="Uniwind 1.10.1" />
<DescriptionItem
description="Generated and compiled in CI"
label="Native verification"
value="Expo + bare RN"
/>
</DescriptionList>
<Separator />
<ListItem
description="Open your profile"
onPress={() => undefined}
title="Profile"
trailing={<Badge variant="success">Active</Badge>}
/>
<SettingsItem
description="Changes app appearance"
onPress={() => undefined}
title="Appearance"
value={theme}
/>
<SettingsItem
description="Native preference control"
title="Push notifications"
trailing={
<Switch
accessibilityLabel="Push notifications"
onValueChange={setNotifications}
value={notifications}
/>
}
/>
</Section>
</Card>
<VStack gap="xs">
<Text tone="muted" variant="caption">
Navigation remains application-owned. This showcase intentionally has no router or docs-site framework.
</Text>
<Link onPress={() => undefined}>Open documentation</Link>
</VStack>
</Box>
</ScrollView>
</SafeArea>
<SafeArea className="bg-surface" edges={['bottom', 'left', 'right']}>
<BottomActionBar>
<Button size="sm" variant="ghost">Cancel</Button>
<Button size="sm">Save changes</Button>
</BottomActionBar>
</SafeArea>
</Screen>
);
}
Composition
Public anatomy
IconButton
The recipe uses the public @beemvp/beeui-ui boundary. Source-owned consumers can use pnpm beeui -- add icon-button from a BeeUI checkout.
Contract
What remains canonical
Behavior, accessibility, provider requirements, density/theming and platform limitations stay authoritative in the component docs. This recipe is a fast preview-and-copy surface, not a second API contract.
Read Icon Button docs →