New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@thewhileloop/whileui

Package Overview
Dependencies
Maintainers
1
Versions
22
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@thewhileloop/whileui

WhileUI Native — Copy-paste components for React Native. You own the code.

latest
Source
npmnpm
Version
1.2.3
Version published
Weekly downloads
134
2580%
Maintainers
1
Weekly downloads
 
Created
Source

WhileUI Native

Copy-paste components for React Native. You own the code.

Beautiful, accessible, themeable components built with Uniwind + Tailwind CSS v4.

Requirements: React 19+, React Native 0.81+, Expo 52+ (if using Expo). Supports React Native Web.

Installation

bun add @thewhileloop/whileui
bun add react@^19.0.0 react-native@^0.81.0 uniwind@^1.0.0 tailwindcss@^4.0.0 react-native-reanimated react-native-safe-area-context react-native-screens
# Only if you use Select, Popover, Tooltip, or HoverCard:
bun add @rn-primitives/portal @rn-primitives/hooks @rn-primitives/slot @rn-primitives/select @rn-primitives/popover @rn-primitives/tooltip @rn-primitives/hover-card

Use npm install instead of bun add if you prefer npm.

Setup Uniwind (required)

  • metro.config.js (wrap with withUniwindConfig):
const { withUniwindConfig } = require('uniwind/metro');

module.exports = withUniwindConfig({
  cssEntryFile: './global.css',
})({
  // your metro config
});

withUniwindConfig must be the outermost wrapper. cssEntryFile must be a relative path string.

  • global.css at app root:
@import 'tailwindcss';
@import 'uniwind';

/* WhileUI Noir theme - copy this or create your own */
@layer theme {
  :root {
    @variant light {
      --color-background: oklch(1 0 0);
      --color-foreground: oklch(0.1316 0.0041 17.69);
      --color-card: oklch(1 0 0);
      --color-card-foreground: oklch(0.1316 0.0041 17.69);
      --color-primary: oklch(0.1316 0.0041 17.69);
      --color-primary-foreground: oklch(0.98 0 0);
      --color-secondary: oklch(0.9598 0.0017 17.69);
      --color-secondary-foreground: oklch(0.1316 0.0041 17.69);
      --color-muted: oklch(0.9598 0.0017 17.69);
      --color-muted-foreground: oklch(0.5415 0.0135 17.69);
      --color-accent: oklch(0.9598 0.0017 17.69);
      --color-accent-foreground: oklch(0.1316 0.0041 17.69);
      --color-destructive: oklch(0.5 0.19 27);
      --color-destructive-foreground: oklch(0.98 0 0);
      --color-border: oklch(0.9039 0.0034 17.69);
      --color-input: oklch(0.9039 0.0034 17.69);
      --color-ring: oklch(0.1316 0.0041 17.69);
      --color-success: oklch(0.59 0.16 145);
      --color-success-foreground: oklch(0.98 0 0);
      --color-warning: oklch(0.75 0.18 85);
      --color-warning-foreground: oklch(0.1316 0.0041 17.69);
      --color-info: oklch(0.65 0.15 245);
      --color-info-foreground: oklch(0.98 0 0);
    }

    @variant dark {
      --color-background: oklch(0.1316 0.0041 17.69);
      --color-foreground: oklch(0.98 0 0);
      --color-card: oklch(0.1316 0.0041 17.69);
      --color-card-foreground: oklch(0.98 0 0);
      --color-primary: oklch(0.98 0 0);
      --color-primary-foreground: oklch(0.1316 0.0041 17.69);
      --color-secondary: oklch(0.2104 0.0084 17.69);
      --color-secondary-foreground: oklch(0.98 0 0);
      --color-muted: oklch(0.2104 0.0084 17.69);
      --color-muted-foreground: oklch(0.6961 0.0174 17.69);
      --color-accent: oklch(0.2104 0.0084 17.69);
      --color-accent-foreground: oklch(0.98 0 0);
      --color-destructive: oklch(0.45 0.18 27);
      --color-destructive-foreground: oklch(0.98 0 0);
      --color-border: oklch(0.2104 0.0084 17.69);
      --color-input: oklch(0.2104 0.0084 17.69);
      --color-ring: oklch(0.8267 0.0206 17.69);
      --color-success: oklch(0.59 0.16 145);
      --color-success-foreground: oklch(0.98 0 0);
      --color-warning: oklch(0.75 0.18 85);
      --color-warning-foreground: oklch(0.1316 0.0041 17.69);
      --color-info: oklch(0.65 0.15 245);
      --color-info-foreground: oklch(0.98 0 0);
    }
  }
}
  • App.tsx:
import './global.css';
import { PortalHost } from '@thewhileloop/whileui';
import { Button, ButtonText } from '@thewhileloop/whileui';

export default function App() {
  return (
    <>
      {/* Your app content */}
      <PortalHost />
    </>
  );
}

Note: If you use Select, Popover, Tooltip, or HoverCard, add <PortalHost /> at the root of your app (as the last child).

Vite + React Native Web + WhileUI setup

Use the WhileUI Vite compatibility helper when your app includes portal-based primitives.

  • Install Vite + RN web basics:
bun add react-dom react-native-web
bun add -d vite @vitejs/plugin-react
  • vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { withWhileUIViteCompat } from '@thewhileloop/whileui/vite';

export default defineConfig(withWhileUIViteCompat({ plugins: [react()] }));
  • Keep PortalHost at app root when using Select/Popover/Tooltip/HoverCard.

Troubleshooting:

SymptomLikely causeFix
Unexpected token < from @rn-primitives/*/dist/*.mjsrn-primitives ships raw JSX in dist for some packagesUse withWhileUIViteCompat(...) in vite.config.ts
className rejected on RN primitives in consumer TS appRN className augmentation not loaded from package entryImport components from @thewhileloop/whileui (or side-effect import the package entry before use)
Theme token resolves differently on native/webMissing --app-color-* fallback for non-RN-native valuesAdd --app-color-* fallbacks in global.css for any oklch(...) token used by native APIs

Usage

import {
  Button,
  ButtonText,
  Card,
  CardHeader,
  CardTitle,
  CardContent,
  Input,
  Text,
} from '@thewhileloop/whileui';

function MyScreen() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Welcome</CardTitle>
      </CardHeader>
      <CardContent>
        <Input placeholder="Email" />
        <Button className="mt-4">
          <ButtonText>Continue</ButtonText>
        </Button>
      </CardContent>
    </Card>
  );
}

Blocks Strategy: Core vs Templates

Core package exports:

  • Primitives — Button, Input, Card, Text, etc.
  • Generic layout blocks — EmptyState, ErrorState, LoadingScreen, ContentSkeleton, PageSkeleton, ScreenSkeleton
  • Layout infrastructure — FormModalScreen, ConfirmActionSheet, SmartInput, ActionBar
  • Navigation, Chat, Lists, Commerce, Media, DatePicker — Blocks that rarely need app-specific customization

Opinionated blocks (auth, profile) are copy-paste templates in the showcase app:

  • Auth: apps/showcase/templates/auth/ — SignInForm, SignUpForm, ForgotPasswordForm, ResetPasswordForm, VerifyEmailForm, SocialConnections, UserMenu
  • Profile: apps/showcase/templates/profile/ — ProfileHeader, SettingsSection, SettingsItem, AccountCard

To use auth or profile blocks: Copy the template file(s) from apps/showcase/templates/ into your app. Customize as needed (error handling, loading state, social auth slot, branding). Each template imports primitives from @thewhileloop/whileui.

Philosophy

  • Copy-Paste Ownership — Components live in your project. No node_modules lock-in.
  • Beautiful by Default — OKLCH color system, light/dark themes, polished out of the box.
  • Fully Customizable — Every component uses tv() variants and accepts className overrides.
  • Accessible — Proper ARIA roles, keyboard support, controlled/uncontrolled state.
  • Tree-Shakeable — Only imports what you use. sideEffects: false.

Quick Reference (AI / Code Generation)

  • Full-screen: AppShell + Header in header + BottomNav in bottomNav + content in children
  • No-jump loading: keep AppShell mounted, set loading + skeleton (e.g., PageSkeleton/ScreenSkeleton)
  • Layout: Stack (vertical), Row (horizontal) — both support gap, align, justify
  • Auth callbacks: Auth templates use objects: onSubmit({ email, password }), onSubmit({ firstName, lastName, email, password }), etc. Copy templates from apps/showcase/templates/auth/.
  • PortalHost: Add <PortalHost /> at app root for Select, Popover, Tooltip, HoverCard.
  • Uniwind: withUniwindConfig must wrap metro config. global.css at app root, imported in App.tsx.
  • Reference: Block props in packages/ui/src/blocks (core) and apps/showcase/templates/ (auth, profile); flow patterns in README "Flow Patterns" section.

Components

Primitives

ComponentNotes
TextThemed text with variant support
ViewThemed view wrapper
PressableThemed pressable wrapper
StackVertical flex layout with gap
RowHorizontal flex layout with gap
BoxFlexible container with variants

Form Controls

ComponentVariantsNotes
Buttondefault, destructive, outline, secondary, ghost, link4 sizes, ButtonText & ButtonIcon sub-components
Inputdefault, errorTextInput wrapper with themed styling
NumericInputdefault, errorNumeric input with prefix/suffix slots, optional steppers, and compact size
OTPInputdefault, error; default, compactVerification code input with auto-focus, paste, secure mask, loading skeleton, error shake
FormFielddefault, compactCompound API: FormField, FormLabel, FormControl, FormHint, FormMessage
LabeledFielddefault, compactField wrapper with label/helper/error plus left/right slots
TextareaMulti-line text input
CheckboxControlled/uncontrolled, accessibility roles
SwitchControlled/uncontrolled, accessibility roles
RadioGroupRadioGroup + RadioGroupItem
SelectUses SelectOption type {value, label}. Includes SelectGroup, SelectLabel, SelectSeparator
LabelForm field label
SegmentedControldefault, pill; single selectSegmentedControl, SegmentedControlItem, SegmentedControlItemText with wrapping layout support
Toggledefault, outlineToggleText sub-component
ToggleGroupsingle, multipleGroup of toggle items

Display

ComponentVariantsNotes
Cardpadding (none, sm, default, lg), unstyledCompound: Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter
Badgedefault, secondary, destructive, outline, successBadgeText sub-component
Alertdefault, destructive, success, warningAlertTitle & AlertDescription
Avatarsm, default, lgAvatarImage + AvatarFallback
DataRowdefault, compactDataRow, DataRowLeft/Center/Right, DataRowLabel/Description/Value
Separatorhorizontal, verticalThemed divider
Progresssm, default, lgValue-based progress bar with accessibility
Spinnersm, default, lgActivityIndicator wrapper
Skeletonpulse, shimmerLoading placeholder (pulse = opacity fade, shimmer = sweep)
AspectRatioMaintain aspect ratio container

Layout

ComponentNotes
TabsTabsList, TabsTrigger, TabsContent
AccordionAccordionItem, AccordionTrigger, Content
CollapsibleCollapsibleTrigger, CollapsibleContent

Overlays & Menus

ComponentNotes
DialogModal dialog with Header, Footer, Title, Description
AlertDialogConfirmation dialog with Action/Cancel buttons
PopoverPosition-aware popover (requires PortalHost)
TooltipPosition-aware tooltip (requires PortalHost)
DropdownMenuDropdownMenuTrigger, Content, Item, Label, Separator
ContextMenuLong-press context menu
HoverCardPosition-aware hover card (requires PortalHost)
MenubarHorizontal menu bar

Feedback

ComponentNotes
ToastToastProvider, ToastContainer, useToast() hook

Blocks (Pre-built Screens)

Auth (Copy from showcase templates)

Copy from apps/showcase/templates/auth/:

BlockFileDescription
SignInFormsign-in-form.tsxEmail/password sign in with callbacks
SignUpFormsign-up-form.tsxRegistration form with callbacks
ForgotPasswordFormforgot-password-form.tsxPassword reset request
ResetPasswordFormreset-password-form.tsxSet new password
VerifyEmailFormverify-email-form.tsxEmail verification code input
SocialConnectionssocial-connections.tsxOAuth provider buttons
UserMenuuser-menu.tsxProfile dropdown for auth flows

Navigation

BlockDescription
AppShellLayout shell with header/footer/bottomNav slots
NavigationSidebarSidebar nav with grouped sections and footer slot
HeaderTop app bar with back/actions
BottomNavTab-style bottom navigation bar
FloatingBottomNavElevated bottom nav with safe area support
TabBarTop tab bar with indicator
DrawerMenuDrawer with sections and items

Layout

BlockDescription
ActionBarSticky bottom action row with safe-area padding
ConfirmActionSheetReusable destructive confirmation sheet
SheetBottom sheet modal with header/content/footer slots
FormModalScreenModal scaffold for forms with loading states
ContentSkeletonPage/content placeholder with variants (list, card, generic)
PageSkeletonVariant-based page layouts (dashboard, list, settings, card, generic)
ScreenSkeletonHeader slot/placeholder + PageSkeleton content in one block
ErrorBoundaryReact ErrorBoundary that renders ErrorState by default
EmptyStateEmpty content placeholder
ErrorStateError display with retry
LoadingScreenFull-screen loading indicator
PullToRefreshScrollViewThemed ScrollView with RefreshControl (useThemeTokens for colors)
SmartInputKeyboard-aware compose input: left/center/right slots, bar/card variant
OnboardingScreenOnboarding flow screen
SplashScreenBranded splash (fade/scale/slide variants)
MinimalSplashMinimal monochrome splash
BrandedSplashSplash with brand imagery

Chat

BlockDescription
ChatAI-style chat: messages, suggestions, SmartInput
ChatMessageBubbleMessage bubble (user/assistant, big/small text)
ChatSuggestionsSuggestion chips when empty

Profile & Settings (Copy from showcase templates)

Copy from apps/showcase/templates/profile/:

BlockFileDescription
ProfileHeaderprofile-header.tsxProfile header with stats
AccountCardaccount-card.tsxAccount summary card
SettingsSectionsettings-section.tsxSection header with optional action
SettingsItemsettings-item.tsxRow for toggles/links/settings

Lists

BlockDescription
ListItemTitle/subtitle row
NotificationItemNotification row with metadata
SwipeableItemSwipe actions list item
TimelineFeedVertical feed with connecting lines

Commerce

BlockDescription
ProductCardProduct card with badge/media
PricingCardPricing tiers with feature list
CheckoutSummaryCart summary with line items
MetricCardStats/progress card for dashboards
SubscriptionCardCurrent plan card with manage/upgrade actions
FeatureGateLocked feature state with upgrade CTA
UsageBarQuota meter with warning/exceeded states
PlanToggleMonthly vs annual plan switcher
UpgradeBannerInline upgrade banner with action/dismiss

Media

BlockDescription
SmartImageImage with aspect ratio and loading

Date Picker

BlockDescription
DatePickerModalBottom sheet modal with calendar, compact trigger
DatePickerInlineInline calendar for forms or dashboards
DateRangePickerModalRange selection modal with period marking

Layout Primitives (Stack, Row, Box)

Use Stack for vertical layouts, Row for horizontal layouts. Both support gap, align, and justify variants.

import { Stack, Row, Box } from '@thewhileloop/whileui';

<Stack gap="lg">
  <Text>Title</Text>
  <Row gap="md" justify="between">
    <Button>
      <ButtonText>Cancel</ButtonText>
    </Button>
    <Button>
      <ButtonText>Save</ButtonText>
    </Button>
  </Row>
</Stack>;
PropStack/RowValues
gap'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'Spacing between children
align'start' | 'center' | 'end' | 'stretch' | 'baseline'Cross-axis alignment
justify'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'Main-axis alignment

Box provides optional padding and margin variants for consistent spacing.

Full-Screen Composition

Standard pattern: AppShell with header, scrollable children, and bottomNav.

import { AppShell, Header, BottomNav, ScrollView } from '@thewhileloop/whileui';

<AppShell
  header={<Header title="Home" rightActions={[...]} />}
  bottomNav={
    <BottomNav
      items={[...]}
      activeKey="home"
      onSelect={(key) => setTab(key)}
    />
  }
>
  <ScrollView className="flex-1 p-4">
    {/* Screen content */}
  </ScrollView>
</AppShell>

Flow Patterns

FlowBlocks
AuthSignInForm → SignUpForm → ForgotPasswordForm → VerifyEmailForm → ResetPasswordForm
FormsFormField + Input/NumericInput/LabeledField + DatePickerModal + FormModalScreen
SettingsProfileHeader + SettingsSection + SettingsItem (+ FormModalScreen for edits)
E-commerceProductCard list → CheckoutSummary + ActionBar
ChatChat + ChatSuggestions + SmartInput (attach, send). Extensible for images/tags
App shellAppShell + Header + BottomNav + content
LoadingAppShell (loading) + PageSkeleton/ScreenSkeleton (keep header mounted)

Component-Level Loading

Many blocks accept a loading prop that renders a skeleton placeholder matching the component's own shape. No separate skeleton component needed — the block knows its own layout.

<ProductCard loading title="" price="" />
<ListItem loading title="" />
<NotificationItem loading title="" message="" time="" />
<MetricCard loading label="" value="" />
<SubscriptionCard loading planName="" price="" />
<PricingCard loading name="" price="" features={[]} />

Blocks with loading support: ProductCard, ListItem, NotificationItem, MetricCard, SubscriptionCard, PricingCard, AppShell, Chat, FormModalScreen, CheckoutSummary.

Block props: see TypeScript interfaces in packages/ui/src/blocks.

Quick Start

bun install
cd apps/showcase
bun run dev
# Or: npx expo start --web

Project Structure

whileui/
├── packages/
│   └── ui/
│       └── src/
│           ├── components/    # All components (copy these!)
│           │   ├── button/
│           │   ├── card/
│           │   ├── form-field/
│           │   ├── numeric-input/
│           │   ├── segmented-control/
│           │   ├── data-row/
│           │   ├── dialog/
│           │   └── ...
│           ├── blocks/        # Pre-built screens (core only)
│           │   ├── chat/
│           │   ├── navigation/
│           │   ├── layout/
│           │   ├── lists/
│           │   ├── commerce/
│           │   ├── splash/
│           │   └── media/
│           ├── lib/           # Utilities
│           │   ├── cn.ts      # clsx + tailwind-merge
│           │   ├── tv.ts      # tailwind-variants re-export
│           │   ├── font-context.ts
│           │   └── theme-bridge.ts
│           └── index.ts       # Barrel export
├── apps/
│   └── showcase/              # Expo demo app
│       ├── templates/         # Copy-paste templates (auth, profile)
│       │   ├── auth/
│       │   └── profile/
│       ├── App.tsx            # Component showcase
│       ├── global.css         # Theme variables (OKLCH) — at app root!
│       └── metro.config.js    # Uniwind + monorepo config
└── package.json               # bun monorepo root

Theming

Themes are defined in global.css using CSS variables with OKLCH colors:

@variant light {
  --color-primary: oklch(0.6 0.2 160);
  --color-background: oklch(1 0 0);
  /* ... */
}

@variant dark {
  --color-primary: oklch(0.6 0.2 160);
  --color-background: oklch(0.145 0 0);
  /* ... */
}

Strict Theme Token Contract

The WhileUI token contract is strict for cross-app reuse. Define these in every theme variant (@variant light, @variant dark, and custom variants):

  • Required core tokens: background, foreground, card, card-foreground, popover, popover-foreground, primary, primary-foreground, secondary, secondary-foreground, muted, muted-foreground, accent, accent-foreground, destructive, destructive-foreground, border, input, ring
  • Optional status tokens: success, success-foreground, warning, warning-foreground, info, info-foreground
  • Optional effect tokens: overlay, overlay-strong, surface-elevated, surface-border, surface-highlight, surface-translucent, surface-translucent-border, state-hover, state-pressed, state-disabled
  • Optional interaction/motion tokens: --ui-press-opacity, --ui-press-opacity-strong, --ui-disabled-opacity, --ui-disabled-opacity-soft, --ui-disabled-opacity-subtle, --ui-inactive-opacity, --ui-motion-fast, --ui-motion-normal, --ui-motion-slow, --ui-drawer-open-duration, --ui-drawer-close-duration, --ui-blur-intensity-subtle, --ui-blur-intensity-medium, --ui-blur-intensity-strong, --ui-blur-saturation-pct, --ui-frosted-highlight-height, --ui-frosted-backdrop-blur-intensity, --ui-frosted-backdrop-blur-scale, --ui-frosted-android-experimental-blur, --ui-drawer-frosted-inset, --ui-drawer-frosted-radius, --ui-drawer-content-top-padding
  • Optional scale tokens: spacing (--spacing, --spacing-*), typography (--text-*, --leading-*, --tracking-*), radius (--radius-*), elevation (--shadow-*)

Minimal contract example:

@layer theme {
  :root {
    @variant light {
      --color-background: oklch(1 0 0);
      --color-foreground: oklch(0.15 0 0);
      --color-card: oklch(1 0 0);
      --color-card-foreground: oklch(0.15 0 0);
      --color-popover: oklch(1 0 0);
      --color-popover-foreground: oklch(0.15 0 0);
      --color-primary: oklch(0.2 0 0);
      --color-primary-foreground: oklch(0.98 0 0);
      --color-secondary: oklch(0.95 0 0);
      --color-secondary-foreground: oklch(0.15 0 0);
      --color-muted: oklch(0.95 0 0);
      --color-muted-foreground: oklch(0.45 0 0);
      --color-accent: oklch(0.9 0.05 180);
      --color-accent-foreground: oklch(0.15 0 0);
      --color-destructive: oklch(0.58 0.2 26);
      --color-destructive-foreground: oklch(0.98 0 0);
      --color-border: oklch(0.9 0 0);
      --color-input: oklch(0.92 0 0);
      --color-ring: oklch(0.22 0 0);
    }
  }
}

Switch themes at runtime via Uniwind:

import { Uniwind } from 'uniwind';

Uniwind.setTheme('dark'); // or 'light' or 'system'

Frosted / Translucent Theme

Some apps want a frosted or translucent look for floating panels (modals, sheets, toolbars). WhileUI stays neutral — no "glass" in core token names. Apps that want this effect opt in by overriding surface tokens in their theme.

Option A — Override in existing light/dark: In your @variant light and @variant dark blocks, set surface tokens to semi-transparent values:

@variant light {
  /* ... other tokens ... */
  --color-surface-elevated: oklch(0.98 0.01 95 / 0.4);
  --color-surface-border: oklch(1 0 0 / 0.25);
  --color-surface-highlight: oklch(1 0 0 / 0.4);
}

Option B — Separate frosted theme: Register extraThemes: ['frosted'] and define :root.frosted with the same structure as your base theme, but with translucent surface values. Then Uniwind.setTheme('frosted') when desired.

Optional: Add expo-blur and register its BlurView once at app startup for full frosted blur. For tint-only (no blur), translucent surface tokens are sufficient.

import { BlurView } from 'expo-blur';
import { registerFrostedBlurView } from '@thewhileloop/whileui';

registerFrostedBlurView(BlurView);

Optional generic tokens: surface-translucent, surface-translucent-border — use them if you need a distinct token from surface-elevated for overlay panels.

Built-in overlays and cards support opt-in frosted mode via:

  • frosted?: boolean
  • blurIntensity?: number
  • blurTintToken?: 'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover'

Default blur presets map to CSS visual tokens:

  • subtle: --ui-blur-intensity-subtle (default 18)
  • medium: --ui-blur-intensity-medium (default 22)
  • strong: --ui-blur-intensity-strong (default 28)

Additional frosted tuning tokens:

  • --ui-blur-saturation-pct (web fallback saturation multiplier)
  • --ui-frosted-highlight-height (top highlight strip height in px, set 0 to disable hard top sheen)
  • --ui-frosted-backdrop-blur-intensity (default backdrop blur amount)
  • --ui-frosted-backdrop-blur-scale (ratio used when component blur is overridden)
  • --ui-frosted-android-experimental-blur (1 enables expo-blur Android experimental path)
  • --ui-drawer-frosted-inset (floating inset for frosted drawer shells)
  • --ui-drawer-frosted-radius (drawer corner radius in px)
  • --ui-drawer-content-top-padding (drawer content top spacing baseline)

Theme Colors for RN Primitives

Some React Native APIs require native color strings (hex/rgb/hsl/rgba/named). Use useThemeColors or useIconColors to read from your global.css theme. If your theme uses oklch(...), add --app-color-* fallbacks — useThemeColors will use them when --color-* is not RN-native.

import { useThemeColors, useIconColors } from '@thewhileloop/whileui';
import { Feather } from '@expo/vector-icons';

// Full palette (core semantic colors + status + overlay/effect tokens)
const colors = useThemeColors();

// Shorthand for icons: foreground, muted, primary, primaryForeground, accent, destructive
const iconColors = useIconColors();

<Feather name="heart" size={20} color={iconColors.muted} />
<Spinner color={colors.foreground} />  // Spinner defaults to this when color not passed
  • useThemeColors / useThemeTokens — Returns RN-safe color strings (hex/rgb/rgba) for semantic tokens (background, card, popover, primary, secondary, muted, accent, destructive, status colors) plus effect tokens (overlay, overlayStrong, surfaceElevated, surfaceTranslucent, surfaceTranslucentBorder, etc.). Falls back to --app-color-* when --color-* is missing/non-RN-native.
  • useIconColors — Subset for icons. Maps mutedmutedForeground (readable on backgrounds).

Input, Textarea, NumericInput, SmartInput, Spinner, and LoadingScreen default to theme colors when you omit placeholderTextColor or spinnerColor.

Interaction + Motion Tokens

WhileUI components also read optional --ui-* tokens for deeper control of press feedback, disabled states, and motion timing:

@theme {
  --ui-press-opacity: 0.72;
  --ui-press-opacity-strong: 0.9;
  --ui-disabled-opacity: 0.5;
  --ui-disabled-opacity-soft: 0.6;
  --ui-disabled-opacity-subtle: 0.4;
  --ui-inactive-opacity: 0.5;
  --ui-motion-fast: 160;
  --ui-motion-normal: 220;
  --ui-motion-slow: 300;
  --ui-drawer-open-duration: 300;
  --ui-drawer-close-duration: 220;
  --ui-blur-intensity-subtle: 18;
  --ui-blur-intensity-medium: 22;
  --ui-blur-intensity-strong: 28;
  --ui-blur-saturation-pct: 185;
  --ui-frosted-highlight-height: 0;
  --ui-frosted-backdrop-blur-intensity: 14;
  --ui-frosted-backdrop-blur-scale: 0.55;
  --ui-frosted-android-experimental-blur: 1;
  --ui-drawer-frosted-inset: 0;
  --ui-drawer-frosted-radius: 28;
  --ui-drawer-content-top-padding: 0;
}

For custom components, use:

import { useInteractionTokens, withInteractivePressableStyle } from '@thewhileloop/whileui';

Optional RN fallback tokens (hex/rgb/hsl/named):

@layer theme {
  :root {
    @variant light {
      --app-color-primary: #000000;
      --app-color-primary-foreground: #ffffff;
      --app-color-foreground: #000000;
      --app-color-background: #ffffff;
      --app-color-card: #ffffff;
      --app-color-card-foreground: #000000;
      --app-color-popover: #ffffff;
      --app-color-popover-foreground: #000000;
      --app-color-secondary: #f5f5f5;
      --app-color-secondary-foreground: #171717;
      --app-color-muted: #f5f5f5;
      --app-color-muted-foreground: #737373;
      --app-color-border: #e5e5e5;
      --app-color-input: #e5e5e5;
      --app-color-ring: #94a3b8;
      --app-color-accent: #22c55e;
      --app-color-accent-foreground: #0a0a0a;
      --app-color-destructive: #dc2626;
      --app-color-destructive-foreground: #ffffff;
      --app-color-success: #16a34a;
      --app-color-success-foreground: #ffffff;
      --app-color-warning: #f59e0b;
      --app-color-warning-foreground: #111827;
      --app-color-info: #3b82f6;
      --app-color-info-foreground: #ffffff;
      --app-color-overlay: rgba(0, 0, 0, 0.4);
      --app-color-overlay-strong: rgba(0, 0, 0, 0.55);
      --app-color-surface-elevated: #ffffff;
      --app-color-surface-translucent: rgba(255, 255, 255, 0.5);
      --app-color-surface-translucent-border: rgba(255, 255, 255, 0.18);
      --app-color-surface-border: rgba(255, 255, 255, 0.3);
      --app-color-surface-highlight: rgba(255, 255, 255, 0.3);
      --app-color-state-hover: rgba(0, 0, 0, 0.05);
      --app-color-state-pressed: rgba(0, 0, 0, 0.12);
      --app-color-state-disabled: rgba(0, 0, 0, 0.4);
    }
    @variant dark {
      --app-color-primary: #ffffff;
      --app-color-primary-foreground: #000000;
      --app-color-foreground: #ffffff;
      --app-color-background: #000000;
      --app-color-card: #0f0f10;
      --app-color-card-foreground: #ffffff;
      --app-color-popover: #121214;
      --app-color-popover-foreground: #ffffff;
      --app-color-secondary: #2e2e2e;
      --app-color-secondary-foreground: #ffffff;
      --app-color-muted: #2e2e2e;
      --app-color-muted-foreground: #999999;
      --app-color-border: #3d3d3d;
      --app-color-input: #3d3d3d;
      --app-color-ring: #7c889a;
      --app-color-accent: #22c55e;
      --app-color-accent-foreground: #0a0a0a;
      --app-color-destructive: #dc2626;
      --app-color-destructive-foreground: #ffffff;
      --app-color-success: #22c55e;
      --app-color-success-foreground: #0a0a0a;
      --app-color-warning: #fbbf24;
      --app-color-warning-foreground: #111827;
      --app-color-info: #60a5fa;
      --app-color-info-foreground: #111827;
      --app-color-overlay: rgba(0, 0, 0, 0.5);
      --app-color-overlay-strong: rgba(0, 0, 0, 0.7);
      --app-color-surface-elevated: #111315;
      --app-color-surface-translucent: rgba(17, 19, 21, 0.45);
      --app-color-surface-translucent-border: rgba(255, 255, 255, 0.18);
      --app-color-surface-border: rgba(255, 255, 255, 0.18);
      --app-color-surface-highlight: rgba(255, 255, 255, 0.18);
      --app-color-state-hover: rgba(255, 255, 255, 0.08);
      --app-color-state-pressed: rgba(255, 255, 255, 0.15);
      --app-color-state-disabled: rgba(255, 255, 255, 0.4);
    }
  }
}

Or use the first-party ThemeBridge helper with optional persistence:

import { useThemeBridge, type ThemeBridgeAdapter } from '@thewhileloop/whileui';

const adapter: ThemeBridgeAdapter = {
  loadThemeMode: async () => 'system',
  saveThemeMode: async (mode) => {
    await storage.setItem('theme-mode', mode);
  },
};

const { mode, resolvedTheme, setMode, cycleMode } = useThemeBridge({ adapter });

Tech Stack

API Reference

Button

import { Button, ButtonText, ButtonIcon } from '@thewhileloop/whileui';

<Button variant="default" size="default" disabled={false} onPress={() => {}}>
  <ButtonIcon>
    <Icon />
  </ButtonIcon>
  <ButtonText>Click me</ButtonText>
  <ButtonIcon position="right">
    <Icon />
  </ButtonIcon>
</Button>;
PropTypeDefaultDescription
variant'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link''default'Button style variant
size'default' | 'sm' | 'lg' | 'icon''default'Button size
disabledbooleanfalseDisable the button
classNamestringAdditional Tailwind classes

Input

import { Input } from '@thewhileloop/whileui';

<Input placeholder="Email" variant="default" value={value} onChangeText={setValue} />;
PropTypeDefaultDescription
variant'default' | 'error''default'Input style variant
placeholderstringPlaceholder text
placeholderTextColorstringHex for placeholder; defaults to theme mutedForeground
editablebooleantrueWhether input is editable

NumericInput

import { NumericInput } from '@thewhileloop/whileui';

<NumericInput
  value={amount}
  onValueChange={setAmount}
  prefix={<Text className="text-muted-foreground">$</Text>}
  suffix={<Text className="text-muted-foreground">USD</Text>}
  min={0}
  step={0.5}
  showSteppers
  size="compact"
/>;
PropTypeDefaultDescription
variant'default' | 'error''default'Input style
size'default' | 'compact''default'Density size
valuenumber | nullControlled numeric value
onValueChange(value: number | null) => voidNumeric value change callback
placeholderTextColorstringHex for placeholder
prefix / suffixReactNodeLeft/right slots
showSteppersbooleanfalseShow decrement/increment controls

OTPInput

import { OTPInput } from '@thewhileloop/whileui';

<OTPInput
  value={otp}
  onValueChange={setOtp}
  onComplete={(code) => verify(code)}
  length={6}
  variant="default"
/>;
PropTypeDefaultDescription
lengthnumber6Number of digit cells
valuestringControlled value
onValueChange(value: string) => voidCalled on each digit change
onComplete(code: string) => voidCalled when all digits are filled
variant'default' | 'error''default'Error variant triggers shake
size'default' | 'compact''default'Cell size
securebooleanfalseMask digits with dots
autoFocusbooleanfalseFocus first cell on mount

FormField

import {
  FormField,
  FormLabel,
  FormControl,
  FormHint,
  FormMessage,
  Input,
} from '@thewhileloop/whileui';

<FormField required invalid={Boolean(error)}>
  <FormLabel>Email</FormLabel>
  <FormControl>
    <Input placeholder="you@example.com" />
  </FormControl>
  {error ? <FormMessage>{error}</FormMessage> : <FormHint>We'll never share your email.</FormHint>}
</FormField>;

LabeledField

import { LabeledField, LabeledFieldControl, Input } from '@thewhileloop/whileui';

<LabeledField
  label="Username"
  hint="3-20 characters"
  leftSlot={<Icon />}
  rightSlot={
    <Button size="sm">
      <ButtonText>Check</ButtonText>
    </Button>
  }
>
  <LabeledFieldControl>
    <Input className="border-0 bg-transparent px-0" />
  </LabeledFieldControl>
</LabeledField>;

SegmentedControl

import {
  SegmentedControl,
  SegmentedControlItem,
  SegmentedControlItemText,
} from '@thewhileloop/whileui';

<SegmentedControl value={unit} onValueChange={setUnit} variant="pill" wrap>
  <SegmentedControlItem value="metric">
    <SegmentedControlItemText>Metric</SegmentedControlItemText>
  </SegmentedControlItem>
  <SegmentedControlItem value="imperial">
    <SegmentedControlItemText>Imperial</SegmentedControlItemText>
  </SegmentedControlItem>
</SegmentedControl>;
PropTypeDefaultDescription
variant'default' | 'pill''default'Pill = rounded-full items

DataRow

import {
  DataRow,
  DataRowLeft,
  DataRowCenter,
  DataRowRight,
  DataRowLabel,
  DataRowDescription,
  DataRowValue,
  Avatar,
  AvatarFallback,
} from '@thewhileloop/whileui';

<DataRow>
  <DataRowLeft>
    <Avatar size="sm">
      <AvatarFallback>JD</AvatarFallback>
    </Avatar>
  </DataRowLeft>
  <DataRowCenter>
    <DataRowLabel>Jane Doe</DataRowLabel>
    <DataRowDescription>Product Designer</DataRowDescription>
  </DataRowCenter>
  <DataRowRight>
    <DataRowValue>Owner</DataRowValue>
  </DataRowRight>
</DataRow>;

Card

import {
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardContent,
  CardFooter,
} from '@thewhileloop/whileui';

<Card padding="default">
  <CardHeader>
    <CardTitle>Title</CardTitle>
    <CardDescription>Description</CardDescription>
  </CardHeader>
  <CardContent>{/* Content */}</CardContent>
  <CardFooter>{/* Footer */}</CardFooter>
</Card>;

<Card unstyled padding="none" className="rounded-xl border border-border bg-card">
  {/* advanced custom layouts */}
</Card>;
PropTypeDefaultDescription
padding'none' | 'sm' | 'default' | 'lg''default'Card interior padding
unstyledbooleanfalseRemove built-in card surface styles
frostedbooleanfalseEnables frosted tint + blur layer
blurIntensitynumbertoken presetOverride blur amount directly
blurTintToken'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover''card'Tint token used for frosted mode

Badge

import { Badge, BadgeText } from '@thewhileloop/whileui';

<Badge variant="default">
  <BadgeText>New</BadgeText>
</Badge>;
PropTypeDefault
variant'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'info''default'

Alert

import { Alert, AlertTitle, AlertDescription } from '@thewhileloop/whileui';

<Alert variant="default">
  <AlertTitle>Heads up!</AlertTitle>
  <AlertDescription>Something happened.</AlertDescription>
</Alert>;
PropTypeDefault
variant'default' | 'destructive' | 'success' | 'warning''default'

Dialog

import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose,
} from '@thewhileloop/whileui';

<Dialog>
  <DialogTrigger asChild>
    <Button>
      <ButtonText>Open</ButtonText>
    </Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Title</DialogTitle>
      <DialogDescription>Description</DialogDescription>
    </DialogHeader>
    {/* Content */}
    <DialogFooter>
      <DialogClose asChild>
        <Button>
          <ButtonText>Close</ButtonText>
        </Button>
      </DialogClose>
    </DialogFooter>
  </DialogContent>
</Dialog>;

DialogContent supports frosted props:

PropTypeDefaultDescription
frostedbooleanfalseEnables frosted tint + blur
blurIntensitynumbermedium presetOverride blur amount
blurTintToken'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover''surfaceTranslucent'Tint token used for frosted mode

AlertDialog

import {
  AlertDialog,
  AlertDialogTrigger,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogAction,
  AlertDialogCancel,
} from '@thewhileloop/whileui';

<AlertDialog>
  <AlertDialogTrigger asChild>
    <Button variant="destructive">
      <ButtonText>Delete</ButtonText>
    </Button>
  </AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Are you sure?</AlertDialogTitle>
      <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction>Delete</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>;

AlertDialogContent supports the same frosted props as DialogContent.

PropTypeDefaultDescription
presentation'modal' | 'native' | 'overlay''modal'modal — themed card in an RN Modal (iOS overFullScreen). nativeAlert.alert on iOS/Android (system UI); use when a second Modal won’t stack; ignored on web. overlay — same themed card as modal, but absolutely positioned inside the parent view (no second Modal). Nest <AlertDialog> under your fullscreen sheet’s root so the confirm matches app styling. Web: 'native' is ignored; overlay behaves like modal.
frostedbooleanfalseSame as DialogContent
blurIntensitynumberSame as DialogContent
blurTintToken'surfaceElevated' | …Same as DialogContent

React Native: stacked modals

If AlertDialog is a sibling of another fullscreen Modal, iOS often fails to show a second Modal on top. Prefer presentation="overlay" and render <AlertDialog> inside that Modal (themed UI, one native modal host). Alternatively use presentation="native" for a system Alert.alert when themed UI is not required.

Manual QA (iOS + Android): Fullscreen sheet + presentation="overlay" nested in the sheet: themed confirm appears, Cancel / Delete behave correctly. Optionally repeat with presentation="native" for system alert stacking.

Checkbox

import { Checkbox } from '@thewhileloop/whileui';

<Checkbox checked={checked} onCheckedChange={setChecked} />;
PropTypeDefault
checkedbooleanfalse
onCheckedChange(checked: boolean) => void
disabledbooleanfalse

Switch

import { Switch } from '@thewhileloop/whileui';

<Switch checked={checked} onCheckedChange={setChecked} />;
PropTypeDefault
checkedbooleanfalse
onCheckedChange(checked: boolean) => void

RadioGroup

import { RadioGroup, RadioGroupItem } from '@thewhileloop/whileui';

<RadioGroup value={value} onValueChange={setValue}>
  <RadioGroupItem value="option1" />
  <RadioGroupItem value="option2" />
</RadioGroup>;

Select

import {
  Select,
  SelectTrigger,
  SelectValue,
  SelectContent,
  SelectItem,
} from '@thewhileloop/whileui';

<Select value={value} onValueChange={setValue}>
  <SelectTrigger>
    <SelectValue placeholder="Select..." />
  </SelectTrigger>
  <SelectContent frosted blurIntensity={24} blurTintToken="surfaceTranslucent">
    <SelectItem label="Option 1" value="1" />
    <SelectItem label="Option 2" value="2" />
  </SelectContent>
</Select>;

SelectContent supports optional frosted props:

PropTypeDefault
frostedbooleanfalse
blurIntensitynumbersubtle preset
blurTintToken'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover''popover'

Tabs

import { Tabs, TabsList, TabsTrigger, TabsContent } from '@thewhileloop/whileui';

<Tabs defaultValue="tab1">
  <TabsList>
    <TabsTrigger value="tab1">
      <Text>Tab 1</Text>
    </TabsTrigger>
    <TabsTrigger value="tab2">
      <Text>Tab 2</Text>
    </TabsTrigger>
  </TabsList>
  <TabsContent value="tab1">{/* Content 1 */}</TabsContent>
  <TabsContent value="tab2">{/* Content 2 */}</TabsContent>
</Tabs>;

Accordion

import {
  Accordion,
  AccordionItem,
  AccordionTrigger,
  AccordionContent,
} from '@thewhileloop/whileui';

<Accordion type="single" collapsible>
  <AccordionItem value="item1">
    <AccordionTrigger>
      <Text>Section 1</Text>
    </AccordionTrigger>
    <AccordionContent>
      <Text>Content 1</Text>
    </AccordionContent>
  </AccordionItem>
</Accordion>;
PropTypeDefault
type'single' | 'multiple''single'
collapsiblebooleanfalse

Avatar

import { Avatar, AvatarImage, AvatarFallback } from '@thewhileloop/whileui';

<Avatar size="default">
  <AvatarImage src="https://..." />
  <AvatarFallback>JD</AvatarFallback>
</Avatar>;
PropTypeDefault
size'sm' | 'default' | 'lg''default'

Progress

import { Progress } from '@thewhileloop/whileui';

<Progress value={50} size="default" />;
PropTypeDefault
valuenumber0
size'sm' | 'default' | 'lg''default'

Toast

import { ToastProvider, ToastContainer, useToast } from '@thewhileloop/whileui';

// Wrap app
<ToastProvider>
  <App />
  <ToastContainer position="top" />
</ToastProvider>;

// Use in component
const { toast } = useToast();
toast({ title: 'Success', description: 'Saved!', variant: 'success' });
Toast OptionsType
titlestring
descriptionstring
variant'default' | 'success' | 'destructive'
durationnumber (ms)

Popover

import { Popover, PopoverTrigger, PopoverContent } from '@thewhileloop/whileui';

<Popover>
  <PopoverTrigger asChild>
    <Button>
      <ButtonText>Open</ButtonText>
    </Button>
  </PopoverTrigger>
  <PopoverContent>
    <Text>Popover content</Text>
  </PopoverContent>
</Popover>;

PopoverContent supports frosted props:

PropTypeDefault
frostedbooleanfalse
blurIntensitynumbersubtle preset
blurTintToken'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover''popover'

DropdownMenu

import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
} from '@thewhileloop/whileui';

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button>
      <ButtonText>Menu</ButtonText>
    </Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuLabel>Actions</DropdownMenuLabel>
    <DropdownMenuSeparator />
    <DropdownMenuItem>
      <Text>Edit</Text>
    </DropdownMenuItem>
    <DropdownMenuItem>
      <Text>Delete</Text>
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>;

DropdownMenuContent, ContextMenuContent, and MenubarContent support the same frosted props as PopoverContent (default blur preset: medium).

HoverCard

HoverCardContent supports the same frosted props as PopoverContent (default blur preset: subtle).

ContextMenu

ContextMenuContent supports the same frosted props as PopoverContent (default blur preset: medium).

Menubar

MenubarContent supports the same frosted props as PopoverContent (default blur preset: medium).

Blocks API

SignInForm

Copy from apps/showcase/templates/auth/sign-in-form.tsx, then:

import { SignInForm } from './templates/auth'; // or your path

<SignInForm
  onSubmit={({ email, password }) => signIn(email, password)}
  onForgotPassword={() => navigate('ForgotPassword')}
  onSignUp={() => navigate('SignUp')}
  onGooglePress={() => signInWithGoogle()}
  onApplePress={() => signInWithApple()}
/>;
PropTypeDescription
onSubmit(data: { email, password }) => voidCalled on sign-in submit
onForgotPassword() => voidCalled when "Forgot?" tapped
onSignUp() => voidCalled when "Sign Up" tapped
onGooglePress() => voidCalled when Google tapped
onApplePress() => voidCalled when Apple tapped

SignUpForm

Copy from apps/showcase/templates/auth/sign-up-form.tsx, then:

import { SignUpForm } from './templates/auth'; // or your path

<SignUpForm
  onSubmit={({ firstName, lastName, email, password }) =>
    signUp(firstName, lastName, email, password)
  }
  onSignIn={() => navigate('SignIn')}
  onGooglePress={() => signInWithGoogle()}
  onApplePress={() => signInWithApple()}
/>;
PropTypeDescription
onSubmit(data: { firstName, lastName, email, password }) => voidCalled on registration
onSignIn() => voidCalled when "Sign In" tapped
onGooglePress() => voidCalled when Google tapped
onApplePress() => voidCalled when Apple tapped

BottomNav

import { BottomNav } from '@thewhileloop/whileui';

<BottomNav
  items={[
    { key: 'home', label: 'Home', icon: <Icon /> },
    { key: 'profile', label: 'Profile', icon: <Icon />, badge: 3 },
  ]}
  activeKey="home"
  onSelect={(key) => {}}
/>;

AppShell

Layout shell for full-screen pages. Keep shell chrome mounted and swap only content via loading + skeleton.

import { AppShell, PageSkeleton } from '@thewhileloop/whileui';

<AppShell
  header={<Header title="Settings" />}
  bottomNav={<BottomNav items={[...]} activeKey="settings" onSelect={setTab} />}
  loading={loading}
  skeleton={<PageSkeleton variant="settings" headerPlaceholder />}
>
  <ScrollView className="flex-1 p-4">{/* Content */}</ScrollView>
</AppShell>;
PropTypeDefaultDescription
headerReactNodeHeader slot
footerReactNodeFooter slot
bottomNavReactNodeBottom navigation slot
safeAreabooleantrueWrap in SafeAreaView
loadingbooleanfalseShow skeleton in content area, keep shell mounted
skeletonReactNodeContent placeholder rendered when loading=true

ActionBar

import { ActionBar, Button, ButtonText } from '@thewhileloop/whileui';

<ActionBar>
  <Button variant="outline" className="flex-1">
    <ButtonText>Cancel</ButtonText>
  </Button>
  <Button className="flex-1">
    <ButtonText>Save</ButtonText>
  </Button>
</ActionBar>;

SmartInput

Keyboard-aware compose input. variant: "bar" (sticky bottom) or "card" (floating). Slots: leftSlot, centerSlot (intent selector), rightSlot. submitBehavior: "newline" (default) or "submit"/"blurAndSubmit". Forwards ref to TextInput.

import { SmartInput, Button, ButtonText } from '@thewhileloop/whileui';

<SmartInput
  value={message}
  onChangeText={setMessage}
  placeholder="Type a message..."
  leftSlot={
    <Button variant="ghost" size="icon">
      <Icon name="add" />
    </Button>
  }
  rightSlot={
    <Button size="icon" onPress={handleSend}>
      <ButtonText>Send</ButtonText>
    </Button>
  }
/>;

Chat

AI-style chat: message list, suggestion chips when empty, SmartInput with attach/send slots. Uses semantic tokens; theme via global.css. Copy-paste block — edit directly. renderMessage for markdown/code, loadingIndicator for typing state.

import { Chat, type ChatMessage } from '@thewhileloop/whileui';

const [messages, setMessages] = useState<ChatMessage[]>([...]);
const [value, setValue] = useState('');

<Chat
  messages={messages}
  value={value}
  onChangeText={setValue}
  onSend={() => { /* append user msg, clear input */ }}
  placeholder="Message..."
  suggestions={['Summarize this', 'Explain simply', 'Translate']}
  onSuggestionPress={(text) => setValue(text)}
  emptyTitle="How can I help?"
  emptyDescription="Ask anything."
  leftSlot={<Button variant="ghost" size="icon"><Icon name="paperclip" /></Button>}
  rightSlot={<Button size="icon" onPress={handleSend}><Icon name="send" /></Button>}
/>;
PropTypeDescription
messagesChatMessage[]{ id, role, content, secondary?, contentSize? }
valuestringInput value
onChangeText(text) => voidInput change
onSend() => voidSend handler
suggestionsstring[]Chips when empty
leftSlot / rightSlotReactNodeAttach, send, etc.
exampleMessageChatMessageShown in empty state
renderMessage(msg) => ReactNodeCustom message (markdown, code, images)
loadingIndicatorReactNodeShown when loading (typing dots)
inputSafeAreabooleanSmartInput safe-area (default true)
keyboardVerticalOffsetnumberFor header offset when keyboard opens

DatePickerModal / DatePickerInline / DateRangePickerModal

Date selection blocks using react-native-calendars. Theme-aware via useCalendarTheme (Uniwind light/dark). Optional theme prop for custom RN color strings (hex/rgb/hsl/named). CalendarTheme maps to react-native-calendars Theme and supports arrowColor, disabledArrowColor, and optional font keys.

DatePickerModal — Compact trigger opens bottom sheet with calendar. Use DatePickerTrigger as the trigger content.

DatePickerInline — Calendar embedded inline for forms or dashboards.

DateRangePickerModal — Range selection with period marking. Use DateRangePickerTrigger as the trigger content.

import {
  DatePickerModal,
  DatePickerTrigger,
  DatePickerInline,
  DateRangePickerModal,
  DateRangePickerTrigger,
  type DateRange,
} from '@thewhileloop/whileui';

// Single date (modal)
const [date, setDate] = useState<string | null>(null);
const [open, setOpen] = useState(false);

<DatePickerModal
  value={date}
  onValueChange={setDate}
  open={open}
  onOpenChange={setOpen}
  trigger={<DatePickerTrigger value={date} placeholder="Pick a date" />}
  title="Select date"
/>;

// Inline calendar
<DatePickerInline value={date} onValueChange={setDate} />;

// Date range (modal)
const [range, setRange] = useState<DateRange | null>(null);

<DateRangePickerModal
  value={range}
  onValueChange={setRange}
  open={rangeOpen}
  onOpenChange={setRangeOpen}
  trigger={<DateRangePickerTrigger value={range} placeholder="Pick range" />}
/>;
PropTypeDescription
valuestring | null / DateRange | nullSelected date(s) YYYY-MM-DD
onValueChange(date) => voidChange handler
open / onOpenChangeModal state (modal variants)
triggerReactNodeCustom trigger (modal variants)
minDate / maxDatestringYYYY-MM-DD bounds
themeCalendarThemeOverride calendar colors
frostedbooleanEnable frosted surface for modal panel
blurIntensitynumberOverride blur amount
blurTintToken'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover'Tint token for frosted panel

ConfirmActionSheet

import { ConfirmActionSheet } from '@thewhileloop/whileui';

<ConfirmActionSheet
  open={open}
  onOpenChange={setOpen}
  title="Delete project?"
  description="This action cannot be undone."
  confirmLabel="Delete"
  destructive
  onConfirm={() => deleteProject()}
/>;
PropTypeDefault
frostedbooleanfalse
blurIntensitynumbermedium preset
blurTintToken'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover''surfaceTranslucent'

Sheet

Bottom sheet modal with slide animation. Slots: SheetHeader, SheetContent, SheetFooter, SheetClose.

import {
  Sheet,
  SheetHeader,
  SheetContent,
  SheetFooter,
  SheetClose,
  Button,
  ButtonText,
} from '@thewhileloop/whileui';

<Sheet open={open} onOpenChange={setOpen} maxHeight="half">
  <SheetHeader title="Settings" description="Adjust preferences" />
  <SheetContent>{/* Scrollable body */}</SheetContent>
  <SheetFooter>
    <SheetClose asChild>
      <Button>
        <ButtonText>Save</ButtonText>
      </Button>
    </SheetClose>
  </SheetFooter>
</Sheet>;
PropTypeDefault
openboolean
onOpenChange(open: boolean) => void
maxHeight'half' | 'full' | number'full'
maxWidthnumber360
frostedbooleanfalse
blurIntensitynumbermedium preset
blurTintToken'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover''surfaceTranslucent'

NavigationSidebar

import { NavigationSidebar } from '@thewhileloop/whileui';

<NavigationSidebar
  sections={[
    {
      title: 'Workspace',
      items: [
        { key: 'overview', label: 'Overview', icon: <Icon /> },
        { key: 'billing', label: 'Billing', icon: <Icon />, badge: 2 },
      ],
    },
  ]}
  activeKey="overview"
  onSelect={(key) => {}}
  header={<Text>Acme Inc.</Text>}
  footer={<Text className="text-xs text-muted-foreground">v1.0.0</Text>}
/>;

Header

import { Header, HeaderBackButton } from '@thewhileloop/whileui';

<Header
  title="Settings"
  subtitle="Manage preferences"
  leftAction={<HeaderBackButton onPress={() => {}} />}
  rightActions={[{ key: 'search', icon: <Icon />, onPress: () => {} }]}
/>;

SplashScreen

import { SplashScreen } from '@thewhileloop/whileui';

<SplashScreen
  logo={<MyLogo />}
  appName="MyApp"
  tagline="Your tagline"
  variant="scale"
  duration={1500}
  showLoading
  onAnimationComplete={() => {}}
/>;
PropTypeDefault
variant'fade' | 'scale' | 'slide''scale'
durationnumber800
showLoadingbooleanfalse

ContentSkeleton

Page/content placeholder with layout presets. Use while loading data instead of a spinner when you want to preview the layout.

import { ContentSkeleton } from '@thewhileloop/whileui';

<ContentSkeleton variant="list" rows={4} />
<ContentSkeleton variant="card" />
<ContentSkeleton variant="generic" />
PropTypeDefaultDescription
variant'list' | 'card' | 'generic''list'Layout preset
rowsnumber4Number of list rows (list variant only)

PageSkeleton

Variant-based page layouts for loading states. Replaces app-specific skeletons with presets for dashboard, list, settings, card, and generic pages.

import { PageSkeleton } from '@thewhileloop/whileui';

<PageSkeleton variant="dashboard" />
<PageSkeleton variant="list" count={5} />
<PageSkeleton variant="settings" count={6} />
<PageSkeleton variant="card" />
<PageSkeleton variant="generic" />
<PageSkeleton variant="dashboard" headerPlaceholder />
<PageSkeleton variant="list" header={<Header title="Loading..." />} />
<PageSkeleton variant="list" padding="none" className="flex-1" />
PropTypeDefaultDescription
variant'dashboard' | 'list' | 'settings' | 'card' | 'generic'requiredLayout preset
countnumber3 (list), 4 (settings)Rows/items for list or settings variant
padding'none' | 'sm' | 'default' | 'lg''default'Container padding
headerReactNodeOptional real header slot above content
headerPlaceholderboolean | 'compact' | 'default'falseSkeleton header when real header is not ready
classNamestringOuter container classes

ScreenSkeleton

Convenience block for loading screens that need both header and content continuity.

import { ScreenSkeleton } from '@thewhileloop/whileui';

<ScreenSkeleton variant="dashboard" headerPlaceholder />
<ScreenSkeleton variant="list" count={5} header={<Header title="Projects" />} />
PropTypeDefaultDescription
variantPageSkeletonVariant'generic'Skeleton content preset
countnumberRows/items for list/settings variants
paddingPageSkeletonPadding'default'Inner content padding
headerReactNodeReal header slot
headerPlaceholderboolean | 'compact' | 'default''default'Placeholder header

ErrorBoundary

React ErrorBoundary that catches render errors and renders ErrorState by default.

import { ErrorBoundary } from '@thewhileloop/whileui';

<ErrorBoundary onError={(err) => console.error(err)}>
  <App />
</ErrorBoundary>;
PropTypeDescription
fallbackReactNode | (error, reset) => ReactNodeCustom fallback; defaults to ErrorState
onError(error, errorInfo) => voidCalled when error is caught

PullToRefreshScrollView

Themed ScrollView with RefreshControl. Uses useThemeTokens for spinner color.

import { PullToRefreshScrollView } from '@thewhileloop/whileui';

<PullToRefreshScrollView refreshing={refreshing} onRefresh={fetchData} refreshColor="#22c55e">
  {content}
</PullToRefreshScrollView>;
PropTypeDescription
refreshingbooleanWhether refresh is in progress
onRefresh() => voidCalled when user pulls to refresh
refreshColorstringOptional hex override; defaults to primary

EmptyState

import { EmptyState } from '@thewhileloop/whileui';

<EmptyState
  icon={<Icon />}
  title="No items"
  description="Add your first item."
  action={{ label: 'Add Item', onPress: () => {} }}
/>;

ProfileHeader

Copy from apps/showcase/templates/profile/profile-header.tsx, then:

import { ProfileHeader } from './templates/profile'; // or your path

<ProfileHeader
  name="John Doe"
  username="johndoe"
  bio="Designer & Developer"
  avatarFallback="JD"
  avatarUrl="https://..."
  verified
  stats={[
    { label: 'Followers', value: '1.2K' },
    { label: 'Following', value: 234 },
  ]}
  action={{ label: 'Edit Profile', onPress: () => {} }}
/>;

SettingsSection / SettingsItem

Copy from apps/showcase/templates/profile/, then:

import { SettingsSection, SettingsItem } from './templates/profile'; // or your path

<SettingsSection title="Preferences">
  <SettingsItem
    icon={<Icon />}
    label="Notifications"
    type="toggle"
    toggleValue={enabled}
    onToggle={setEnabled}
  />
  <SettingsItem icon={<Icon />} label="Privacy" value="Public" onPress={() => {}} />
  <SettingsItem icon={<Icon />} label="Sign Out" type="action" destructive />
</SettingsSection>;

ProductCard

import { ProductCard } from '@thewhileloop/whileui';

<ProductCard
  title="Product Name"
  price="$99"
  originalPrice="$129"
  badge="-23%"
  rating={4.5}
  reviewCount={128}
  variant="vertical"
  onPress={() => {}}
/>;
PropTypeDefault
variant'vertical' | 'horizontal''vertical'
loadingbooleanfalse

PricingCard

import { PricingCard } from '@thewhileloop/whileui';

<PricingCard
  name="Pro"
  description="For teams"
  price="$29"
  period="/month"
  badge="Popular"
  highlighted
  features={[
    { label: 'Unlimited users', included: true },
    { label: 'Priority support', included: true },
    { label: 'Custom domain', included: false },
  ]}
  onPress={() => {}}
/>;

SubscriptionCard

import { SubscriptionCard } from '@thewhileloop/whileui';

<SubscriptionCard
  planName="Pro"
  price="$29"
  period="/month"
  expiresAt="April 18, 2026"
  isActive
  onManage={() => {}}
  onUpgrade={() => {}}
/>;
PropTypeDescription
planNamestringCurrent plan label
pricestringPlan price
periodstringBilling period label
expiresAtstringRenewal/expiry date text
isActivebooleanActive/inactive badge state
onManage() => voidManage action
onUpgrade() => voidUpgrade action
loadingbooleanShow skeleton placeholder

FeatureGate

import { FeatureGate } from '@thewhileloop/whileui';

<FeatureGate
  title="Advanced export is locked"
  description="Upgrade to unlock 4K export."
  buttonLabel="Upgrade"
  onUpgrade={() => {}}
/>;

Use children to show dimmed preview content with an overlay CTA.

UsageBar

import { UsageBar } from '@thewhileloop/whileui';

<UsageBar label="AI generations" used={8} limit={20} />;
PropTypeDefaultDescription
labelstringUsage label
usednumberUsed amount
limitnumberQuota limit
variant'default' | 'warning' | 'exceeded'autoOptional explicit visual state

PlanToggle

import { PlanToggle } from '@thewhileloop/whileui';

<PlanToggle
  selected="monthly"
  monthlyLabel="Monthly"
  annualLabel="Annual"
  annualDiscount="Save 20%"
  onChange={(next) => {}}
/>;

UpgradeBanner

import { UpgradeBanner } from '@thewhileloop/whileui';

<UpgradeBanner
  message="Unlock unlimited exports with Pro."
  actionLabel="See plans"
  onAction={() => {}}
  onDismiss={() => {}}
/>;

DrawerMenu

import { DrawerMenu } from '@thewhileloop/whileui';

<DrawerMenu
  visible={open}
  onClose={() => setOpen(false)}
  sections={[
    {
      title: 'Menu',
      items: [
        { key: 'home', label: 'Home', icon: <Icon /> },
        { key: 'settings', label: 'Settings', icon: <Icon /> },
      ],
    },
  ]}
  activeKey="home"
  onSelect={(key) => {}}
  header={<View>...</View>}
  footer={<Text>v1.0</Text>}
/>;
PropTypeDefault
frostedbooleanfalse
blurIntensitynumbermedium preset
blurTintToken'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover''surfaceTranslucent'

Roadmap

Tracked work items for future releases.

New Components

  • Chip / Tag — selectable, dismissible, multi-select with tv() variants, loading skeleton
  • Rating / Stars — interactive + read-only, half-star precision, swipe gesture, pairs with ProductCard
  • Slider — single + range (two thumbs), step marks, labels, Reanimated gesture, haptic on snap
  • Carousel — Reanimated-powered, auto-play, pagination dots, snap-to-item
  • FAB (Floating Action Button) — expandable action menu, Reanimated spring, auto-hide on scroll
  • Combobox — searchable select with keyboard navigation, empty state, async loading
  • Data Table — sortable headers, skeleton loading rows, row actions, responsive stacking
  • Banner — dismissible info/warning/success/destructive bar with icon + action, auto-dismiss timer
  • Pagination — compact (dots) + expanded (numbers) variants, edge-aware ellipsis, 44px touch targets
  • Inline Calendar — standalone calendar view reusing DatePicker logic, range selection

Component-Level Loading (loading prop)

  • ProductCard, ListItem, NotificationItem, MetricCard, SubscriptionCard, PricingCard
  • AppShell, Chat, FormModalScreen, CheckoutSummary (already had loading)
  • Add loading to remaining blocks: Header, BottomNav, DrawerMenu, TimelineFeed, SwipeableItem

Documentation Gaps

  • Add ## API sections for: Textarea, Toggle, ToggleGroup, Label, Separator, Skeleton, AspectRatio, Collapsible, Text, View, Pressable
  • Add ## API sections for blocks: FloatingBottomNav, TabBar, FormModalScreen, ErrorState, LoadingScreen, OnboardingScreen, ListItem, NotificationItem, MetricCard, SmartImage, TimelineFeed
  • Build apps/site/ (docs website) with registry.ts, demos.tsx, block-demos.tsx, props-data.ts

Infrastructure

  • Publish to npm (@thewhileloop/whileui)
  • CI: typecheck + format check on PR
  • Automated visual regression tests (screenshot comparison)

License

MIT — Source

Keywords

react-native

FAQs

Package last updated on 04 Apr 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts