Styling
You’ve learned how to create elements and position them. Now let’s make them look great! In this tutorial, we’ll cover all the styling options Freya provides.
[!NOTE] Styling Philosophy Unlike web development with CSS files, Freya uses method chaining directly on elements. This keeps styles close to the elements they affect and makes it easy to see what’s happening.
Colors
Colors are fundamental to styling. Let’s explore all the ways to work with colors in Freya.
Creating Colors
RGB (Red, Green, Blue)
Each component is 0-255:
Color::from_rgb(255, 0, 0) // Red
Color::from_rgb(0, 255, 0) // Green
Color::from_rgb(0, 0, 255) // Blue
Color::from_rgb(255, 255, 0) // Yellow (Red + Green)
Color::from_rgb(0, 255, 255) // Cyan (Green + Blue)
Color::from_rgb(255, 0, 255) // Magenta (Red + Blue)
Color::from_rgb(255, 255, 255) // White (all colors)
Color::from_rgb(0, 0, 0) // Black (no color)
ARGB (With Alpha/Transparency)
The first parameter is alpha (0-255), where 0 is fully transparent and 255 is fully opaque:
Color::from_argb(255, 255, 0, 0) // Fully opaque red
Color::from_argb(128, 255, 0, 0) // Semi-transparent red
Color::from_argb(0, 255, 0, 0) // Fully transparent red (invisible)
Hex Color Codes
Color::from_hex("#ff0000")? // Red
Color::from_hex("#00ff00")? // Green
Color::from_hex("ff0000")? // Without # also works
Color::from_hex("#ff000080")? // With alpha (50% transparent red)
Using Tuples
let color: Color = (255, 0, 0).into(); // RGB
let color: Color = (255, 0, 0, 128).into(); // RGBA (alpha as u8)
let color: Color = (255, 0, 0, 0.5).into(); // RGBA (alpha as f32)
Predefined Colors
Freya includes convenient predefined colors:
Color::TRANSPARENT // Fully transparent
Color::BLACK // Black
Color::WHITE // White
Color::RED // Red
Color::GREEN // Green
Color::BLUE // Blue
Color::YELLOW // Yellow
Color::CYAN // Cyan
Color::MAGENTA // Magenta
Color::GRAY // Gray
Color::LIGHT_GRAY // Light gray
Color::DARK_GRAY // Dark gray
Color Manipulation
Getting Components
let color = Color::RED;
let r = color.r(); // Red channel (0-255)
let g = color.g(); // Green channel (0-255)
let b = color.b(); // Blue channel (0-255)
let a = color.a(); // Alpha channel (0-255)
Modifying Colors
let color = Color::RED;
// Change alpha (transparency)
let transparent = color.with_a(128); // 50% transparent
let semi_transparent = color.with_a(200); // ~20% transparent
// Change HSV components
let shifted = color.with_h(180.0); // Shift hue to 180°
let desaturated = color.with_s(0.5); // 50% saturation
let darker = color.with_v(0.5); // 50% value (brightness)
Blending Colors
let color = Color::WHITE;
// Darken by multiplying
let darker = color.mul(0.5); // 50% darker
// Conditional multiply
let maybe_darker = color.mul_if(true, 0.5); // Darken if true
Applying Colors
rect()
.background(Color::WHITE) // Background
.color(Color::BLACK) // Text color (inherited by children)
.border(Border::new().fill(Color::GRAY)) // Border color
.shadow(Shadow::new().color(Color::BLACK)) // Shadow color
[!TIP] The
.color()method.color()sets the text color and is inherited by all children. This is useful for setting a default text color on a container:rect() .color(Color::WHITE) // All text inside will be white .background(Color::BLACK) .child(label().text("White text")) // Inherits white color .child(label().text("Also white")) .child(label().color(Color::RED).text("Red exception")) // Override
Common Color Recipes
// Semi-transparent overlay
Color::BLACK.with_a(128) // 50% dark overlay
// Subtle shadow
Color::BLACK.with_a(30) // ~12% black for shadows
// Hover state (slightly darker)
Color::BLUE.mul(0.9) // 10% darker blue
// Pressed state (darker still)
Color::BLUE.mul(0.8) // 20% darker blue
// Disabled state (grayed out)
Color::GRAY
// Success green
Color::from_rgb(34, 197, 94)
// Error red
Color::from_rgb(239, 68, 68)
// Warning yellow
Color::from_rgb(234, 179, 8)
Borders
Borders add visual separation and can indicate focus or importance.
Basic Border
rect()
.border(
Border::new()
.fill(Color::BLACK) // Border color
.width(2.0) // Border thickness in pixels
)
Border Width Options
// Uniform width
Border::new().width(2.0)
// Individual side widths
Border::new().width(BorderWidth {
top: 1.0,
right: 2.0,
bottom: 1.0,
left: 2.0,
})
Border Alignment
Control where the border is drawn relative to the element’s box:
Border::new()
.alignment(BorderAlignment::Inner) // Inside the element (default)
.alignment(BorderAlignment::Outer) // Outside the element
.alignment(BorderAlignment::Center) // Centered on the edge
[!NOTE] Why Border Alignment Matters If you have a 100x100 element with a 4px inner border, your content area is 96x96. With an outer border, your content stays 100x100 but the element takes 104x104 total space.
Common Border Patterns
// Subtle separator
.border(Border::new().width(1.0).fill(Color::LIGHT_GRAY))
// Input focus ring
.border(Border::new().width(2.0).fill(Color::BLUE))
// Card outline
.border(Border::new().width(1.0).fill(Color::from_rgb(200, 200, 200)))
// Bottom border only (for list items)
.border(Border::new()
.width(BorderWidth { top: 0.0, right: 0.0, bottom: 1.0, left: 0.0 })
.fill(Color::LIGHT_GRAY)
)
Corner Radius (Rounded Corners)
Rounded corners soften the look of your UI and are a hallmark of modern design.
Uniform Radius
rect()
.corner_radius(8.0) // All corners rounded by 8px
Individual Corners
// Using CornerRadius struct
rect()
.corner_radius(CornerRadius {
top_left: 16.0,
top_right: 16.0,
bottom_right: 0.0,
bottom_left: 0.0,
smoothing: 0.0,
})
// Using tuple (TL, TR, BR, BL)
rect()
.corner_radius(16.0, 16.0, 0.0, 0.0)
Smooth Corners
The smoothing property creates iOS-style continuous curves:
rect()
.corner_radius(CornerRadius {
top_left: 20.0,
top_right: 20.0,
bottom_right: 20.0,
bottom_left: 20.0,
smoothing: 0.6, // 0.0 = circular, 1.0 = maximum smoothing
})
[!TIP] When to Use Smoothing Smoothing looks best for larger rounded corners (20px+). For small radii (4-8px), the difference is negligible.
Common Corner Radius Patterns
// Subtle rounding (buttons, inputs)
.corner_radius(6.0)
// Card rounding
.corner_radius(12.0)
// Pill shape (height/2 for perfect pill)
.height(Size::px(40.0))
.corner_radius(20.0)
// Circular avatar
.width(Size::px(48.0))
.height(Size::px(48.0))
.corner_radius(24.0) // Half of width/height
// Modal/dialog
.corner_radius(16.0)
// Bottom sheet (rounded top only)
.corner_radius(16.0, 16.0, 0.0, 0.0)
Shadows
Shadows add depth and help elements stand out from the background.
Basic Shadow
rect()
.shadow(
Shadow::new()
.color(Color::BLACK) // Shadow color
.x(0.0) // Horizontal offset
.y(4.0) // Vertical offset
.blur(10.0) // Blur radius
.spread(0.0) // Spread (expands shadow)
)
Shadow Properties Explained
| Property | Effect | Typical Values |
|---|---|---|
x | Horizontal offset | 0 to 8 |
y | Vertical offset (positive = down) | 0 to 16 |
blur | How soft the shadow is | 0 to 40 |
spread | Expands or contracts shadow | Usually 0 |
color | Shadow color + transparency | Black with low alpha |
Shadow Positions
// Normal (outset) shadow - default
Shadow::new().normal()
// Inset shadow (inside element)
Shadow::new().inset()
Inset shadows create an “inner” effect, useful for pressed states or sunken panels.
Multiple Shadows
You can stack shadows for more complex effects:
rect()
.shadow(Shadow::new()
.color(Color::BLACK.with_a(20))
.y(2.0)
.blur(4.0)
)
.shadow(Shadow::new()
.color(Color::BLACK.with_a(15))
.y(8.0)
.blur(16.0)
)
Tuple Shorthand
For quick shadows:
// (x, y, blur, spread, color)
rect()
.shadow((0.0, 4.0, 10.0, 0.0, Color::BLACK.with_a(50)))
Shadow Recipes
// Subtle lift
.shadow(Shadow::new()
.color(Color::BLACK.with_a(30))
.y(2.0)
.blur(4.0)
)
// Card shadow
.shadow(Shadow::new()
.color(Color::BLACK.with_a(40))
.y(4.0)
.blur(12.0)
)
// Elevated/popup shadow
.shadow(Shadow::new()
.color(Color::BLACK.with_a(50))
.y(8.0)
.blur(24.0)
)
// Pressed state (inset)
.shadow(Shadow::new()
.inset()
.color(Color::BLACK.with_a(30))
.y(2.0)
.blur(4.0)
)
Gradients
Gradients smoothly transition between colors.
Linear Gradients
use freya_core::prelude::Gradient;
rect()
.background(Gradient::linear(
GradientType::Horizontal, // Direction
vec![
GradientStop::new(0.0, Color::RED), // Start color
GradientStop::new(1.0, Color::BLUE), // End color
],
))
Gradient Types
GradientType::Horizontal // Left to right
GradientType::Vertical // Top to bottom
GradientType::Diagonal // Top-left to bottom-right
Multiple Stops
rect()
.background(Gradient::linear(
GradientType::Horizontal,
vec![
GradientStop::new(0.0, Color::RED), // Start
GradientStop::new(0.5, Color::YELLOW), // Middle
GradientStop::new(1.0, Color::GREEN), // End
],
))
The position values (0.0 to 1.0) indicate where each color appears along the gradient.
Common Gradient Recipes
// Button gradient
.background(Gradient::linear(
GradientType::Vertical,
vec![
GradientStop::new(0.0, Color::from_rgb(79, 70, 229)),
GradientStop::new(1.0, Color::from_rgb(67, 56, 202)),
],
))
// Sunset background
.background(Gradient::linear(
GradientType::Vertical,
vec![
GradientStop::new(0.0, Color::from_rgb(251, 146, 60)),
GradientStop::new(1.0, Color::from_rgb(234, 88, 12)),
],
))
// Subtle card gradient
.background(Gradient::linear(
GradientType::Vertical,
vec![
GradientStop::new(0.0, Color::WHITE),
GradientStop::new(1.0, Color::from_rgb(248, 250, 252)),
],
))
Typography
Typography - the art of text styling - is crucial for readable, professional UIs.
Font Size
label()
.font_size(16.0) // In points
Common sizes:
- 12px: Captions, helper text
- 14px: Body text (small)
- 16px: Body text (standard)
- 18px: Subheadings
- 20-24px: Headings
- 32-48px: Large titles
Font Weight
label()
.font_weight(FontWeight::Normal) // 400 - Default
.font_weight(FontWeight::Medium) // 500
.font_weight(FontWeight::SemiBold) // 600
.font_weight(FontWeight::Bold) // 700
Full range:
FontWeight::Thin // 100
FontWeight::ExtraLight // 200
FontWeight::Light // 300
FontWeight::Normal // 400
FontWeight::Medium // 500
FontWeight::SemiBold // 600
FontWeight::Bold // 700
FontWeight::ExtraBold // 800
FontWeight::Black // 900
Font Style
label()
.font_style(FontStyle::Normal) // Regular text
.font_style(FontStyle::Italic) // Italic text
Font Family
label()
.font_family("Inter") // Single font
.font_family("Inter, Arial") // With fallback
[!NOTE] Font Availability The font must be available on the system or embedded in your app. We’ll cover embedding fonts in Part 13: Window Configuration.
Line Height
Controls spacing between lines of text:
label()
.line_height(1.5) // 1.5x the font size
Standard values:
- 1.0: Tight (headings)
- 1.25: Normal (body)
- 1.5: Relaxed (readable body text)
- 2.0: Very loose
Text Alignment
label()
.text_align(TextAlign::Start) // Left (in LTR languages)
.text_align(TextAlign::Center) // Centered
.text_align(TextAlign::End) // Right (in LTR languages)
.text_align(TextAlign::Justify) // Full width
Text Decoration
label()
.text_decoration(TextDecoration::None) // No decoration
.text_decoration(TextDecoration::Underline) // Underlined
.text_decoration(TextDecoration::LineThrough) // Strikethrough
Text Overflow
Handle text that’s too long:
paragraph()
.text_overflow(TextOverflow::Clip) // Cut off
.text_overflow(TextOverflow::Ellipsis) // Add "..."
.max_lines(1) // Single line only
Text Shadow
Add shadows to text:
label()
.text_shadow(
TextShadow::new()
.color(Color::BLACK.with_a(100))
.x(1.0)
.y(1.0)
.blur(2.0)
)
Combining Styles: Real Examples
Styled Button
fn styled_button(text: &str) -> impl IntoElement {
rect()
.padding_horizontal(24.0)
.padding_vertical(12.0)
.background(Color::from_rgb(79, 70, 229))
.corner_radius(8.0)
.shadow(
Shadow::new()
.color(Color::from_rgb(79, 70, 229).with_a(100))
.y(4.0)
.blur(12.0)
)
.main_align_center()
.cross_align_center()
.child(
label()
.text(text)
.font_size(14.0)
.font_weight(FontWeight::SemiBold)
.color(Color::WHITE)
)
}
Input Field
fn styled_input(placeholder: &str) -> impl IntoElement {
rect()
.width(Size::px(300.0))
.height(Size::px(44.0))
.padding_horizontal(16.0)
.background(Color::WHITE)
.border(Border::new()
.width(1.0)
.fill(Color::from_rgb(200, 200, 200))
)
.corner_radius(8.0)
.main_align_center()
.child(
label()
.text(placeholder)
.font_size(14.0)
.color(Color::GRAY)
)
}
Card Component
fn card() -> impl IntoElement {
rect()
.width(Size::px(340.0))
.background(Color::WHITE)
.corner_radius(16.0)
.border(Border::new()
.width(1.0)
.fill(Color::from_rgb(230, 230, 230))
)
.shadow(
Shadow::new()
.color(Color::BLACK.with_a(40))
.y(4.0)
.blur(16.0)
)
.overflow(Overflow::Clip)
.child(
// Image placeholder
rect()
.height(Size::px(180.0))
.width(Size::fill())
.background(Gradient::linear(
GradientType::Diagonal,
vec![
GradientStop::new(0.0, Color::from_rgb(129, 140, 248)),
GradientStop::new(1.0, Color::from_rgb(79, 70, 229)),
],
))
)
.child(
// Content
rect()
.padding(20.0)
.direction(Direction::Vertical)
.gap(8.0)
.child(
label()
.text("Card Title")
.font_size(18.0)
.font_weight(FontWeight::Bold)
.color(Color::from_rgb(30, 30, 30))
)
.child(
label()
.text("This is a description of the card content. It provides additional context.")
.font_size(14.0)
.color(Color::GRAY)
.line_height(1.5)
)
)
}
Alert/Notification
fn alert() -> impl IntoElement {
rect()
.width(Size::px(400.0))
.padding(16.0)
.background(Color::from_rgb(236, 253, 245)) // Light green
.border(Border::new()
.width(1.0)
.fill(Color::from_rgb(34, 197, 94)) // Green border
)
.corner_radius(8.0)
.direction(Direction::Horizontal)
.gap(12.0)
.child(
// Icon placeholder
rect()
.width(Size::px(24.0))
.height(Size::px(24.0))
.corner_radius(12.0)
.background(Color::from_rgb(34, 197, 94))
)
.child(
rect()
.width(Size::fill())
.direction(Direction::Vertical)
.gap(4.0)
.child(
label()
.text("Success!")
.font_size(14.0)
.font_weight(FontWeight::SemiBold)
.color(Color::from_rgb(22, 101, 52))
)
.child(
label()
.text("Your changes have been saved successfully.")
.font_size(14.0)
.color(Color::from_rgb(34, 197, 94))
)
)
}
Visual Effects
Opacity
rect()
.opacity(0.5) // 50% transparent (0.0 = invisible, 1.0 = solid)
Rotation
rect()
.rotate(45.0) // Rotate 45 degrees
Scale
rect()
.scale(Scale::new(1.5)) // Scale to 150%
Blur
rect()
.blur(5.0) // Apply 5px blur to content
Summary
In this tutorial, you learned:
- Colors: RGB, ARGB, hex, predefined colors, and manipulation
- Borders: Width, fill, and alignment
- Corner radius: Uniform, individual, and smooth corners
- Shadows: Basic shadows, inset shadows, and multiple shadows
- Gradients: Linear gradients with multiple stops
- Typography: Font size, weight, style, family, line height, and alignment
- Text effects: Decoration, overflow, and shadows
- Visual effects: Opacity, rotation, scale, and blur
Previous: Part 3: Layout System ←
Next: Part 5: Hooks →
In the next tutorial, we’ll dive into hooks - the secret sauce that makes Freya UIs reactive and interactive.