Part 8 of 14 22 Jan 2026 By Raj Patil 35 min read

Part 8: Built-in Components - Ready-to-Use UI Elements

Part 8 of the Freya Rust GUI series. Explore Freya's comprehensive set of built-in components including Button, Input, Switch, Slider, ScrollView, Card, and more.

Beginner #rust #freya #gui #components #widgets #ui #tutorial
Building Native GUIs with Rust & Freya 8 / 14

Built-in Components

Freya provides a comprehensive set of built-in components that are accessible, themeable, and support keyboard navigation out of the box.

[!NOTE] Why Built-in Components? Built-in components save you time by providing common UI patterns with proper accessibility, keyboard navigation, and theming already implemented.


Button

Clickable button with multiple style variants.

Variants

VariantDescription
NormalDefault button style
FilledSolid background
OutlineBorder-only style
FlatNo visible background

Basic Usage

Button::new()
    .on_press(|_| println!("Pressed!"))
    .child("Click me")

Filled Button

Button::new()
    .filled()
    .on_press(|_| handle_submit())
    .child("Submit")

Outline Button

Button::new()
    .outline()
    .on_press(|_| handle_cancel())
    .child("Cancel")

Size Variants

// Smaller padding
Button::new().compact().child("Compact")

// Full width
Button::new().expanded().child("Expanded")

Disabled Button

Button::new()
    .enabled(false)
    .child("Disabled")

Secondary Press (Right-click)

Button::new()
    .on_secondary_press(|_| {
        println!("Right-clicked!");
    })
    .child("Right-click me")

Custom Theming

Button::new()
    .filled()
    .theme_colors(ButtonColorsThemePartial {
        background: Color::from_rgb(79, 70, 229),
        color: Color::WHITE,
        ..Default::default()
    })
    .child("Custom Theme")

Complete Button Example

fn button_demo() -> impl IntoElement {
    let count = use_state(|| 0);

    rect()
        .direction(Direction::Vertical)
        .gap(16.0)
        .padding(24.0)
        .child(
            label()
                .text(format!("Count: {}", count()))
                .font_size(24.0)
        )
        .child(
            rect()
                .direction(Direction::Horizontal)
                .gap(8.0)
                .child(
                    Button::new()
                        .outline()
                        .on_press(move |_| *count.write() -= 1)
                        .child("-")
                )
                .child(
                    Button::new()
                        .filled()
                        .on_press(move |_| *count.write() += 1)
                        .child("+")
                )
        )
        .child(
            Button::new()
                .expanded()
                .on_press(move |_| count.set(0))
                .child("Reset")
        )
}

Input

Text input field with validation support.

Style Variants

VariantDescription
NormalStandard bordered input
FilledSolid background
FlatNo border

Basic Usage

let name = use_state(String::new);

Input::new(name)
    .placeholder("Enter your name")

Password Input

let password = use_state(String::new);

Input::new(password)
    .mode(InputMode::new_password())
    .placeholder("Password")

With Validation

let email = use_state(String::new);

Input::new(email.clone())
    .placeholder("Email")
    .on_validate(move |validator: InputValidator| {
        let is_valid = validator.text().contains('@');
        validator.set_valid(is_valid);
    })

With Submit Handler

let search = use_state(String::new);

Input::new(search.clone())
    .placeholder("Search...")
    .on_submit(move |text| {
        perform_search(text);
    })

Auto Focus

Input::new(value)
    .placeholder("Auto-focused")
    .auto_focus(true)

Input Properties

Input::new(value)
    .placeholder("Enter text")
    .width(Size::px(300.0))
    .text_align(TextAlign::Start)
    .enabled(true)
    .compact()    // Smaller size

Complete Form Example

fn login_form() -> impl IntoElement {
    let username = use_state(String::new);
    let password = use_state(String::new);

    rect()
        .width(Size::px(300.0))
        .direction(Direction::Vertical)
        .gap(16.0)
        .child(
            label()
                .text("Login")
                .font_size(24.0)
                .font_weight(FontWeight::Bold)
        )
        .child(
            Input::new(username)
                .placeholder("Username")
                .expanded()
        )
        .child(
            Input::new(password)
                .placeholder("Password")
                .mode(InputMode::new_password())
                .expanded()
        )
        .child(
            Button::new()
                .filled()
                .expanded()
                .on_press(|_| {
                    // Handle login
                })
                .child("Sign In")
        )
}

Switch

Toggle switch for boolean values.

Basic Usage

let enabled = use_state(|| false);

rect()
    .direction(Direction::Horizontal)
    .gap(12.0)
    .child(Switch::new()
        .toggled(enabled())
        .on_toggle(move |_| enabled.toggle())
    )
    .child(label().text("Enable feature"))

With Custom Theme

Switch::new()
    .toggled(enabled())
    .on_toggle(move |_| enabled.toggle())
    .theme(SwitchTheme {
        active_background: Color::from_rgb(34, 197, 94),
        inactive_background: Color::GRAY,
        thumb: Color::WHITE,
        ..Default::default()
    })

Complete Settings Example

fn settings_panel() -> impl IntoElement {
    let notifications = use_state(|| true);
    let dark_mode = use_state(|| false);
    let auto_save = use_state(|| true);

    rect()
        .width(Size::px(400.0))
        .direction(Direction::Vertical)
        .gap(24.0)
        .padding(24.0)
        .background(Color::WHITE)
        .corner_radius(12.0)
        .child(
            label()
                .text("Settings")
                .font_size(20.0)
                .font_weight(FontWeight::Bold)
        )
        .child(setting_row("Notifications", notifications))
        .child(setting_row("Dark Mode", dark_mode))
        .child(setting_row("Auto Save", auto_save))
}

fn setting_row(label_text: &str, state: State<bool>) -> impl IntoElement {
    rect()
        .direction(Direction::Horizontal)
        .main_align_space_between()
        .child(label().text(label_text))
        .child(
            Switch::new()
                .toggled(state())
                .on_toggle(move |_| state.toggle())
        )
}

Slider

Slider for selecting numeric values.

Basic Usage

let volume = use_state(|| 50.0);

rect()
    .direction(Direction::Vertical)
    .gap(8.0)
    .child(label().text(format!("Volume: {:.0}%", volume())))
    .child(
        Slider::new(move |value| volume.set(value))
            .value(volume())
    )

Vertical Slider

Slider::new(move |value| volume.set(value))
    .value(volume())
    .direction(Direction::Vertical)
    .height(Size::px(200.0))

Size Control

Slider::new(move |value| volume.set(value))
    .value(volume())
    .size(Size::px(300.0))  // Horizontal: sets width

Checkbox

Checkbox for multi-select scenarios.

Basic Usage

let checked = use_state(|| false);

Checkbox::new()
    .selected(checked())

With Label

rect()
    .direction(Direction::Horizontal)
    .gap(8.0)
    .child(Checkbox::new().selected(checked()))
    .child(label().text("Accept terms and conditions"))

Checkbox List

fn todo_list() -> impl IntoElement {
    let todos = use_state(|| vec![
        ("Learn Freya", true),
        ("Build an app", false),
        ("Deploy", false),
    ]);

    rect()
        .direction(Direction::Vertical)
        .gap(8.0)
        .children(todos.read().iter().enumerate().map(|(i, (task, done))| {
            todo_item(i, task, *done, todos.clone())
        }))
}

fn todo_item(index: usize, task: &str, done: bool, todos: State<Vec<(&str, bool)>>) -> impl IntoElement {
    rect()
        .direction(Direction::Horizontal)
        .gap(8.0)
        .child(
            Checkbox::new()
                .selected(done)
                .on_select(move |_| {
                    todos.with_mut(|t| {
                        t[index].1 = !t[index].1;
                    });
                })
        )
        .child(label().text(task))
}

RadioItem

Radio buttons for mutually exclusive options - only one can be selected at a time.

Basic Usage

let selected = use_state(|| "option1");

rect()
    .direction(Direction::Vertical)
    .gap(8.0)
    .child(
        RadioItem::new()
            .selected(selected() == "option1")
            .on_select(move |_| selected.set("option1"))
            .child("Option 1")
    )
    .child(
        RadioItem::new()
            .selected(selected() == "option2")
            .on_select(move |_| selected.set("option2"))
            .child("Option 2")
    )
    .child(
        RadioItem::new()
            .selected(selected() == "option3")
            .on_select(move |_| selected.set("option3"))
            .child("Option 3")
    )

RadioGroup Pattern

fn radio_group() -> impl IntoElement {
    let selected_theme = use_state(|| "light");

    rect()
        .direction(Direction::Vertical)
        .gap(12.0)
        .child(label().text("Choose theme:").font_weight(FontWeight::Bold))
        .child(radio_option("Light", "light", selected_theme))
        .child(radio_option("Dark", "dark", selected_theme))
        .child(radio_option("System", "system", selected_theme))
}

fn radio_option(label_text: &str, value: &str, selected: State<&str>) -> impl IntoElement {
    let value_owned = value.to_string();
    RadioItem::new()
        .selected(selected() == value)
        .on_select(move |_| selected.set(value_owned.clone()))
        .child(label_text)
}

Tile

Interactive tile for lists, menus, and selections. Great for settings panels and list items.

Basic Usage

Tile::new()
    .on_select(|_| println!("Tile selected"))
    .child("Tile content")

With Leading and Trailing Elements

Tile::new()
    .on_select(move |_| toggle_setting())
    .leading(
        // Icon or avatar on the left
        rect()
            .width(Size::px(40.0))
            .height(Size::px(40.0))
            .corner_radius(20.0)
            .background(Color::BLUE)
    )
    .child(
        // Main content
        rect()
            .direction(Direction::Vertical)
            .child(label().text("Settings").font_weight(FontWeight::Bold))
            .child(label().text("Configure options").color(Color::GRAY))
    )
    .trailing(
        // Trailing element (like a badge or icon)
        Switch::new().toggled(enabled())
    )

Tile with Checkbox

fn selectable_tile() -> impl IntoElement {
    let checked = use_state(|| false);

    Tile::new()
        .on_select(move |_| checked.toggle())
        .leading(Checkbox::new().selected(checked()))
        .child("Accept terms and conditions")
}

Settings List Pattern

fn settings_list() -> impl IntoElement {
    rect()
        .direction(Direction::Vertical)
        .gap(4.0)
        .child(
            Tile::new()
                .leading(icon("account"))
                .child("Account")
                .trailing(chevron_icon())
        )
        .child(
            Tile::new()
                .leading(icon("notifications"))
                .child("Notifications")
                .trailing(chevron_icon())
        )
        .child(
            Tile::new()
                .leading(icon("privacy"))
                .child("Privacy")
                .trailing(chevron_icon())
        )
}

ScrollView

Scrollable container for content that exceeds viewport.

Basic Usage

ScrollView::new()
    .width(Size::fill())
    .height(Size::px(300.0))
    .child(/* large content */)

Horizontal Scroll

ScrollView::new()
    .direction(Direction::Horizontal)
    .width(Size::fill())
    .height(Size::px(200.0))
    .child(/* wide content */)

With Scroll Handle

fn scrollable_with_controls() -> impl IntoElement {
    let scroll = use_scroll();

    rect()
        .direction(Direction::Vertical)
        .gap(8.0)
        .child(
            ScrollView::new()
                .scroll_handle(scroll)
                .height(Size::px(300.0))
                .child(long_content())
        )
        .child(
            rect()
                .direction(Direction::Horizontal)
                .gap(8.0)
                .child(
                    Button::new()
                        .on_press(move |_| scroll.scroll_to(ScrollPosition::Top))
                        .child("Top")
                )
                .child(
                    Button::new()
                        .on_press(move |_| scroll.scroll_to(ScrollPosition::Bottom))
                        .child("Bottom")
                )
        )
}

Card

Container with elevated styling.

Basic Usage

Card::new()
    .child(
        rect()
            .padding(16.0)
            .direction(Direction::Vertical)
            .gap(8.0)
            .child(
                label()
                    .text("Card Title")
                    .font_weight(FontWeight::Bold)
            )
            .child(label().text("Card content goes here."))
    )

Card with Image

Card::new()
    .child(
        rect()
            .direction(Direction::Vertical)
            .child(
                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(
                rect()
                    .padding(16.0)
                    .direction(Direction::Vertical)
                    .gap(8.0)
                    .child(label().text("Featured Item"))
                    .child(label().text("Description text here."))
            )
    )

Tooltip

Hover tooltip for additional information.

Basic Usage

Tooltip::new()
    .text("Helpful tip")
    .child(Button::new().child("?"))

With Custom Content

Tooltip::new()
    .child(Button::new().child(Icon::new()))
    .popup(
        rect()
            .padding(8.0)
            .background(Color::from_rgb(30, 30, 30))
            .corner_radius(4.0)
            .child(label().text("Custom tooltip content"))
    )

ProgressBar

Visual progress indicator.

Basic Usage

let progress = use_state(|| 0.5);

ProgressBar::new()
    .progress(progress())

Dynamic Progress

fn download_progress() -> impl IntoElement {
    let progress = use_state(|| 0.0);

    // Simulate progress
    use_effect(move || {
        let progress = progress.clone();
        async move {
            for i in 0..=100 {
                sleep(Duration::from_millis(50)).await;
                progress.set(i as f64 / 100.0);
            }
        }
    });

    rect()
        .direction(Direction::Vertical)
        .gap(8.0)
        .child(
            label().text(format!("Downloading: {:.0}%", progress() * 100.0))
        )
        .child(
            ProgressBar::new()
                .progress(progress())
        )
}

Loader

Loading spinner.

Basic Usage

Loader::new()
    .size(Size::px(32.0))

Loading State Pattern

fn data_loader() -> impl IntoElement {
    let data = use_state(|| None::<Vec<String>>);

    // Fetch data
    use_effect(move || {
        let data = data.clone();
        async move {
            let result = fetch_data().await;
            data.set(Some(result));
        }
    });

    match data.peek() {
        Some(items) => show_list(items),
        None => rect()
            .expanded()
            .main_align_center()
            .cross_align_center()
            .child(Loader::new().size(Size::px(48.0))),
    }
}

Chip

Compact element representing an input, attribute, or action.

Basic Usage

Chip::new()
    .child("Tag")

With Close Button

Chip::new()
    .child("Removable Tag")
    .on_close(|_| remove_tag())

Tag List

fn tag_input() -> impl IntoElement {
    let tags = use_state(|| vec!["Rust".to_string(), "GUI".to_string()]);

    rect()
        .direction(Direction::Horizontal)
        .gap(8.0)
        .children(tags.read().iter().map(|tag| {
            Chip::new()
                .child(tag.clone())
                .on_close(|_| remove_tag(tag))
        }))
}

Expandable dropdown menu.

Basic Usage

Dropdown::new()
    .trigger(Button::new().child("Menu"))
    .child(
        rect()
            .direction(Direction::Vertical)
            .child(menu_item("Option 1"))
            .child(menu_item("Option 2"))
            .child(menu_item("Option 3"))
    )

Select

Dropdown select component.

Basic Usage

let selected = use_state(String::new);
let options = vec!["Option 1", "Option 2", "Option 3"];

Select::new()
    .selected(selected())
    .options(options)
    .on_select(move |value| selected.set(value))

Accordion

Collapsible content sections.

Basic Usage

Accordion::new()
    .item("Section 1", |content| {
        content.child(label().text("Content for section 1"))
    })
    .item("Section 2", |content| {
        content.child(label().text("Content for section 2"))
    })
    .item("Section 3", |content| {
        content.child(label().text("Content for section 3"))
    })

Calendar

Date picker component for selecting dates.

Basic Usage

let selected_date = use_state(|| None::<NaiveDate>);

Calendar::new()
    .on_select(move |date| {
        selected_date.set(Some(date));
    })

With Initial Selection

use chrono::NaiveDate;

let selected_date = use_state(|| {
    Some(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap())
});

Calendar::new()
    .value(selected_date())
    .on_select(move |date| selected_date.set(Some(date)))

Complete Date Picker Example

fn date_picker() -> impl IntoElement {
    let selected_date = use_state(|| None::<NaiveDate>);
    let show_calendar = use_state(|| false);

    rect()
        .direction(Direction::Vertical)
        .gap(8.0)
        .child(
            // Date display button
            Button::new()
                .on_press(move |_| show_calendar.toggle())
                .child(
                    match selected_date.peek() {
                        Some(date) => format!("{}", date.format("%B %d, %Y")),
                        None => "Select date".to_string(),
                    }
                )
        )
        .child(
            if show_calendar() {
                Calendar::new()
                    .value(selected_date.peek().clone())
                    .on_select(move |date| {
                        selected_date.set(Some(date));
                        show_calendar.set(false);
                    })
            } else {
                rect()
            }
        )
}

[!NOTE] Date Handling Calendar uses chrono::NaiveDate for date representation. Make sure to add chrono to your Cargo.toml.


ColorPicker

Color selection component for choosing colors.

Basic Usage

let color = use_state(|| Color::RED);

ColorPicker::new()
    .color(color())
    .on_change(move |new_color| color.set(new_color))

With Display

fn color_chooser() -> impl IntoElement {
    let selected_color = use_state(|| Color::from_rgb(79, 70, 229));

    rect()
        .direction(Direction::Vertical)
        .gap(16.0)
        .child(
            // Color preview
            rect()
                .width(Size::px(100.0))
                .height(Size::px(100.0))
                .corner_radius(8.0)
                .background(selected_color())
        )
        .child(
            // Picker
            ColorPicker::new()
                .color(selected_color())
                .on_change(move |c| selected_color.set(c))
        )
        .child(
            // Color value display
            label().text(format!("Selected: {:?}", selected_color()))
        )
}

Theme Customizer Example

fn theme_customizer() -> impl IntoElement {
    let primary_color = use_state(|| Color::BLUE);
    let background_color = use_state(|| Color::WHITE);

    rect()
        .direction(Direction::Vertical)
        .gap(16.0)
        .padding(16.0)
        .child(label().text("Theme Colors").font_weight(FontWeight::Bold))
        .child(
            rect()
                .direction(Direction::Horizontal)
                .gap(24.0)
                .child(
                    rect()
                        .direction(Direction::Vertical)
                        .gap(8.0)
                        .child(label().text("Primary"))
                        .child(
                            ColorPicker::new()
                                .color(primary_color())
                                .on_change(move |c| primary_color.set(c))
                        )
                )
                .child(
                    rect()
                        .direction(Direction::Vertical)
                        .gap(8.0)
                        .child(label().text("Background"))
                        .child(
                            ColorPicker::new()
                                .color(background_color())
                                .on_change(move |c| background_color.set(c))
                        )
                )
        )
        .child(
            // Preview
            rect()
                .background(background_color())
                .padding(16.0)
                .child(
                    Button::new()
                        .theme_colors(ButtonColorsThemePartial {
                            background: primary_color(),
                            ..Default::default()
                        })
                        .child("Preview Button")
                )
        )
}

ImageViewer

Display and interact with images, including zoom capabilities.

Basic Usage

ImageViewer::new()
    .image(image_data)
    .width(Size::px(400.0))
    .height(Size::px(300.0))

Zoomable Image

ImageViewer::new()
    .image(image_data)
    .zoomable(true)
    .width(Size::fill())
    .height(Size::fill())
fn image_gallery(images: Vec<ImageData>) -> impl IntoElement {
    let selected = use_state(|| None::<usize>);
    let show_viewer = use_state(|| false);

    rect()
        .direction(Direction::Vertical)
        .gap(16.0)
        .child(
            // Thumbnail grid
            rect()
                .direction(Direction::Horizontal)
                .gap(8.0)
                .children(images.iter().enumerate().map(|(i, img)| {
                    rect()
                        .width(Size::px(100.0))
                        .height(Size::px(100.0))
                        .corner_radius(8.0)
                        .overflow(Overflow::Clip)
                        .on_pointer_down(move |_| {
                            selected.set(Some(i));
                            show_viewer.set(true);
                        })
                        .child(
                            image()
                                .image_data(img.bytes)
                                .width(Size::fill())
                                .height(Size::fill())
                                .image_fill(ImageFill::Cover)
                        )
                }))
        )
        .child(
            // Full viewer modal
            if show_viewer() {
                rect()
                    .width(Size::fill())
                    .height(Size::px(400.0))
                    .child(
                        ImageViewer::new()
                            .image(images[selected() as usize].clone())
                            .zoomable(true)
                    )
            } else {
                rect()
            }
        )
}

Markdown

Render markdown content directly in your UI.

Basic Usage

let content = r#"
# Heading

This is **bold** and *italic* text.

- List item 1
- List item 2

```rust
fn main() {
    println!("Hello!");
}

”#;

Markdown::new() .content(content)


### Markdown Viewer Component

```rust
fn markdown_viewer() -> impl IntoElement {
    let markdown_text = use_state(|| String::new);

    rect()
        .direction(Direction::Vertical)
        .gap(16.0)
        .child(
            label()
                .text("Markdown Preview")
                .font_weight(FontWeight::Bold)
        )
        .child(
            ScrollView::new()
                .height(Size::px(400.0))
                .child(
                    Markdown::new()
                        .content(markdown_text())
                )
        )
}

### Use Cases

- Documentation viewers
- README displays
- Rich text content
- Blog post rendering
- Help text formatting

> [!TIP]
> **Markdown Support**
> Freya's Markdown component supports common markdown features including headings, bold, italic, lists, code blocks, and links.

---

## Common Component Features

### Focus Management

All interactive components support keyboard focus:
- Tab to navigate between focusable elements
- Enter/Space to activate
- Arrow keys for navigation within components

### Theming

All components support custom theming:

```rust
Button::new()
    .theme_colors(ButtonColorsThemePartial {
        background: Color::BLUE,
        color: Color::WHITE,
        ..Default::default()
    })
    .theme_layout(ButtonLayoutThemePartial {
        padding: 16.0,
        corner_radius: 8.0,
        ..Default::default()
    })

Accessibility

Components include built-in accessibility:

Enabled/Disabled State

Button::new()
    .enabled(false)  // Disables the button
    .child("Disabled")

Summary

In this tutorial, you learned about Freya’s built-in components:


Previous: Part 7: Event Handling ←

Next: Part 9: Animation →

In the next tutorial, we’ll bring your UIs to life with animations - smooth transitions, motion effects, and delightful interactions.