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-workletsExpo 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.
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
@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
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:
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.