npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

panelui-native

v0.40.0

Published

High-performance React Native UI components for Expo. Semantic design tokens, powered by Uniwind (Tailwind v4) and Reanimated.

Readme

PanelUI — React Native UI components for Expo, styled with Tailwind CSS

PanelUI (panelui-native) is an accessible, high-performance React Native component library for Expo apps. 79 typed components — buttons, bottom sheets, dialogs, selects, toasts, forms — styled with Tailwind CSS v4 and animated on the UI thread with Reanimated. Zero native code, so it runs in Expo Go.

npm version npm downloads bundle size MIT license platforms iOS and Android Expo SDK 57+

  • Tailwind CSS for React Native via Uniwind — no Babel transform, roughly 2.4–3× faster styling than NativeWind.
  • 🧵 60fps animations on the UI thread with Reanimated 4. Press feedback, switches, sheets, dialogs and tabs never touch the JS thread.
  • 🎨 Six built-in themes — Panel, Moon and Grass, each in light and dark. A theme sets radius as well as colour, so switching one restyles the shape of the UI too.
  • 🌗 Native dark mode. Theme switching is applied natively by Uniwind, without re-rendering your component tree.
  • Accessible by default — every interactive component wires up accessibilityRole, state and labels.
  • 📦 TypeScript, tree-shakeable, zero native modules — works in Expo Go, no prebuild needed.

Quick start

Needs an Expo SDK 57+ app and Node 20+. No Xcode, no Android Studio, no prebuild — PanelUI has no native modules, so it runs in Expo Go. No app yet? npx create-expo-app@latest my-app.

Five steps. The full walkthrough, with a fix for every error people hit, is at panelui.dev/docs/installation.

1. Install the packages

npx expo install panelui-native uniwind tailwindcss @react-native-masked-view/masked-view expo-linear-gradient react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets

Install all of them, including the ones you think you don't need. Metro resolves every import in the library when it builds your bundle, so leaving one out fails the first bundle with Unable to resolve module … — for a component you may never use. npx expo install (rather than npm install) picks the versions that match your SDK.

Optional, each behind a guarded import: expo-haptics (the haptics prop), expo-blur (blurred overlay backdrops), expo-clipboard (useCopyToClipboard), react-native-keyboard-controller (keyboard avoidance on Android), expo-file-system + react-native-view-shot (Signature export), @expo/ui (native platform controls).

2. Add metro.config.js

In the root of your project, next to package.json — not in app/, not in src/. A Metro config anywhere else is silently ignored and none of your classes will do anything. A fresh Expo app has no such file; npx expo customize metro.config.js writes one.

// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const { withUniwindConfig } = require('uniwind/metro');

const config = getDefaultConfig(__dirname);

module.exports = withUniwindConfig(config, {
  cssEntryFile: './src/global.css',
  dtsFile: './uniwind-types.d.ts',
  // Only needed to switch to the Moon or Grass themes at runtime.
  extraThemes: ['moon', 'moon-dark', 'grass', 'grass-dark'],
});

cssEntryFile and dtsFile are relative to this file. ./src/global.css is where create-expo-app puts the CSS — if yours is at the project root, change it to './global.css'.

Nothing validates that path. Point it at a file that does not exist and Metro still bundles, the app still launches, and not one class resolves — no flex-1, so views collapse and you get a blank screen with Uniwind - We couldn't find your variable --color-background.

3. Add the imports to global.css

Look for this file before you create one — an app made by create-expo-app already has src/global.css. Add these lines at the top of it and leave the rest below. A second CSS file at the project root gives you two entries, only one of which is compiled, and nothing gets styled.

/* src/global.css */
@import 'tailwindcss';
@import 'uniwind';
@import 'panelui-native/theme.css';

@source '../node_modules/panelui-native/src';

@source is relative to the CSS file, and has to land on node_modules/panelui-native/src — hence the ../ above, from src/. From the project root it is './node_modules/…', from src/styles/ it is '../../node_modules/…'. Get it wrong and your own classes work while PanelUI's components come out unstyled. Whichever file you used is what cssEntryFile must name.

4. Import the CSS and add the provider

At the top of your app's entry file. The default template uses Expo Router with routes under src/, so there is usually no App.tsx — the entry is src/app/_layout.tsx, and it already returns a navigation ThemeProvider to wrap rather than replace:

// src/app/_layout.tsx
import '../global.css';

import { DarkTheme, DefaultTheme, ThemeProvider } from 'expo-router';
import { useColorScheme } from 'react-native';
import { PanelUIProvider } from 'panelui-native';

import AppTabs from '@/components/app-tabs';

export default function RootLayout() {
  const colorScheme = useColorScheme();

  return (
    <PanelUIProvider>
      <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
        <AppTabs />
      </ThemeProvider>
    </PanelUIProvider>
  );
}

That navigation theme paints over PanelUI's background once you switch themes — see Using Expo Router? below for the version fed from live tokens.

PanelUIProvider sets up the gesture root, the themed page background, the portal host used by overlays, and the toast viewport. One at the root is enough — don't nest a second.

You do not need a babel.config.js; Expo's default preset already wires the worklets plugin Reanimated needs.

5. Restart

npx expo start --clear

Metro reads metro.config.js, global.css and extraThemes once, at startup. After changing any of them, stop the dev server and start it again — --clear on a running one is not enough.

import { Button, Card } from 'panelui-native';

<Card>
  <Card.Header>
    <Card.Title>It works</Card.Title>
    <Card.Description>PanelUI is installed and themed.</Card.Description>
  </Card.Header>
  <Card.Footer>
    <Button>Deploy</Button>
  </Card.Footer>
</Card>;

A themed background, a card with a border and radius, and a button that dips when pressed means you are done. Unstyled text on a white screen means the styles are not reaching the bundle — see Troubleshooting.

Components

| Component | What it does | | --- | --- | | Accordion | Collapsible sections with single or multiple selection | | Alert | Status message with a built-in icon | | AreaChart | Filled bands over time, stacked or overlaid | | Attachment | File row with upload states, built on Item | | Avatar | User image with an initials fallback and an optional badge overlay | | Badge | Compact status label, dot, or notification count | | BarChart | Categories compared by length, grouped or stacked | | BottomSheet | Draggable sheet anchored to the bottom of the screen | | Breadcrumb | The trail of links back up the hierarchy to the current page | | Button | Pressable action with variants, sizes, loading state and icon slots | | Calendar | A month of days, for picking one, several, or a range | | Card | Content surface with header, body and footer | | Carousel | A run of slides, one at a time, dragged with a finger | | Checkbox | Animated checkbox, as a row or a selectable card | | Chip | Interactive pill — a filter, a tag, or a removable token | | CodeBlock | Syntax-highlighted code with a header and a copy button | | Combobox | A text field that filters a list of options as you type | | DatePicker | A calendar behind a button | | Dialog | Modal dialog with a backdrop and footer actions | | Direction | Reading direction for everything below it | | Drawer | Panel that slides in from an edge of the screen | | EmptyState | Placeholder for a list or screen with no content | | Field | Layout and validation-state kit a form control composes into | | Flow | Pan-and-zoom canvas of draggable nodes joined by animated edges | | Form | Form state — values, validation and submission — with no form library underneath | | Frame | Widget shell — a card of rows sitting in a titled tray | | HeatmapChart | Contribution grid with a themed colour ramp and a readout | | Input | Text field with label, description and error message | | InputGroup | Input with leading and trailing decorators | | Item | Row of media, text and actions for lists and settings | | Label | Form field label with required, invalid and disabled states | | LineChart | Animated time series, drawn on the UI thread | | Loader | Nine loading animations behind one variant prop | | Map | Vector map whose basemap is drawn from your theme tokens | | Marker | Inline note between conversation turns | | Menu | The list of things you can do to something | | Message | Chat turn with avatar, bubble, header and footer | | MessageScroller | Scroll behaviour a chat transcript needs | | NumberInput | Numeric field stepped by buttons or typed by hand | | OtpInput | One-time-code field drawn as a row of separate cells | | Pagination | Paged navigation over a long result set | | Panelside | Collapsible side panel with its own search, groups and scenes | | Plan | What an agent intends to do, before it does it | | Popover | Panel anchored to the element that opened it | | Post | Social card — author, body, media and the counts underneath | | Progress | Determinate and indeterminate progress bar | | RadioGroup | Single-select list of options | | Rating | A row of stars to read or set a score | | Reasoning | An agent’s thinking, collapsed until you want it | | Response | Streamed markdown rendered as native components | | RingChart | Concentric arcs, each measured against its own target | | ScrollCanvas | Image frame whose contents move as you scroll | | ScrollFade | Fades the edges of a scroll container | | ScrollText | Text that resolves word by word as you scroll | | SectionRail | Floating section navigator for a long screen | | Select | Picker shown in a bottom sheet, expanded in place, or floating over the page, with an optional filter | | Separator | Horizontal or vertical rule between content, optionally labelled | | Shimmer | Animated highlight sweeping across content | | Signature | Sign with a finger, and get the result back out as SVG or PNG | | Skeleton | Shimmer placeholder for loading content | | Slider | Pick a value by dragging a thumb along a track | | Soundwave | What a voice looks like while an app listens | | Sources | The references behind an answer, behind a disclosure | | Spinner | Indeterminate loading indicator | | Steps | Stepper for multi-step flows | | Surface | Elevated container with a variant ladder | | Swipe | A row that slides aside to reveal the things you can do to it | | Switch | Animated on/off toggle | | Table | Rows and columns that stay lined up, with sortable headers | | Tabs | Segmented navigation with an animated indicator | | Task | A step an agent is working through, with its files | | Textarea | Multi-line text field that can grow with its content | | ThinkingOrb | Dotted orb saying which kind of busy an agent is | | Timeline | Vertical sequence of events | | TimePicker | Pick a time — as a wheel, a ruler, or a pair of fields | | Toast | Transient notification queue with swipe to dismiss | | ToggleButton | A button that stays down, on its own or in a group | | Tooltip | A small label that names the control under your finger | | Typography | Semantic text presets |

Plus PanelUIProvider, Portal, AnimatedPressable, KeyboardAvoider, ScrollProgress, Text, a set of SVG icons (with brand marks for Google, Facebook and Apple), and the cn class-merging helper.

Hooks: useTheme, useThemeMode, useToast, useCopyToClipboard, useDisclosure, useBreakpoint, useKeyboard, useKeyboardAvoidance, useScrollSections, useRevealProgress, useDebouncedValue, usePrevious.

Icons inside a coloured surface inherit a readable colour automatically — Button provides the foreground its variant reads against, so an icon in startContent follows the theme without a hardcoded hex. Wrap your own surfaces in IconColorProvider to do the same.

Every component takes a className, so anything can be restyled with Tailwind utilities:

<Button className="w-full rounded-full" labelClassName="uppercase">
  Continue
</Button>

Toasts

import { useToast } from 'panelui-native';

const { toast } = useToast();

toast.show('Link copied');
toast.show({
  variant: 'success',
  label: 'Deployment complete',
  description: 'panelui.dev is live on production.',
  actionLabel: 'View',
  onActionPress: ({ hide }) => hide(),
});

The queue lives outside React, so toast.show() also works from API clients and other non-component code.

Theming and dark mode

PanelUI ships three theme families, each in light and dark: Panel (light / dark), Moon (moon / moon-dark) and Grass (grass / grass-dark). A family sets its own radius scale as well as its own palette — Panel is the default, Moon is sharp and monochrome, Grass is soft and green.

import { useTheme, useThemeMode, PANEL_THEMES } from 'panelui-native';

// Switch to a specific theme
const { theme, setTheme } = useTheme();
setTheme('moon-dark');
setTheme('system'); // follow the device

// Or treat brand and light/dark as separate axes
const { family, mode, setFamily, toggleMode } = useThemeMode();
toggleMode();            // dark ↔ light, staying in the current brand
setFamily('grass');      // switch family, staying in the current mode

Tokens are plain CSS variables, so you can override any of them in your own global.css:

@import 'panelui-native/theme.css';

@layer theme {
  :root {
    @variant dark {
      --color-primary: #818cf8;
    }
  }
}

Using Expo Router? Read this

React Navigation paints its own theme background over every screen, and it defaults to an opaque light grey — which sits on top of PanelUIProvider's background and makes theme switching look like it does nothing. Feed it the live PanelUI tokens:

import { DarkTheme, DefaultTheme, Stack, ThemeProvider } from 'expo-router';
import { useCSSVariable } from 'uniwind';
import { useThemeMode } from 'panelui-native';

function ThemedNavigation() {
  const { mode } = useThemeMode();
  const [background, card, text, border] = useCSSVariable([
    '--color-background', '--color-card', '--color-foreground', '--color-border',
  ]) as (string | undefined)[];

  const base = mode === 'dark' ? DarkTheme : DefaultTheme;

  return (
    <ThemeProvider value={{ ...base, dark: mode === 'dark',
                            colors: { ...base.colors, background, card, text, border } }}>
      <Stack />
    </ThemeProvider>
  );
}

useCSSVariable subscribes to Uniwind's theme changes, so this re-runs on every switch — including the named themes, which the OS Appearance API knows nothing about. For the same reason, drive <StatusBar> from mode rather than style="auto".

FAQ

The first bundle fails with Unable to resolve module …

A required package is missing. Run the install command again — all of it, not just the package named in the error. Metro resolves every import in the library, so this happens even for components you never use.

Cannot use @variant with unknown variant: moon when building

Fixed in 0.22.5 — upgrade with npx expo install panelui-native. Before that release the Moon and Grass token blocks leaned on variants that only existed in the artifact Uniwind generates from your Metro config, so the dev server worked while npx expo export and EAS builds failed.

None of my classes do anything

In order of likelihood: metro.config.js is not in the project root; it does not wrap the config in withUniwindConfig; cssEntryFile does not point at your CSS file; import './global.css' is missing from the entry file; or the dev server was running when you changed one of those. Full list at Troubleshooting.

How is PanelUI different from NativeWind?

NativeWind is a styling engine; PanelUI is a component library. PanelUI is built on Uniwind, a faster Tailwind v4 engine for React Native that skips the Babel transform and applies theme changes natively.

Does it work with Expo Go?

Yes. PanelUI is pure TypeScript with no native modules, so no development build or prebuild is required.

Is it accessible?

Every interactive component sets an accessibilityRole, mirrors its state through accessibilityState, and exposes labels. Decorative icons are hidden from screen readers.

Can I use it in a bare React Native app?

Yes, as long as Uniwind, Reanimated and Gesture Handler are configured. Expo is the tested path.

Why do my Moon or Grass themes throw "it was not registered"?

Named themes must be listed in extraThemes in your Metro config — see Quick start.

Links

License

MIT © Khalid Abdi