Part 4 of 14 18 Jan 2026 By Raj Patil 30 min read

Part 4: Styling - Making Your UI Beautiful

Part 4 of the Freya Rust GUI series. Learn to style your applications with colors, borders, corner radius, shadows, gradients, and typography to create polished, professional interfaces.

Beginner #rust #freya #gui #styling #colors #typography #css #tutorial
Building Native GUIs with Rust & Freya 4 / 14

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

PropertyEffectTypical Values
xHorizontal offset0 to 8
yVertical offset (positive = down)0 to 16
blurHow soft the shadow is0 to 40
spreadExpands or contracts shadowUsually 0
colorShadow color + transparencyBlack 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:

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:

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:


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.