gluestack-ui - Theming
Expert knowledge of gluestack-ui's theming system, design tokens, and NativeWind integration for creating consistent, customizable UI across React and React Native.
Overview
gluestack-ui uses NativeWind (Tailwind CSS for React Native) for styling. The theming system provides design tokens, dark mode support, and customization through Tailwind configuration.
Key Concepts
Configuration File
gluestack-ui projects use gluestack-ui.config.json at the project root:
{ "tailwind": { "config": "tailwind.config.js", "css": "global.css" }, "components": { "path": "components/ui" }, "typescript": true, "framework": "expo" }
Theme Provider Setup
Wrap your application with the GluestackUIProvider:
// App.tsx import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'; import { config } from '@/components/ui/gluestack-ui-provider/config';
export default function App() { return ( <GluestackUIProvider config={config}> <YourApp /> </GluestackUIProvider> ); }
For dark mode support:
import { useColorScheme } from 'react-native'; import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider';
export default function App() { const colorScheme = useColorScheme();
return ( <GluestackUIProvider mode={colorScheme === 'dark' ? 'dark' : 'light'}> <YourApp /> </GluestackUIProvider> ); }
NativeWind Configuration
Configure Tailwind CSS via tailwind.config.js :
// tailwind.config.js const { theme } = require('@gluestack-ui/nativewind-utils/theme');
/** @type {import('tailwindcss').Config} / module.exports = { darkMode: 'class', content: [ './app/**/.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}', ], presets: [require('nativewind/preset')], theme: { extend: { colors: theme.colors, fontFamily: theme.fontFamily, fontSize: theme.fontSize, borderRadius: theme.borderRadius, boxShadow: theme.boxShadow, }, }, plugins: [], };
Design Tokens
Color Tokens
gluestack-ui provides semantic color tokens:
// tailwind.config.js module.exports = { theme: { extend: { colors: { // Primary colors primary: { 0: '#E5F4FF', 50: '#CCE9FF', 100: '#B3DEFF', 200: '#80C8FF', 300: '#4DB3FF', 400: '#1A9DFF', 500: '#0077E6', 600: '#005CB3', 700: '#004080', 800: '#00264D', 900: '#000D1A', 950: '#00060D', }, // Secondary colors secondary: { 0: '#F5F5F5', 50: '#E6E6E6', // ... more shades }, // Semantic colors success: { 50: '#ECFDF5', 500: '#22C55E', 700: '#15803D', }, warning: { 50: '#FFFBEB', 500: '#F59E0B', 700: '#B45309', }, error: { 50: '#FEF2F2', 500: '#EF4444', 700: '#B91C1C', }, info: { 50: '#EFF6FF', 500: '#3B82F6', 700: '#1D4ED8', }, // Typography colors typography: { 0: '#FFFFFF', 50: '#F9FAFB', 100: '#F3F4F6', 200: '#E5E7EB', 300: '#D1D5DB', 400: '#9CA3AF', 500: '#6B7280', 600: '#4B5563', 700: '#374151', 800: '#1F2937', 900: '#111827', 950: '#030712', }, // Background colors background: { 0: '#FFFFFF', 50: '#F9FAFB', 100: '#F3F4F6', 200: '#E5E7EB', // Dark mode variants dark: '#0F172A', }, // Outline/border colors outline: { 0: '#FFFFFF', 50: '#F9FAFB', 100: '#F3F4F6', 200: '#E5E7EB', 300: '#D1D5DB', }, }, }, }, };
Typography Tokens
Configure font families and sizes:
// tailwind.config.js module.exports = { theme: { extend: { fontFamily: { heading: ['Inter-Bold', 'sans-serif'], body: ['Inter-Regular', 'sans-serif'], mono: ['JetBrainsMono-Regular', 'monospace'], }, fontSize: { '2xs': ['10px', { lineHeight: '14px' }], xs: ['12px', { lineHeight: '16px' }], sm: ['14px', { lineHeight: '20px' }], md: ['16px', { lineHeight: '24px' }], lg: ['18px', { lineHeight: '28px' }], xl: ['20px', { lineHeight: '28px' }], '2xl': ['24px', { lineHeight: '32px' }], '3xl': ['30px', { lineHeight: '36px' }], '4xl': ['36px', { lineHeight: '40px' }], '5xl': ['48px', { lineHeight: '1' }], '6xl': ['60px', { lineHeight: '1' }], }, }, }, };
Spacing and Sizing
Consistent spacing scale:
// tailwind.config.js module.exports = { theme: { extend: { spacing: { px: '1px', 0: '0px', 0.5: '2px', 1: '4px', 1.5: '6px', 2: '8px', 2.5: '10px', 3: '12px', 3.5: '14px', 4: '16px', 5: '20px', 6: '24px', 7: '28px', 8: '32px', 9: '36px', 10: '40px', 11: '44px', 12: '48px', 14: '56px', 16: '64px', 20: '80px', 24: '96px', 28: '112px', 32: '128px', }, borderRadius: { none: '0px', sm: '2px', DEFAULT: '4px', md: '6px', lg: '8px', xl: '12px', '2xl': '16px', '3xl': '24px', full: '9999px', }, }, }, };
Dark Mode
Automatic Dark Mode
Use system color scheme:
import { useColorScheme } from 'react-native'; import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider';
function App() { const colorScheme = useColorScheme();
return ( <GluestackUIProvider mode={colorScheme === 'dark' ? 'dark' : 'light'}> <MainApp /> </GluestackUIProvider> ); }
Manual Dark Mode Toggle
Create a theme context for manual control:
// contexts/ThemeContext.tsx import { createContext, useContext, useState, useEffect } from 'react'; import { useColorScheme } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage';
type ThemeMode = 'light' | 'dark' | 'system';
interface ThemeContextType { mode: ThemeMode; resolvedMode: 'light' | 'dark'; setMode: (mode: ThemeMode) => void; }
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) { const systemColorScheme = useColorScheme(); const [mode, setModeState] = useState<ThemeMode>('system');
useEffect(() => { AsyncStorage.getItem('theme-mode').then((stored) => { if (stored) setModeState(stored as ThemeMode); }); }, []);
const setMode = (newMode: ThemeMode) => { setModeState(newMode); AsyncStorage.setItem('theme-mode', newMode); };
const resolvedMode: 'light' | 'dark' = mode === 'system' ? (systemColorScheme ?? 'light') : mode;
return ( <ThemeContext.Provider value={{ mode, resolvedMode, setMode }}> {children} </ThemeContext.Provider> ); }
export function useTheme() { const context = useContext(ThemeContext); if (!context) throw new Error('useTheme must be used within ThemeProvider'); return context; }
Usage in App:
// App.tsx import { ThemeProvider, useTheme } from '@/contexts/ThemeContext'; import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider';
function ThemedApp() { const { resolvedMode } = useTheme();
return ( <GluestackUIProvider mode={resolvedMode}> <MainApp /> </GluestackUIProvider> ); }
export default function App() { return ( <ThemeProvider> <ThemedApp /> </ThemeProvider> ); }
Dark Mode Styling
Use dark: prefix for dark mode styles:
<Box className="bg-background-0 dark:bg-background-dark"> <Text className="text-typography-900 dark:text-typography-50"> Hello World </Text> </Box>
Best Practices
- Use Semantic Color Tokens
Use semantic tokens instead of literal colors:
// Good: Semantic tokens <Box className="bg-background-0 dark:bg-background-dark"> <Text className="text-typography-900 dark:text-typography-0">Content</Text> <Button action="primary"> <ButtonText>Action</ButtonText> </Button> </Box>
// Avoid: Literal colors <Box className="bg-white dark:bg-slate-900"> <Text className="text-gray-900 dark:text-white">Content</Text> </Box>
- Create a Design System File
Centralize design decisions:
// design-system/tokens.ts export const tokens = { colors: { brand: { primary: 'primary-500', secondary: 'secondary-500', accent: 'info-500', }, feedback: { success: 'success-500', warning: 'warning-500', error: 'error-500', }, }, spacing: { page: 'px-4 py-6', section: 'py-8', card: 'p-4', }, radius: { card: 'rounded-xl', button: 'rounded-lg', input: 'rounded-md', }, } as const;
// Usage import { tokens } from '@/design-system/tokens';
<Box className={bg-${tokens.colors.brand.primary} ${tokens.spacing.card} ${tokens.radius.card}}>
- Extend Theme Properly
Extend rather than override the base theme:
// tailwind.config.js const { theme: gluestackTheme } = require('@gluestack-ui/nativewind-utils/theme');
module.exports = { theme: { extend: { // Extend colors, don't replace colors: { ...gluestackTheme.colors, // Add brand colors brand: { 50: '#FFF5F7', 100: '#FFEAEF', 500: '#FF1493', 600: '#DB1086', 700: '#B80D6E', }, }, }, }, };
- Create Reusable Style Utilities
Build consistent style helpers:
// utils/styles.ts import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
// Card styles export const cardStyles = cn( 'bg-background-0 dark:bg-background-100', 'border border-outline-200 dark:border-outline-700', 'rounded-xl', 'p-4' );
// Interactive states export const interactiveStyles = cn( 'active:opacity-80', 'focus:ring-2 focus:ring-primary-500 focus:ring-offset-2' );
- Handle Platform-Specific Theming
Account for platform differences:
import { Platform } from 'react-native';
// Platform-specific shadows const shadowClass = Platform.select({ ios: 'shadow-md', android: 'elevation-4', web: 'shadow-lg', });
<Box className={cn('bg-background-0 rounded-xl', shadowClass)}> <Text>Card content</Text> </Box>
Examples
Custom Theme Configuration
Complete custom theme setup:
// tailwind.config.js const { theme: gluestackTheme } = require('@gluestack-ui/nativewind-utils/theme');
/** @type {import('tailwindcss').Config} / module.exports = { darkMode: 'class', content: [ './app/**/.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}', ], presets: [require('nativewind/preset')], theme: { extend: { colors: { ...gluestackTheme.colors, // Custom brand palette brand: { 50: '#F0F9FF', 100: '#E0F2FE', 200: '#BAE6FD', 300: '#7DD3FC', 400: '#38BDF8', 500: '#0EA5E9', 600: '#0284C7', 700: '#0369A1', 800: '#075985', 900: '#0C4A6E', 950: '#082F49', }, // Override primary to use brand primary: { 50: '#F0F9FF', 100: '#E0F2FE', 200: '#BAE6FD', 300: '#7DD3FC', 400: '#38BDF8', 500: '#0EA5E9', 600: '#0284C7', 700: '#0369A1', 800: '#075985', 900: '#0C4A6E', 950: '#082F49', }, }, fontFamily: { heading: ['Poppins-Bold', 'sans-serif'], body: ['Poppins-Regular', 'sans-serif'], mono: ['FiraCode-Regular', 'monospace'], }, borderRadius: { ...gluestackTheme.borderRadius, card: '16px', button: '12px', }, }, }, plugins: [], };
Theme Switcher Component
import { useState } from 'react'; import { HStack } from '@/components/ui/hstack'; import { Button, ButtonText, ButtonIcon } from '@/components/ui/button'; import { SunIcon, MoonIcon, MonitorIcon } from 'lucide-react-native'; import { useTheme } from '@/contexts/ThemeContext';
type ThemeOption = 'light' | 'dark' | 'system';
export function ThemeSwitcher() { const { mode, setMode } = useTheme();
const options: { value: ThemeOption; icon: typeof SunIcon; label: string }[] = [ { value: 'light', icon: SunIcon, label: 'Light' }, { value: 'dark', icon: MoonIcon, label: 'Dark' }, { value: 'system', icon: MonitorIcon, label: 'System' }, ];
return ( <HStack space="sm"> {options.map((option) => ( <Button key={option.value} variant={mode === option.value ? 'solid' : 'outline'} action={mode === option.value ? 'primary' : 'secondary'} size="sm" onPress={() => setMode(option.value)} > <ButtonIcon as={option.icon} /> <ButtonText>{option.label}</ButtonText> </Button> ))} </HStack> ); }
Themed Card Component
import { Box } from '@/components/ui/box'; import { VStack } from '@/components/ui/vstack'; import { Heading } from '@/components/ui/heading'; import { Text } from '@/components/ui/text'; import { cn } from '@/utils/styles';
interface ThemedCardProps { title: string; description: string; variant?: 'default' | 'elevated' | 'outlined'; children?: React.ReactNode; }
export function ThemedCard({ title, description, variant = 'default', children, }: ThemedCardProps) { const variantStyles = { default: 'bg-background-0 dark:bg-background-100', elevated: cn( 'bg-background-0 dark:bg-background-100', 'shadow-lg dark:shadow-none', 'dark:border dark:border-outline-700' ), outlined: cn( 'bg-transparent', 'border-2 border-outline-300 dark:border-outline-600' ), };
return ( <Box className={cn('rounded-xl p-4', variantStyles[variant])}> <VStack space="sm"> <Heading size="md" className="text-typography-900 dark:text-typography-50"> {title} </Heading> <Text size="sm" className="text-typography-500 dark:text-typography-400"> {description} </Text> {children} </VStack> </Box> ); }
Common Patterns
Gradient Backgrounds
import { LinearGradient } from 'expo-linear-gradient'; import { Box } from '@/components/ui/box';
function GradientCard({ children }: { children: React.ReactNode }) { return ( <Box className="rounded-xl overflow-hidden"> <LinearGradient colors={['#0EA5E9', '#6366F1']} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} style={{ padding: 16 }} > {children} </LinearGradient> </Box> ); }
Conditional Theme Styles
import { useTheme } from '@/contexts/ThemeContext';
function AdaptiveImage() { const { resolvedMode } = useTheme();
return ( <Image source={ resolvedMode === 'dark' ? require('@/assets/logo-dark.png') : require('@/assets/logo-light.png') } className="w-32 h-32" /> ); }
Anti-Patterns
Do Not Use Hardcoded Colors
// Bad: Hardcoded hex values <Box className="bg-[#FFFFFF] dark:bg-[#1F2937]"> <Text className="text-[#111827] dark:text-[#F9FAFB]">Hello</Text> </Box>
// Good: Semantic tokens <Box className="bg-background-0 dark:bg-background-dark"> <Text className="text-typography-900 dark:text-typography-50">Hello</Text> </Box>
Do Not Mix Theming Systems
// Bad: Mixing StyleSheet with NativeWind const styles = StyleSheet.create({ container: { backgroundColor: '#FFFFFF' }, });
<Box style={styles.container} className="p-4"> <Text>Content</Text> </Box>
// Good: Use NativeWind consistently <Box className="bg-background-0 p-4"> <Text>Content</Text> </Box>
Do Not Forget Dark Mode Variants
// Bad: Missing dark mode <Box className="bg-white border-gray-200"> <Text className="text-gray-900">Content</Text> </Box>
// Good: Include dark mode variants <Box className="bg-background-0 dark:bg-background-dark border-outline-200 dark:border-outline-700"> <Text className="text-typography-900 dark:text-typography-50">Content</Text> </Box>
Related Skills
-
gluestack-components: Building UI with gluestack-ui components
-
gluestack-accessibility: Ensuring accessible implementations