Theming
A good theme makes your application feel cohesive and professional. In this tutorial, we’ll explore how to create and manage themes in Freya.
[!NOTE] What is Theming? Theming is the process of defining consistent visual properties (colors, spacing, typography) across your application. A good theme ensures every component looks like it belongs together.
Built-in Themes
Freya includes a powerful built-in theming system with pre-defined light and dark themes.
Using Built-in Themes
Set the theme for your entire application using use_init_root_theme:
use freya::prelude::*;
fn app() -> impl IntoElement {
use_init_root_theme(|| DARK_THEME);
rect()
.background("surface_tertiary")
.child(label().text("Hello, World!"))
}
Available Themes
| Theme | Description |
|---|---|
LIGHT_THEME | Light background with dark text |
DARK_THEME | Dark background with light text |
BASE_THEME | Base theme for creating custom themes |
Theme Switching
fn app() -> impl IntoElement {
let is_dark = use_state(|| false);
let theme = use_theme();
rect()
.child(
Button::new()
.on_press(move |_| {
is_dark.toggle();
*theme.write() = if is_dark() { DARK_THEME } else { LIGHT_THEME };
})
.child("Toggle Theme")
)
}
Color Sheet
Themes define a comprehensive color palette organized into categories. You can reference these colors by name using strings.
Using Theme Colors
rect()
.background("surface_primary") // Reference by name
.color("text_primary")
.border(Border::new().fill("border"))
Brand Colors
| Color | Description |
|---|---|
primary | Main brand color |
secondary | Secondary brand color |
tertiary | Tertiary brand color |
Status Colors
| Color | Description |
|---|---|
success | Success states (green) |
warning | Warning states (yellow) |
error | Error states (red) |
info | Info states (blue) |
Surface Colors
| Color | Description |
|---|---|
background | Main background |
surface_primary | Primary surface |
surface_secondary | Secondary surface |
surface_tertiary | Tertiary surface |
surface_inverse | Inverse surface |
surface_inverse_secondary | Secondary inverse |
surface_inverse_tertiary | Tertiary inverse |
Border Colors
| Color | Description |
|---|---|
border | Default border |
border_focus | Focused border |
border_disabled | Disabled border |
Text Colors
| Color | Description |
|---|---|
text_primary | Primary text |
text_secondary | Secondary text |
text_placeholder | Placeholder text |
text_inverse | Inverse text |
text_highlight | Highlighted text |
State Colors
| Color | Description |
|---|---|
hover | Hover state |
focus | Focus state |
active | Active state |
disabled | Disabled state |
Utility Colors
| Color | Description |
|---|---|
overlay | Modal overlays |
shadow | Shadows |
Theme Management
Accessing Theme
Read the current theme in any component:
fn themed_component() -> impl IntoElement {
let theme = use_theme();
rect()
.background(theme.read().colors.background)
.child(label()
.text("Current theme")
.color(theme.read().colors.text_primary)
)
}
Nested Themes
Override the theme for a subtree:
fn app() -> impl IntoElement {
use_init_root_theme(|| LIGHT_THEME);
rect()
.child(label().text("Light theme"))
.child(
rect()
.with(|| use_init_theme(|| DARK_THEME))
.child(label().text("Dark theme"))
)
}
Theme Preferences
Themes use a preference system for colors:
// Specific color
Preference::Specific(Color::BLUE)
// Reference to color sheet
Preference::Reference("primary")
This allows themes to reference colors from the color sheet, making it easy to change all components by modifying the color sheet.
Surface Theme Indicator
For components that need to adapt based on their background:
use_init_surface_theme_indicator(|| SurfaceThemeIndicator::Opposite);
// Children can check:
let surface = use_surface_theme_indicator();
match surface {
SurfaceThemeIndicator::Primary => { /* on primary surface */ }
SurfaceThemeIndicator::Opposite => { /* on opposite surface */ }
}
Custom Themes
Creating a Custom Theme
Create your own theme by building on BASE_THEME:
const MY_THEME: Theme = Theme {
name: "my-theme",
colors: ColorsSheet {
primary: Color::from_rgb(0, 120, 200),
secondary: Color::from_rgb(100, 180, 255),
tertiary: Color::from_rgb(0, 80, 150),
success: Color::from_rgb(34, 197, 94),
warning: Color::from_rgb(234, 179, 8),
error: Color::from_rgb(239, 68, 68),
info: Color::from_rgb(59, 130, 246),
background: Color::from_rgb(255, 255, 255),
surface_primary: Color::from_rgb(249, 250, 251),
surface_secondary: Color::from_rgb(243, 244, 246),
surface_tertiary: Color::from_rgb(229, 231, 235),
text_primary: Color::from_rgb(17, 24, 39),
text_secondary: Color::from_rgb(107, 114, 128),
border: Color::from_rgb(229, 231, 235),
// ... other colors
..ColorsSheet::default()
},
// Component themes inherit from BASE_THEME
..BASE_THEME
};
Using Your Custom Theme
fn app() -> impl IntoElement {
use_init_root_theme(|| MY_THEME);
rect()
.background("background")
.child(label().text("Custom themed app!"))
}
Custom Design Tokens (Alternative Approach)
Instead of using the built-in theme system, you can also create your own design token system using Freya’s context. This gives you more control but requires more setup.
Design tokens are the building blocks of your theme - named values for colors, spacing, typography, etc.
Defining Color Tokens
struct ColorTokens {
// Brand colors
primary: Color,
secondary: Color,
accent: Color,
// Semantic colors
success: Color,
warning: Color,
error: Color,
info: Color,
// Neutral colors
background: Color,
surface: Color,
border: Color,
text: Color,
text_secondary: Color,
}
Light Theme Tokens
impl Default for ColorTokens {
fn default() -> Self {
Self {
// Brand
primary: Color::from_rgb(79, 70, 229), // Indigo
secondary: Color::from_rgb(107, 114, 128), // Gray
accent: Color::from_rgb(236, 72, 153), // Pink
// Semantic
success: Color::from_rgb(34, 197, 94), // Green
warning: Color::from_rgb(234, 179, 8), // Yellow
error: Color::from_rgb(239, 68, 68), // Red
info: Color::from_rgb(59, 130, 246), // Blue
// Neutral
background: Color::from_rgb(255, 255, 255), // White
surface: Color::from_rgb(249, 250, 251), // Light gray
border: Color::from_rgb(229, 231, 235), // Border gray
text: Color::from_rgb(17, 24, 39), // Near black
text_secondary: Color::from_rgb(107, 114, 128), // Gray
}
}
}
Dark Theme Tokens
fn dark_colors() -> ColorTokens {
ColorTokens {
// Brand (adjusted for dark mode)
primary: Color::from_rgb(129, 140, 248), // Lighter indigo
secondary: Color::from_rgb(156, 163, 175), // Lighter gray
accent: Color::from_rgb(244, 114, 182), // Lighter pink
// Semantic (adjusted for dark mode)
success: Color::from_rgb(74, 222, 128), // Lighter green
warning: Color::from_rgb(250, 204, 21), // Lighter yellow
error: Color::from_rgb(248, 113, 113), // Lighter red
info: Color::from_rgb(96, 165, 250), // Lighter blue
// Neutral (inverted)
background: Color::from_rgb(17, 24, 39), // Near black
surface: Color::from_rgb(31, 41, 55), // Dark gray
border: Color::from_rgb(55, 65, 81), // Border dark
text: Color::from_rgb(249, 250, 251), // Near white
text_secondary: Color::from_rgb(156, 163, 175), // Light gray
}
}
Custom Theme Context
If you prefer more control, you can share a custom theme across your application using context:
Define Theme Type
#[derive(Clone, PartialEq)]
struct Theme {
colors: ColorTokens,
mode: ThemeMode,
}
#[derive(Clone, PartialEq, Copy)]
enum ThemeMode {
Light,
Dark,
}
Provide Theme at Root
fn app() -> impl IntoElement {
let theme = use_state(|| Theme::default());
use_provide_context(|| theme.clone());
rect()
.expanded()
.background(theme.colors.background)
.color(theme.colors.text)
.child(NavBar {})
.child(MainContent {})
}
Consume Theme in Components
fn themed_button() -> impl IntoElement {
let theme = use_consume::<State<Theme>>();
rect()
.background(theme.colors.primary)
.corner_radius(8.0)
.padding_horizontal(16.0)
.padding_vertical(8.0)
.child(label()
.text("Themed Button")
.color(Color::WHITE)
)
}
Custom Light/Dark Mode
For the custom theme approach, here’s how to implement light/dark mode switching:
Toggle Implementation
fn theme_toggle() -> impl IntoElement {
let theme = use_consume::<State<Theme>>();
rect()
.on_pointer_down(move |_| {
theme.with_mut(|t| {
t.mode = match t.mode {
ThemeMode::Light => ThemeMode::Dark,
ThemeMode::Dark => ThemeMode::Light,
};
t.colors = match t.mode {
ThemeMode::Light => ColorTokens::default(),
ThemeMode::Dark => dark_colors(),
};
});
})
.child(
rect()
.width(Size::px(50.0))
.height(Size::px(26.0))
.background(theme.colors.surface)
.corner_radius(13.0)
.border(Border::new()
.width(1.0)
.fill(theme.colors.border)
)
.child(
rect()
.position(Position::Absolute)
.left(Size::px(if theme.mode == ThemeMode::Light { 2.0 } else { 24.0 }))
.width(Size::px(22.0))
.height(Size::px(22.0))
.background(theme.colors.primary)
.corner_radius(11.0)
)
)
}
Theme-Aware Components
fn card() -> impl IntoElement {
let theme = use_consume::<State<Theme>>();
rect()
.background(theme.colors.surface)
.border(Border::new()
.width(1.0)
.fill(theme.colors.border)
)
.corner_radius(12.0)
.padding(16.0)
.child(
label()
.text("Card Content")
.color(theme.colors.text)
)
}
Component Theming
Freya components support custom themes through theme partials.
Button Theming
Button::new()
.theme_colors(ButtonColorsThemePartial {
background: Color::from_rgb(79, 70, 229),
color: Color::WHITE,
hover_background: Color::from_rgb(67, 56, 202),
pressed_background: Color::from_rgb(55, 48, 163),
..Default::default()
})
.theme_layout(ButtonLayoutThemePartial {
padding: 16.0,
corner_radius: 8.0,
..Default::default()
})
Input Theming
Input::new(value)
.theme_colors(InputColorsThemePartial {
background: Color::WHITE,
border: Color::from_rgb(200, 200, 200),
focused_border: Color::from_rgb(79, 70, 229),
text: Color::BLACK,
placeholder: Color::GRAY,
..Default::default()
})
Switch Theming
Switch::new()
.theme(SwitchTheme {
active_background: Color::from_rgb(79, 70, 229),
inactive_background: Color::from_rgb(200, 200, 200),
thumb: Color::WHITE,
..Default::default()
})
Semantic Colors
Use semantic color names that describe purpose, not appearance.
Define Semantic Colors
struct SemanticColors {
// Backgrounds
background_primary: Color,
background_secondary: Color,
background_elevated: Color,
// Text
text_primary: Color,
text_secondary: Color,
text_disabled: Color,
// Interactive
interactive_primary: Color,
interactive_hover: Color,
interactive_pressed: Color,
// Status
status_success: Color,
status_warning: Color,
status_error: Color,
status_info: Color,
}
Usage
// Good: Semantic name
label().color(colors.text_secondary)
// Bad: Appearance-based name
label().color(colors.gray)
[!TIP] Why Semantic Colors? Semantic names let you change the actual color values without updating every component. “text_secondary” can be gray in light mode and light gray in dark mode.
Spacing System
Consistent spacing creates visual harmony.
Spacing Scale
struct SpacingTokens {
xs: f32, // 4px
sm: f32, // 8px
md: f32, // 16px
lg: f32, // 24px
xl: f32, // 32px
xxl: f32, // 48px
}
impl Default for SpacingTokens {
fn default() -> Self {
Self {
xs: 4.0,
sm: 8.0,
md: 16.0,
lg: 24.0,
xl: 32.0,
xxl: 48.0,
}
}
}
Usage
rect()
.padding(spacing.md)
.gap(spacing.sm)
.margin_vertical(spacing.lg)
Typography System
Consistent typography improves readability.
Typography Tokens
struct TypographyTokens {
// Font sizes
font_xs: f32,
font_sm: f32,
font_md: f32,
font_lg: f32,
font_xl: f32,
font_2xl: f32,
font_3xl: f32,
// Font weights
weight_normal: FontWeight,
weight_medium: FontWeight,
weight_semibold: FontWeight,
weight_bold: FontWeight,
// Line heights
leading_tight: f32,
leading_normal: f32,
leading_relaxed: f32,
}
Usage
// Heading
label()
.text("Welcome")
.font_size(typography.font_2xl)
.font_weight(typography.weight_bold)
// Body
label()
.text("Description")
.font_size(typography.font_md)
.font_weight(typography.weight_normal)
.line_height(typography.leading_relaxed)
// Caption
label()
.text("Helper text")
.font_size(typography.font_sm)
.color(colors.text_secondary)
Complete Theme Example
// Full theme structure
#[derive(Clone, PartialEq)]
struct Theme {
colors: ColorTokens,
spacing: SpacingTokens,
typography: TypographyTokens,
radii: RadiusTokens,
shadows: ShadowTokens,
}
// Usage in app
fn themed_app() -> impl IntoElement {
let theme = use_state(|| Theme::default());
rect()
.expanded()
.background(theme.colors.background)
.color(theme.colors.text)
.padding(theme.spacing.md)
.direction(Direction::Vertical)
.gap(theme.spacing.lg)
.child(
// Header
rect()
.padding(theme.spacing.md)
.background(theme.colors.surface)
.corner_radius(theme.radii.lg)
.shadow(theme.shadows.sm)
.child(
label()
.text("My App")
.font_size(theme.typography.font_xl)
.font_weight(theme.typography.weight_bold)
)
)
.child(
// Content card
rect()
.padding(theme.spacing.lg)
.background(theme.colors.surface)
.corner_radius(theme.radii.lg)
.shadow(theme.shadows.md)
.direction(Direction::Vertical)
.gap(theme.spacing.md)
.child(
label()
.text("Welcome!")
.font_size(theme.typography.font_2xl)
.font_weight(theme.typography.weight_bold)
)
.child(
label()
.text("This is a themed application.")
.font_size(theme.typography.font_md)
.color(theme.colors.text_secondary)
)
)
}
Best Practices
1. Use Semantic Names
// Good
colors.status_error
colors.text_primary
spacing.component_padding
// Bad
colors.red
colors.black
spacing.px16
2. Keep Tokens Consistent
Use the same scale for spacing, font sizes, etc. across your app.
3. Support Both Modes
Test your app in both light and dark modes. Ensure text is readable in both.
4. Provide Contrast
- Text should have at least 4.5:1 contrast ratio against backgrounds
- Interactive elements should be clearly visible
5. Document Your Tokens
Keep a reference of all your tokens so you use them consistently.
Summary
In this tutorial, you learned:
Built-in Theme System
- LIGHT_THEME / DARK_THEME - Pre-built themes ready to use
- use_init_root_theme - Setting the root theme
- use_theme - Accessing and modifying the current theme
- Color sheet - String-based color references (
"surface_primary","text_primary") - Nested themes - Overriding themes for subtrees
- Theme preferences - Preference::Specific vs Preference::Reference
Custom Theme Approach
- Design tokens - The building blocks of custom themes
- Color tokens - Brand, semantic, and neutral colors
- Light/Dark mode - Custom implementation with context
- Component theming - Customizing built-in components
- Semantic colors - Naming colors by purpose
- Spacing system - Consistent spacing scale
- Typography system - Consistent text styling
[!TIP] Which Approach? Use the built-in theme system for most applications - it’s simpler and integrates with all Freya components automatically. Use the custom design tokens approach when you need maximum control over your theming system.
Previous: Part 9: Animation ←
Next: Part 11: Accessibility →
In the next tutorial, we’ll explore accessibility - making your applications usable by everyone, including keyboard navigation and screen reader support.