Installation

Install PanelUI in an Expo app — packages, Metro config, Tailwind entry, and the provider.

PanelUI needs four things: the packages, a Metro plugin, a CSS entry, and the provider at the root of your app.

Install

npx expo install panelui-native uniwind tailwindcss react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-svg react-native-worklets

Expo SDK 57+

PanelUI targets Expo SDK 57 and React Native 0.86. It contains no native modules, so it runs in Expo Go — no development build or prebuild required.

Configure Metro

Uniwind compiles your Tailwind classes at bundle time, so it needs to wrap the Metro config.

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

const config = getDefaultConfig(__dirname);

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

Restart after changing extraThemes

Metro reads this file once at startup. A dev server started before you edited extraThemes keeps the old list in memory and will rewrite Uniwind's generated CSS with it — producing Theme … is missing variable … errors, or Cannot use @variant with unknown variant. Restart the dev server; --clear on a running one is not enough.

Create global.css

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

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

The @source line tells Tailwind to scan PanelUI's own class names, so its styles are included in your bundle.

Wrap your app

App.tsx
import './global.css';
import { PanelUIProvider } from 'panelui-native';

export default function App() {
  return <PanelUIProvider>{/* your app */}</PanelUIProvider>;
}

PanelUIProvider owns four things: the gesture handler root, the themed page background, the portal host used by overlays (Dialog, BottomSheet, Select), and the toast viewport.

Using Expo Router

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

app/_layout.tsx
import { DarkTheme, DefaultTheme, Stack, ThemeProvider } from 'expo-router';
import { useCSSVariable } from 'uniwind';
import { PanelUIProvider, 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>
  );
}

export default function RootLayout() {
  return (
    <PanelUIProvider>
      <ThemedNavigation />
    </PanelUIProvider>
  );
}

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".

TypeScript

Uniwind generates uniwind-types.d.ts on first bundle, which types your registered themes. Commit it, or add it to .gitignore and let it regenerate.

On this page