* fix(theme): restore original web colors for info, success, and warning
Restores web's original color values for info, success, and warning colors
in both light and dark modes. The unified theme refactor (#6861) used
mobile's palette as the base, which had different tints that caused
unexpected color changes in the web app.
Changes:
- Light mode info: #00BFE5 → #5FDDFF (brighter cyan)
- Light mode warning: #FF8C00 → #FF8061 (coral vs orange)
- Dark mode colors also restored to original web values
- Updated MUI theme generator with web-specific overrides
- Updated CSS vars generator to use web colors
- Regenerated vars.css with correct colors
- Updated snapshot test for theme changes
This maintains the unified theme system while preserving platform-specific
color preferences - mobile keeps its colors, web gets its originals back.
Fixes colors regression from #6861
* test(web): update Storybook snapshots for restored colors
Update 16 Storybook snapshots to reflect the restored web colors for
info, success, and warning. MUI generates different CSS class hashes when
theme colors change, causing snapshot differences.
Updated snapshots:
- SettingsChange stories
- StakingStatus stories
- Swap component stories (StatusLabel, SwapOrder, SwapProgress, etc.)
* docs(theme): add unified theme architecture documentation
Comprehensive documentation of the unified theme system including:
- Architecture diagram showing flow from palettes to platform themes
- How theme generation works for web (MUI) and mobile (Tamagui)
- Complete table of color divergences between platforms
- Why colors differ (design language, semantic consistency, tint philosophy)
- How to modify shared vs platform-specific colors
- Directory structure and usage examples
Key insights:
- Web uses softer, pastel tints with subtle backgrounds
- Mobile uses more saturated colors with stronger contrast
- Web maintains semantic dark/light meaning in both modes
- Mobile inverts dark/light shades in dark mode
- 11 colors diverge in light mode, 11 in dark mode
* fix(docs): correct Mermaid diagram syntax in theme architecture
Replace HTML br tags with proper Mermaid multi-line text syntax.
Adds emoji icons to subgraph labels for better visual organization.
* fix(theme): restore original web error colors
Add error color overrides alongside info/success/warning fixes.
Error colors also diverged between web and mobile during the unified
theme refactor, following the same inversion pattern in dark mode.
Changes:
- Light mode error.dark: #8A1C27 → #AC2C3B (brighter red)
- Light mode error.light: #F79BA7 → #FFB4BD (lighter pink)
- Light mode error.background: #FFE0E6 → #FFE6EA (different tint)
- Dark mode error.dark: #FFE0E6 → #AC2C3B (semantic consistency)
- Dark mode error.light: #4A2125 → #FFB4BD (semantic consistency)
- Dark mode error.background: #4A2125 → #2F2527 (darker)
Updated MUI generator, CSS vars generator, and test expectations.
* docs(theme): add error colors to divergence tables
Update architecture documentation to include error color divergences:
- Add 3 error color rows to light mode table (14 total rows now)
- Add 3 error color rows to dark mode table (14 total rows now)
- Update total divergences count: 22 → 28 color properties
- Update Key Insights to mention error colors
* test(web): update Storybook snapshots for error colors
Update 2 Storybook snapshots for SwapOrder stories to reflect the
restored web error colors. MUI generates different CSS class hashes
when theme colors change.
* test(web): update remaining Storybook snapshots for error colors
Update CustomTooltip and StatusLabel snapshots for error color changes.
* fix(ci): fix Storybook screenshot capture timeout
The screenshot capture script was waiting for 'networkidle' which never
occurs for some stories due to HMR/polling. This caused all affected
stories to timeout and show ERROR screenshots.
Changes:
- Change page.goto to wait for 'domcontentloaded' instead of 'networkidle'
- Add explicit wait times for story rendering (3s + 1s)
- Remove the secondary networkidle wait that was causing issues
This fixes screenshot capture for CustomTooltip and swap feature stories.
* chore: trigger deployment with screenshot fix
This empty commit triggers a new deployment that includes the screenshot
capture script fix from the previous commit.
* fix(ci): increase screenshot capture timeouts for CI environment
Increased wait times to account for slower CI environment:
- Initial wait: 3s → 5s
- Visibility timeout: 10s → 15s
- Final wait: 1s → 2s
* Revert "fix(styles): Adjust text color for the Add Funds banner (dark theme) (#6878)"
This reverts commit c5296e22e6.
---------
Co-authored-by: Hanzo Dev <[email protected]>
16 KiB
Unified Theme System Architecture
This document explains how the unified theme system works across web (MUI) and mobile (Tamagui) platforms in the Safe Wallet monorepo.
Overview
The @safe-global/theme package serves as a single source of truth for design tokens (colors, spacing, typography, radius) across both web and mobile applications. It provides:
- Unified color palettes (light, dark, static)
- Platform-specific generators (MUI for web, Tamagui for mobile, CSS variables for web)
- Dual spacing systems (4px base for mobile, 8px base for web)
- Platform-specific color overrides (web uses different tints than mobile)
Architecture Diagram
flowchart TB
subgraph palettes["📦 Source Palettes"]
LP["Light Palette
palettes/light.ts"]
DP["Dark Palette
palettes/dark.ts"]
SP["Static Colors
palettes/static.ts"]
end
subgraph tokens["🎨 Tokens"]
T1["Typography"]
T2["Spacing"]
T3["Radius"]
end
subgraph generators["⚙️ Platform Generators"]
MUI["MUI Generator
generateMuiTheme"]
TAMA["Tamagui Generator
generateTamaguiTokens"]
CSS["CSS Vars Generator
generateCSSVars"]
end
subgraph overrides["🎯 Web Overrides"]
WO["Color Overrides
info, success, warning"]
end
subgraph output["📱 Platform Themes"]
WT["Web Theme
MUI Theme Object"]
MT["Mobile Theme
Tamagui Config"]
CV["CSS Variables
vars.css"]
end
subgraph apps["🚀 Applications"]
WEB["Web App
Next.js + MUI"]
MOB["Mobile App
Expo + Tamagui"]
end
LP --> MUI
DP --> MUI
SP --> MUI
LP --> TAMA
DP --> TAMA
SP --> TAMA
LP --> CSS
DP --> CSS
SP --> CSS
T1 --> MUI
T1 --> TAMA
T2 --> MUI
T2 --> TAMA
T3 --> MUI
T3 --> TAMA
WO --> MUI
WO --> CSS
MUI --> WT
TAMA --> MT
CSS --> CV
WT --> WEB
CV --> WEB
MT --> MOB
style WO fill:#ffeb99,stroke:#ff9900,stroke-width:2px
style LP fill:#e1f5ff,stroke:#0288d1
style DP fill:#424242,stroke:#212121,color:#fff
style SP fill:#f3e5f5,stroke:#7b1fa2
Directory Structure
packages/theme/
├── src/
│ ├── palettes/
│ │ ├── light.ts # Unified light mode palette
│ │ ├── dark.ts # Unified dark mode palette
│ │ ├── static.ts # Theme-independent brand colors
│ │ └── types.ts # TypeScript color palette types
│ ├── tokens/
│ │ ├── typography.ts # Font family, sizes, weights
│ │ ├── spacing.ts # Mobile (4px) & Web (8px) spacing
│ │ └── radius.ts # Border radius scale
│ ├── generators/
│ │ ├── mui.ts # MUI theme generator (web)
│ │ ├── tamagui.ts # Tamagui theme generator (mobile)
│ │ ├── css-vars.ts # CSS variables generator (web)
│ │ └── mui-extensions.ts # MUI TypeScript type extensions
│ └── index.ts # Public API exports
├── package.json
├── tsconfig.json
└── README.md
How Theme Generation Works
1. Source Palettes (Base)
The unified palettes in packages/theme/src/palettes/ define colors that are mostly shared between platforms:
// packages/theme/src/palettes/light.ts
const lightPalette: ColorPalette = {
text: { primary: '#121312', secondary: '#A1A3A7', ... },
primary: { dark: '#3c3c3c', main: '#121312', light: '#636669' },
success: { dark: '#1C5538', main: '#00B460', ... }, // Mobile colors
info: { dark: '#15566A', main: '#00BFE5', ... }, // Mobile colors
warning: { dark: '#6C2D19', main: '#FF8C00', ... }, // Mobile colors
// ...
}
Important: The base palettes use mobile's color values for info, success, and warning.
2. Web Generator with Platform Overrides
The MUI generator (packages/theme/src/generators/mui.ts) applies web-specific overrides for colors that differ from mobile:
export function generateMuiTheme(mode: PaletteMode): Theme {
const isDarkMode = mode === 'dark'
const colors = isDarkMode ? darkPalette : lightPalette
return createTheme({
palette: {
mode: isDarkMode ? 'dark' : 'light',
...colors,
// Web-specific color overrides (different tints than mobile)
...(isDarkMode
? {
success: { dark: '#388E3C', main: '#00B460', light: '#81C784', background: '#1F2920' },
info: { dark: '#52BFDC', main: '#5FDDFF', light: '#B7F0FF', background: '#19252C' },
warning: { dark: '#C04C32', main: '#FF8061', light: '#FFBC9F', background: '#2F2318' },
}
: {
success: { dark: '#028D4C', main: '#00B460', light: '#D3F2E4', background: '#EFFAF1' },
info: { dark: '#52BFDC', main: '#5FDDFF', light: '#D7F6FF', background: '#EFFCFF' },
warning: { dark: '#C04C32', main: '#FF8061', light: '#FFBC9F', background: '#FFF1E0' },
}),
},
// ... typography, spacing, component overrides
})
}
3. CSS Variables Generator
The CSS variables generator (packages/theme/src/generators/css-vars.ts) also applies web-specific overrides:
export function generateCSSVars(): string {
// Apply web-specific color overrides
const webLightPalette: ColorPalette = {
...lightPalette,
success: {
/* web colors */
},
info: {
/* web colors */
},
warning: {
/* web colors */
},
}
const webDarkPalette: ColorPalette = {
...darkPalette,
success: {
/* web colors */
},
info: {
/* web colors */
},
warning: {
/* web colors */
},
}
// Generate CSS custom properties
const lightVars = flattenPaletteToCSS(webLightPalette)
const darkVars = flattenPaletteToCSS(webDarkPalette)
// Output as :root and [data-theme="dark"] rules
}
4. Tamagui Generator (Mobile)
The Tamagui generator (packages/theme/src/generators/tamagui.ts) uses the base palettes without overrides:
export function generateTamaguiTokens() {
return {
color: {
...flattenPalette(lightPalette, { suffix: 'Light' }),
...flattenPalette(darkPalette, { suffix: 'Dark' }),
},
// ... other tokens
}
}
Mobile uses the colors from the unified palettes directly.
Color Divergences Between Platforms
The following colors have different tints between web and mobile:
Light Mode Divergences
| Color | Component | Web (Original) | Mobile (Unified Base) | Reason |
|---|---|---|---|---|
| error.dark | Dark error shade | #AC2C3B (medium red) |
#8A1C27 (darker, less saturated) |
Mobile uses darker shades |
| error.light | Light error shade | #FFB4BD (light pink) |
#F79BA7 (different light pink) |
Different tint preferences |
| error.background | Error background | #FFE6EA (very light pink) |
#FFE0E6 (slightly different) |
Web prefers warmer tints |
| info.main | Main info color | #5FDDFF (bright cyan) |
#00BFE5 (darker cyan) |
Different design language |
| info.dark | Dark info shade | #52BFDC |
#15566A (much darker, teal-ish) |
Web uses brighter tints |
| info.light | Light info shade | #D7F6FF (very light blue) |
#78D2E7 (medium blue) |
Web has lighter backgrounds |
| info.background | Info background | #EFFCFF (almost white) |
#CEF0FD (more saturated) |
Web prefers subtle backgrounds |
| success.dark | Dark success shade | #028D4C (bright green) |
#1C5538 (darker, desaturated) |
Mobile uses darker shades |
| success.light | Light success shade | #D3F2E4 (very light green) |
#84D9A0 (medium green) |
Web has lighter backgrounds |
| success.background | Success background | #EFFAF1 (almost white) |
#CBF2DB (more saturated) |
Web prefers subtle backgrounds |
| warning.main | Main warning color | #FF8061 (coral/salmon) |
#FF8C00 (pure orange) |
Web uses softer, coral tones |
| warning.dark | Dark warning shade | #C04C32 (reddish-orange) |
#6C2D19 (dark brown) |
Mobile uses very dark shades |
| warning.light | Light warning shade | #FFBC9F (light coral) |
#F9B37C (peachy) |
Different color temperature |
| warning.background | Warning background | #FFF1E0 (cream) |
#FFECC2 (yellow-orange) |
Web uses warmer, softer tones |
Dark Mode Divergences
| Color | Component | Web (Original) | Mobile (Unified Base) | Reason |
|---|---|---|---|---|
| error.dark | Dark error shade | #AC2C3B (medium red) |
#FFE0E6 (very light pink!) |
Mobile inverts dark/light in dark mode |
| error.light | Light error shade | #FFB4BD (light pink) |
#4A2125 (very dark!) |
Web keeps semantic meaning |
| error.background | Error background | #2F2527 (dark brown-red) |
#4A2125 (slightly lighter) |
Different background strategies |
| info.main | Main info color | #5FDDFF (bright cyan) |
#00BFE5 (darker cyan) |
Consistent with light mode |
| info.dark | Dark info shade | #52BFDC (bright) |
#D9F4FB (very light) |
Mobile inverts dark/light in dark mode |
| info.light | Light info shade | #B7F0FF (light) |
#458898 (dark) |
Web keeps semantic meaning |
| info.background | Info background | #19252C (dark blue-gray) |
#203339 (slightly lighter) |
Different background strategies |
| success.dark | Dark success shade | #388E3C (medium green) |
#DEFDEA (very light green) |
Mobile inverts dark/light in dark mode |
| success.light | Light success shade | #81C784 (light green) |
#3B7A54 (dark green) |
Web keeps semantic meaning |
| success.background | Success background | #1F2920 (dark green-gray) |
#173026 (slightly different) |
Different background strategies |
| warning.main | Main warning color | #FF8061 (coral) |
#FF8C00 (pure orange) |
Consistent with light mode |
| warning.dark | Dark warning shade | #C04C32 (reddish-orange) |
#FFE4CB (very light) |
Mobile inverts dark/light in dark mode |
| warning.light | Light warning shade | #FFBC9F (light coral) |
#A65F34 (dark brown) |
Web keeps semantic meaning |
| warning.background | Warning background | #2F2318 (dark brown) |
#4A3621 (lighter brown) |
Different background strategies |
Key Insights
-
Semantic Consistency (Web): Web maintains semantic meaning where
.darkis always darker than.mainand.lightis always lighter, regardless of light/dark mode. -
Inversion Pattern (Mobile): Mobile inverts the
.darkand.lightshades in dark mode -.darkbecomes light (for text/foreground),.lightbecomes dark (for backgrounds). -
Tint Philosophy:
- Web: Softer, more pastel tints with subtle backgrounds
- Mobile: More saturated colors with stronger contrast
-
Color Temperature:
- Web warning: Coral/salmon tones (warmer, softer)
- Mobile warning: Pure orange (more saturated)
-
Main Color Alignment:
success.mainis shared (#00B460), buterror.main,info.main, andwarning.maindiffer between platforms. -
Total Divergences: 28 color property differences (14 in light mode + 14 in dark mode) across 4 color groups: error, info, success, and warning.
Why This Architecture?
Before: Duplicated Palettes
Previously, web and mobile had separate, duplicated palette files:
apps/web/src/components/theme/lightPalette.ts
apps/web/src/components/theme/darkPalette.ts
apps/mobile/src/theme/palettes/lightPalette.ts
apps/mobile/src/theme/palettes/darkPalette.ts
Problems:
- Color definitions were duplicated
- Changes had to be made in multiple places
- Colors drifted between platforms over time
- No single source of truth
After: Unified with Platform Overrides
Now there's one set of palettes with platform-specific overrides:
packages/theme/src/palettes/light.ts (single source)
packages/theme/src/palettes/dark.ts (single source)
packages/theme/src/generators/mui.ts (web overrides)
packages/theme/src/generators/css-vars.ts (web overrides)
packages/theme/src/generators/tamagui.ts (uses base directly)
Benefits:
- Single source of truth for most colors
- Platform-specific overrides are explicit and documented
- Changes to shared colors propagate automatically
- Type-safe with shared TypeScript interfaces
How to Modify Colors
Changing Shared Colors (Affects Both Platforms)
Edit the unified palettes:
// packages/theme/src/palettes/light.ts
const lightPalette: ColorPalette = {
primary: { main: '#NEW_COLOR' }, // Changes both web and mobile
}
Then regenerate CSS vars for web:
yarn workspace @safe-global/web css-vars
Changing Web-Only Colors
Edit the platform overrides in generators:
// packages/theme/src/generators/mui.ts
export function generateMuiTheme(mode: PaletteMode): Theme {
return createTheme({
palette: {
...colors,
info: { main: '#NEW_WEB_COLOR' }, // Only affects web
},
})
}
// packages/theme/src/generators/css-vars.ts
export function generateCSSVars(): string {
const webLightPalette: ColorPalette = {
...lightPalette,
info: { main: '#NEW_WEB_COLOR' }, // Only affects web CSS vars
}
}
Changing Mobile-Only Colors
Edit the base palette (since mobile uses it directly):
// packages/theme/src/palettes/light.ts
const lightPalette: ColorPalette = {
info: { main: '#NEW_MOBILE_COLOR' }, // Changes mobile
}
Then add web override to keep web unchanged:
// packages/theme/src/generators/mui.ts
info: { main: '#OLD_WEB_COLOR' }, // Preserve web color
Usage in Applications
Web (MUI)
// apps/web/src/components/theme/safeTheme.ts
import { generateMuiTheme } from '@safe-global/theme'
export const lightTheme = generateMuiTheme('light')
export const darkTheme = generateMuiTheme('dark')
Components use theme via MUI's theme system:
import { useTheme } from '@mui/material'
function MyComponent() {
const theme = useTheme()
return <Box bgcolor={theme.palette.info.main}>...</Box>
}
Or via CSS variables:
.my-element {
color: var(--color-info-main);
background: var(--color-info-background);
}
Mobile (Tamagui)
// apps/mobile/src/theme/tamagui.config.ts
import { generateTamaguiTokens, generateTamaguiThemes } from '@safe-global/theme'
const tokens = generateTamaguiTokens()
const themes = generateTamaguiThemes()
export default createTamagui({
tokens,
themes,
// ...
})
Components use theme via Tamagui:
import { YStack } from 'tamagui'
function MyComponent() {
return <YStack backgroundColor="$successMainLight">...</YStack>
}
Testing
Run tests for the theme package:
yarn workspace @safe-global/theme test
Regenerate CSS variables after palette changes:
yarn workspace @safe-global/web css-vars
Run type checking:
yarn workspace @safe-global/theme type-check
Summary
The unified theme system provides:
- ✅ Single source of truth for design tokens
- ✅ Platform-specific generators for MUI, Tamagui, and CSS variables
- ✅ Explicit color overrides for web's different tints
- ✅ Type safety across the entire system
- ✅ Automatic propagation of shared color changes
- ✅ Clear documentation of platform divergences
This architecture balances code reuse with platform-specific design requirements.