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
| Variant | Description |
|---|---|
| Normal | Default button style |
| Filled | Solid background |
| Outline | Border-only style |
| Flat | No 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
| Variant | Description |
|---|---|
| Normal | Standard bordered input |
| Filled | Solid background |
| Flat | No 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))
}))
}
Dropdown
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::NaiveDatefor date representation. Make sure to addchronoto yourCargo.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())
Image Gallery Pattern
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:
- Screen reader support
- ARIA roles
- Focus indicators
- Keyboard navigation
Enabled/Disabled State
Button::new()
.enabled(false) // Disables the button
.child("Disabled")
Summary
In this tutorial, you learned about Freya’s built-in components:
- Button - Clickable buttons with multiple variants
- Input - Text input with validation
- Switch - Toggle switches
- Slider - Numeric sliders
- Checkbox - Checkboxes for selection
- RadioItem - Radio buttons for mutually exclusive options
- Tile - Interactive list items with leading/trailing elements
- ScrollView - Scrollable containers
- Card - Elevated containers
- Tooltip - Hover information
- ProgressBar - Progress indicators
- Loader - Loading spinners
- Chip - Compact tags
- Dropdown/Select - Dropdown menus
- Accordion - Collapsible sections
- Calendar - Date picker component
- ColorPicker - Color selection component
- ImageViewer - Display and zoom images
- Markdown - Render markdown content
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.