gluestack-theming
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesegluestack-ui - Theming
gluestack-ui - 主题定制
Expert knowledge of gluestack-ui's theming system, design tokens, and NativeWind integration for creating consistent, customizable UI across React and React Native.
深入讲解gluestack-ui的主题系统、设计令牌以及NativeWind集成方案,助力在React与React Native中创建一致性、可定制的UI界面。
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.
gluestack-ui 使用 NativeWind(适用于React Native的Tailwind CSS)进行样式处理。其主题系统提供设计令牌、深色模式支持,并可通过Tailwind配置实现自定义。
Key Concepts
核心概念
Configuration File
配置文件
gluestack-ui projects use at the project root:
gluestack-ui.config.jsonjson
{
"tailwind": {
"config": "tailwind.config.js",
"css": "global.css"
},
"components": {
"path": "components/ui"
},
"typescript": true,
"framework": "expo"
}gluestack-ui项目在根目录使用文件:
gluestack-ui.config.jsonjson
{
"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:
tsx
// 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:
tsx
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>
);
}使用GluestackUIProvider包裹你的应用:
tsx
// 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>
);
}如需支持深色模式:
tsx
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
NativeWind配置
Configure Tailwind CSS via :
tailwind.config.jsjavascript
// 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: [],
};通过配置Tailwind CSS:
tailwind.config.jsjavascript
// 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:
javascript
// 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',
},
},
},
},
};gluestack-ui提供语义化颜色令牌:
javascript
// tailwind.config.js
module.exports = {
theme: {
extend: {
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: {
0: '#F5F5F5',
50: '#E6E6E6',
// ... 更多色调
},
// 语义反馈色
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: {
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: {
0: '#FFFFFF',
50: '#F9FAFB',
100: '#F3F4F6',
200: '#E5E7EB',
// 深色模式变体
dark: '#0F172A',
},
// 边框/轮廓色
outline: {
0: '#FFFFFF',
50: '#F9FAFB',
100: '#F3F4F6',
200: '#E5E7EB',
300: '#D1D5DB',
},
},
},
},
};Typography Tokens
排版令牌
Configure font families and sizes:
javascript
// 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' }],
},
},
},
};配置字体族与字号:
javascript
// 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:
javascript
// 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',
},
},
},
};统一的间距尺度:
javascript
// 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:
tsx
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>
);
}使用系统配色方案:
tsx
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:
tsx
// 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:
tsx
// 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>
);
}创建主题上下文以实现手动控制:
tsx
// 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;
}在应用中使用:
tsx
// 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:
tsx
<Box className="bg-background-0 dark:bg-background-dark">
<Text className="text-typography-900 dark:text-typography-50">
Hello World
</Text>
</Box>使用dark:前缀定义深色模式样式:
tsx
<Box className="bg-background-0 dark:bg-background-dark">
<Text className="text-typography-900 dark:text-typography-50">
Hello World
</Text>
</Box>Best Practices
最佳实践
1. Use Semantic Color Tokens
1. 使用语义化颜色令牌
Use semantic tokens instead of literal colors:
tsx
// 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>使用语义化令牌而非字面颜色值:
tsx
// 推荐:语义化令牌
<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>
// 避免:硬编码颜色
<Box className="bg-white dark:bg-slate-900">
<Text className="text-gray-900 dark:text-white">Content</Text>
</Box>2. Create a Design System File
2. 创建设计系统文件
Centralize design decisions:
typescript
// 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}`}>集中管理设计决策:
typescript
// 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;
// 使用示例
import { tokens } from '@/design-system/tokens';
<Box className={`bg-${tokens.colors.brand.primary} ${tokens.spacing.card} ${tokens.radius.card}`}>3. Extend Theme Properly
3. 正确扩展主题
Extend rather than override the base theme:
javascript
// 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',
},
},
},
},
};扩展而非覆盖基础主题:
javascript
// tailwind.config.js
const { theme: gluestackTheme } = require('@gluestack-ui/nativewind-utils/theme');
module.exports = {
theme: {
extend: {
// 扩展颜色,而非替换
colors: {
...gluestackTheme.colors,
// 添加品牌颜色
brand: {
50: '#FFF5F7',
100: '#FFEAEF',
500: '#FF1493',
600: '#DB1086',
700: '#B80D6E',
},
},
},
},
};4. Create Reusable Style Utilities
4. 创建可复用样式工具
Build consistent style helpers:
typescript
// 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'
);构建统一的样式辅助函数:
typescript
// utils/styles.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// 卡片样式
export const cardStyles = cn(
'bg-background-0 dark:bg-background-100',
'border border-outline-200 dark:border-outline-700',
'rounded-xl',
'p-4'
);
// 交互状态样式
export const interactiveStyles = cn(
'active:opacity-80',
'focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
);5. Handle Platform-Specific Theming
5. 处理平台特定主题
Account for platform differences:
tsx
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>适配不同平台的差异:
tsx
import { Platform } from 'react-native';
// 平台专属阴影样式
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:
javascript
// 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: [],
};完整的自定义主题设置:
javascript
// 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,
// 自定义品牌调色板
brand: {
50: '#F0F9FF',
100: '#E0F2FE',
200: '#BAE6FD',
300: '#7DD3FC',
400: '#38BDF8',
500: '#0EA5E9',
600: '#0284C7',
700: '#0369A1',
800: '#075985',
900: '#0C4A6E',
950: '#082F49',
},
// 覆盖主色调为品牌色
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
主题切换器组件
tsx
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>
);
}tsx
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: '浅色' },
{ value: 'dark', icon: MoonIcon, label: '深色' },
{ value: 'system', icon: MonitorIcon, label: '跟随系统' },
];
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
主题化卡片组件
tsx
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>
);
}tsx
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
渐变背景
tsx
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>
);
}tsx
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
条件主题样式
tsx
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"
/>
);
}tsx
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
避免硬编码颜色
tsx
// 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>tsx
// 不推荐:硬编码十六进制值
<Box className="bg-[#FFFFFF] dark:bg-[#1F2937]">
<Text className="text-[#111827] dark:text-[#F9FAFB]">Hello</Text>
</Box>
// 推荐:使用语义化令牌
<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
避免混合主题系统
tsx
// 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>tsx
// 不推荐:混合StyleSheet与NativeWind
const styles = StyleSheet.create({
container: { backgroundColor: '#FFFFFF' },
});
<Box style={styles.container} className="p-4">
<Text>Content</Text>
</Box>
// 推荐:统一使用NativeWind
<Box className="bg-background-0 p-4">
<Text>Content</Text>
</Box>Do Not Forget Dark Mode Variants
不要忘记深色模式变体
tsx
// 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>tsx
// 不推荐:缺失深色模式
<Box className="bg-white border-gray-200">
<Text className="text-gray-900">Content</Text>
</Box>
// 推荐:包含深色模式变体
<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
- gluestack-components: 使用gluestack-ui组件构建UI界面
- gluestack-accessibility: 确保实现无障碍访问特性