Makepad-skills makepad-2.0-theme
git clone https://github.com/ZhangHanDong/makepad-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/ZhangHanDong/makepad-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/makepad-2.0-theme" ~/.claude/skills/zhanghandong-makepad-skills-makepad-2-0-theme && rm -rf "$T"
skills/makepad-2.0-theme/SKILL.mdMakepad 2.0 Theme System
Overview
The Makepad 2.0 theme system provides a comprehensive set of design tokens accessed through
theme.* variables in Splash scripts. It delivers consistent styling for
colors, typography, spacing, and widget states across your entire application.
Three built-in themes are available:
-- dark desktop theme (default)mod.themes.dark
-- light desktop thememod.themes.light
-- minimal skeleton theme with hardcoded valuesmod.themes.skeleton
Golden rule: Always use
theme.* variables instead of hardcoded values for any
color, font size, or spacing in production UIs. This ensures your app automatically
supports theme switching and maintains visual consistency.
Theme Setup in App::run
The theme must be loaded before widgets are loaded. The standard pattern is:
impl App { fn run(vm: &mut ScriptVm) -> Self { // Step 1: Load theme definitions (dark, light, skeleton) crate::makepad_widgets::theme_mod(vm); // Step 2: Select active theme (MUST come before widgets_mod) script_eval!(vm, { mod.theme = mod.themes.light // or mod.themes.dark }); // Step 3: Load widget definitions (they reference mod.theme) crate::makepad_widgets::widgets_mod(vm); // Step 4: Load your app's script_mod App::from_script_mod(vm, self::script_mod) } }
If you skip steps 1-2, the default theme is dark (set inside
theme_mod).
The counter example uses the simplified one-liner crate::makepad_widgets::script_mod(vm)
which bundles steps 1-3 with the default dark theme.
How It Works Internally
theme_mod() does the following:
- Calls
to load drawing primitivesmakepad_draw::script_mod(vm) - Creates the
modulemod.themes - Loads
,theme_desktop_dark
, andtheme_desktop_lighttheme_desktop_skeleton - Sets
as the defaultmod.theme = mod.themes.dark
Then
widgets_mod() creates mod.prelude.widgets_internal with theme: mod.theme,
making theme.* available in all widget scripts that use mod.prelude.widgets.*.
Theme Global Parameters
Each theme defines tunable global parameters that control the overall feel:
| Parameter | Purpose | Default (dark/light) |
|---|---|---|
| Controls color palette spread | 1.0 |
| Tint applied to backgrounds | |
| How much tint to apply (0-1) | 0.0 |
| Base spacing multiplier | 6.0 |
| Base corner radius | 2.5 |
| Bevel intensity | 0.75 |
| Base font size in px | 10.0 |
| Font size step between levels | 2.5 |
Theme Color Variables -- Primary
These are the colors you will use most often in application code:
| Variable | Purpose | Light Appearance | Dark Appearance |
|---|---|---|---|
| App background | Light gray (~#DDD) | Dark gray (~#333) |
| Foreground layer | Slightly darker | Slightly lighter |
| Card/container bg | Semi-transparent light | Semi-transparent dark |
| Alternating row (even) | Lighter | Darker |
| Alternating row (odd) | Darker | Lighter |
| Highlight background | White-ish | White-ish low opacity |
| Inline highlight | | |
| Unfocused highlight | 85% of bg_highlight | 85% of bg_highlight |
| Caption bar bg | Transparent | Transparent |
| Pure white | | |
| Makepad brand | | |
Theme Color Variables -- Text and Labels
| Variable | Purpose |
|---|---|
| Primary text on inner elements (buttons, labels) |
| Text on hover |
| Text when pressed |
| Text when focused |
| Text when active/selected |
| Secondary/muted text |
| Disabled text |
| Primary text on outer elements (tabs, headers) |
| Outer text when off |
| Disabled outer text |
| General text color |
| Text on hover |
| Text on focus |
| Disabled text |
| Placeholder text |
| Metadata text |
| Text cursor color |
Theme Color Variables -- Widget States
Outset colors (buttons, raised elements):
| Variable | Purpose |
|---|---|
| Default button background |
| Button on hover |
| Button when pressed |
| Active toggle state |
| Focused button |
| Disabled button |
| Inactive button |
Inset colors (text inputs, checkboxes, radio buttons):
| Variable | Purpose |
|---|---|
| Default input background |
| Input on hover |
| Input on focus |
| Disabled input |
| Empty input |
Selection and highlight:
| Variable | Purpose |
|---|---|
| Text selection highlight |
| Selection on hover |
| General accent/highlight |
| Cursor color |
| Focused cursor |
Theme Color Variables -- Semantic/Status
| Variable | Purpose | Value |
|---|---|---|
| Error state | Red () |
| Warning state | Orange () |
| High severity | Red () |
| Medium severity | Orange () |
| Low severity | Yellow-green () |
| Panic/critical | Magenta () |
Theme Color Variables -- Bevel System
The theme has a layered bevel system for 3D-like widget effects:
| Group | Variables | Purpose |
|---|---|---|
| , , , , | Flat bevel |
| Same suffixes | Inner shadow (layer 1) |
| Same suffixes | Inner highlight (layer 2) |
| Same suffixes | Outer highlight (layer 1) |
| Same suffixes | Outer shadow (layer 2) |
Theme Color Variables -- Additional Widget Colors
| Variable Group | Purpose |
|---|---|
| Icon colors (default, inactive, active, disabled) |
| Checkmark/radio mark colors |
| Progress bar and slider fill colors |
| Slider handle colors |
| Shadow effects |
| Drag preview overlay |
| Active dock tab background |
Theme Font Variables
Font Sizes
Font sizes are computed from
font_size_base and font_size_contrast:
| Variable | Formula | Default Value |
|---|---|---|
| base + 8 * contrast | 30.0 (largest heading) |
| base + 4 * contrast | 20.0 (medium heading) |
| base + 2 * contrast | 15.0 (small heading) |
| base + 1 * contrast | 12.5 (subheading) |
| base | 10.0 (body text) |
| fixed | 9.0 (monospace code) |
Font Styles (TextStyle objects)
| Variable | Description | Font File |
|---|---|---|
| Regular weight body text | IBMPlexSans-Text.ttf |
| Bold/semibold text | IBMPlexSans-SemiBold.ttf |
| Italic text | IBMPlexSans-Italic.ttf |
| Bold italic text | IBMPlexSans-BoldItalic.ttf |
| Monospace code font | LiberationMono-Regular.ttf |
| Label text (legacy) | IBMPlexSans-Text.ttf |
| Icon font (FontAwesome) | fa-solid-900.ttf |
Each font style includes multi-language support:
- Latin: IBM Plex Sans family
- Chinese: LXGW WenKai family
- Emoji: Noto Color Emoji
Line Spacing Constants
| Variable | Value | Purpose |
|---|---|---|
| 1.2 | Widget text |
| 1.05 | Heading text |
| 1.2 | Long-form text |
Theme Spacing Variables
Base Spacing
Spacing is derived from
space_factor (default 6.0):
| Variable | Formula | Default Value |
|---|---|---|
| 0.5 * space_factor | 3.0 (extra small) |
| 1.0 * space_factor | 6.0 (small/standard) |
| 1.5 * space_factor | 9.0 (medium) |
Margin/Padding Presets (Inset objects)
All-sides presets:
| Variable | Description | Values |
|---|---|---|
| XS padding all sides | 3px each |
| SM padding all sides | 6px each |
| MD padding all sides | 9px each |
Horizontal-only presets:
| Variable | Description | Values |
|---|---|---|
| XS horizontal padding | left/right: 3px, top/bottom: 0 |
| SM horizontal padding | left/right: 6px, top/bottom: 0 |
| MD horizontal padding | left/right: 9px, top/bottom: 0 |
Vertical-only presets:
| Variable | Description | Values |
|---|---|---|
| XS vertical padding | top/bottom: 3px, left/right: 0 |
| SM vertical padding | top/bottom: 6px, left/right: 0 |
| MD vertical padding | top/bottom: 9px, left/right: 0 |
Dimension Variables
| Variable | Purpose | Default |
|---|---|---|
| Standard data row height | ~23px |
| Standard icon width | ~16px |
| Standard icon height | ~22px |
| Container border radius | 5.0 |
| Text selection radius | 1.25 |
| Tab bar height | 36.0 |
Using Theme Variables in Splash
Colors
// Background color draw_bg.color: theme.color_bg_container // Text color draw_text.color: theme.color_label_inner // Muted/secondary text draw_text.color: theme.color_label_inner_inactive // Color math -- multiply for opacity draw_text.color: theme.color_label_inner_inactive * 0.8 // Semantic colors draw_bg.color: theme.color_warning draw_bg.color: theme.color_error
Font Sizes
// Body text size draw_text.text_style.font_size: theme.font_size_p // Heading sizes draw_text.text_style.font_size: theme.font_size_1 // Largest draw_text.text_style.font_size: theme.font_size_2 // Medium draw_text.text_style.font_size: theme.font_size_3 // Small draw_text.text_style.font_size: theme.font_size_4 // Sub-heading // Code font size draw_text.text_style.font_size: theme.font_size_code
Font Styles
// Bold text with custom size (override with {} syntax) draw_text.text_style: theme.font_bold{font_size: theme.font_size_2} // Bold text keeping default size draw_text.text_style: theme.font_bold{} // Code font draw_text.text_style: theme.font_code{} // Override font size using +: merge syntax draw_text +: {text_style +: {font_size: theme.font_size_3}}
Spacing
// Uniform padding padding: theme.mspace_2 // Padding with overrides padding: theme.mspace_2{left: theme.space_3, right: theme.space_3} // Horizontal-only padding with custom values padding: theme.mspace_h_1{left: theme.space_2, right: theme.space_2} // Spacing between children spacing: theme.space_2 // Computed spacing width: Fill height: 9. * theme.space_1 padding: theme.mspace_3{left: theme.space_3 * 2, right: theme.space_3 * 2}
Color Syntax Reference
Splash supports multiple color formats (not theme-specific but essential):
| Syntax | Example | Description |
|---|---|---|
| | Short hex (red) |
| | Full hex |
| | Hex with alpha |
| | Hex starting with (prefix ) |
| | Grayscale shorthand |
| | RGBA float (0.0-1.0) |
| | Theme variable |
Important: When a hex color starts with the letter
e, use the #x prefix
to avoid ambiguity with scientific notation. For example, #x2ecc71 not #2ecc71.
Color math is supported:
// Multiply for opacity theme.color_label_inner_inactive * 0.8 // Mix two colors mix(theme.color_w, theme.color_b, 0.5)
Theme Switching at Runtime
You can switch themes dynamically in Rust event handlers:
impl MatchEvent for App { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { if self.ui.button(cx, ids!(toggle_theme)).clicked(actions) { script_eval!(cx, { mod.theme = mod.themes.dark // or mod.themes.light }); // All widgets using theme.* will pick up new values // You may need to trigger a re-render } } }
Or switch before widgets load (in
App::run) for a static theme choice:
fn run(vm: &mut ScriptVm) -> Self { crate::makepad_widgets::theme_mod(vm); script_eval!(vm, { mod.theme = mod.themes.light }); crate::makepad_widgets::widgets_mod(vm); App::from_script_mod(vm, self::script_mod) }
Complete Theme-Aware UI Example
This example demonstrates a themed card list using only theme variables:
script_mod! { use mod.prelude.widgets.* let CardItem = RoundedView{ width: Fill height: Fit padding: theme.mspace_2{left: theme.space_3, right: theme.space_3} flow: Right spacing: theme.space_2 align: Align{y: 0.5} draw_bg.color: theme.color_bg_container draw_bg.border_radius: theme.container_corner_radius icon := Label{ width: 24 height: 24 text: "" } content := View{ width: Fill height: Fit flow: Down spacing: theme.space_1 title := Label{ text: "Title" draw_text.color: theme.color_label_inner draw_text.text_style: theme.font_bold{font_size: theme.font_size_4} } subtitle := Label{ text: "Subtitle" draw_text.color: theme.color_label_inner_inactive draw_text.text_style.font_size: theme.font_size_p } } badge := RoundedView{ width: Fit height: Fit padding: theme.mspace_h_1 draw_bg.color: theme.color_bg_highlight_inline draw_bg.border_radius: 4.0 badge_label := Label{ text: "new" draw_text.color: theme.color_highlight draw_text.text_style.font_size: theme.font_size_code draw_text.text_style: theme.font_bold{} } } } startup() do #(App::script_component(vm)){ ui: Root{ main_window := Window{ pass.clear_color: theme.color_bg_app window.inner_size: vec2(400, 600) body +: { width: Fill height: Fill flow: Down spacing: 0 // Header SolidView{ width: Fill height: Fit padding: theme.mspace_3 draw_bg.color: theme.color_app_caption_bar Label{ text: "My Cards" draw_text.color: theme.color_label_inner draw_text.text_style: theme.font_bold{font_size: theme.font_size_2} } } // Card list ScrollYView{ width: Fill height: Fill padding: theme.mspace_2 flow: Down spacing: theme.space_1 CardItem{title.text: "First Card" subtitle.text: "Description here"} CardItem{title.text: "Second Card" subtitle.text: "Another card"} } // Footer SolidView{ width: Fill height: Fit padding: theme.mspace_2 draw_bg.color: theme.color_bg_container Label{ text: "2 items" draw_text.color: theme.color_label_inner_inactive draw_text.text_style.font_size: theme.font_size_code } } } } } } }
Best Practices
-
ALWAYS use
for colors in production apps -- never hardcodetheme.*
when#ff0000
exists. This enables theme switching and accessibility.theme.color_error -
Use theme fonts for consistent typography -- prefer
over manually specifying font families.theme.font_bold{font_size: theme.font_size_2} -
Use theme spacing for consistent layout --
andtheme.mspace_2
create a harmonious rhythm. Avoid magic numbers liketheme.space_2
.padding: Inset{top: 8, ...} -
Override with
syntax -- extend theme values without replacing them:{}
keeps the bold font family but overrides size.theme.font_bold{font_size: 20}
keeps top/right/bottom but overrides left.theme.mspace_2{left: theme.space_3} -
Use merge
for partial overrides -- when you only want to change one nested property:+:draw_text +: {text_style +: {font_size: theme.font_size_3}} -
Choose the right text color variable:
for primary UI text (buttons, labels)theme.color_label_inner
for secondary/muted texttheme.color_label_inner_inactive
for general content texttheme.color_text
for placeholder texttheme.color_text_placeholder
-
Use state-aware color variants -- widgets that change on hover/focus/press should use the matching
,_hover
,_focus
suffixes._down -
Multiply for subtle opacity --
creates a subtler variant without a new variable.theme.color_label_inner_inactive * 0.8 -
Select theme before
-- the theme must be set betweenwidgets_mod
andtheme_mod()
calls, so widget definitions pick up the correct theme values.widgets_mod() -
For simple apps, use
which loads everything with the default dark theme in one call.crate::makepad_widgets::script_mod(vm)
Source Files
- Theme dark:
widgets/src/theme_desktop_dark.rs - Theme light:
widgets/src/theme_desktop_light.rs - Theme skeleton:
widgets/src/theme_desktop_skeleton.rs - Theme loader:
(widgets/src/lib.rs
andtheme_mod
functions)widgets_mod - Example usage:
(light theme with full theme variable usage)examples/todo/src/app.rs - Example usage:
(default dark theme)examples/counter/src/app.rs