Skip to main content

Quick Start

This tutorial will guide you through creating a simple counter application with Freya. You’ll learn the fundamentals of components, state management, and event handling.
Make sure you’ve completed the installation guide before starting this tutorial.

What We’re Building

We’ll create an interactive counter app with:
  • A display showing the current count
  • Buttons to increase and decrease the count
  • Styled UI elements with colors, shadows, and spacing
Counter App Screenshot

Step 1: Create a New Project

1

Create the project

cargo new counter-app
cd counter-app
2

Add Freya dependency

Edit Cargo.toml and add Freya:
[package]
name = "counter-app"
version = "0.1.0"
edition = "2021"

[dependencies]
freya = "0.4.0-rc.11"

Step 2: Set Up the Basic App

Replace the contents of src/main.rs with this code:
#![cfg_attr(
    all(not(debug_assertions), target_os = "windows"),
    windows_subsystem = "windows"
)]

use freya::prelude::*;

fn main() {
    launch(LaunchConfig::new().with_window(WindowConfig::new(app)))
}

fn app() -> impl IntoElement {
    rect()
        .center()
        .expanded()
        .child("Hello, Freya!")
}
  • #![cfg_attr(...)] - On Windows release builds, prevents console window from appearing
  • use freya::prelude::* - Imports all common Freya types and functions
  • launch() - Starts the Freya runtime with configuration
  • WindowConfig::new(app) - Creates a window with our app function as the root component
  • rect() - Creates a rectangular container element
  • .expanded() - Makes the rect fill available space
  • .center() - Centers child content both horizontally and vertically
  • .child() - Adds a child element (in this case, text)
Run your app to verify it works:
cargo run
You should see a window with “Hello, Freya!” centered on the screen.

Step 3: Add State

Now let’s add state to track the counter value. Freya uses hooks for state management. The use_state hook creates reactive state that automatically triggers re-renders when changed. Update your app function:
fn app() -> impl IntoElement {
    let mut count = use_state(|| 4);

    rect()
        .center()
        .expanded()
        .child(count.read().to_string())
}
  • use_state(|| 4) - Creates state initialized to 4. The closure is only called once.
  • count.read() - Reads the current value. This subscribes the component to updates.
  • count.write() - Gets a mutable reference to update the value (we’ll use this next)
  • When state changes, Freya automatically re-renders the component
Run the app - you should now see “4” displayed.

Step 4: Create the Counter Display

Let’s make the counter display more visually appealing with styling:
fn app() -> impl IntoElement {
    let mut count = use_state(|| 4);

    let counter = rect()
        .width(Size::fill())
        .height(Size::percent(50.))
        .center()
        .color((255, 255, 255))
        .background((15, 163, 242))
        .font_weight(FontWeight::BOLD)
        .font_size(75.)
        .shadow((0., 4., 20., 4., (0, 0, 0, 80)))
        .child(count.read().to_string());

    rect().child(counter)
}
  • width(Size::fill()) - Fill available width
  • height(Size::percent(50.)) - Take up 50% of parent height
  • color((255, 255, 255)) - White text color (RGB)
  • background((15, 163, 242)) - Blue background
  • font_weight(FontWeight::BOLD) - Bold text
  • font_size(75.) - Large text size
  • shadow((x, y, blur, spread, color)) - Drop shadow effect

Step 5: Add Interactive Buttons

Now let’s add buttons to modify the counter:
fn app() -> impl IntoElement {
    let mut count = use_state(|| 4);

    let counter = rect()
        .width(Size::fill())
        .height(Size::percent(50.))
        .center()
        .color((255, 255, 255))
        .background((15, 163, 242))
        .font_weight(FontWeight::BOLD)
        .font_size(75.)
        .shadow((0., 4., 20., 4., (0, 0, 0, 80)))
        .child(count.read().to_string());

    let actions = rect()
        .horizontal()
        .width(Size::fill())
        .height(Size::percent(50.))
        .center()
        .spacing(8.0)
        .child(
            Button::new()
                .on_press(move |_| {
                    *count.write() += 1;
                })
                .child("Increase"),
        )
        .child(
            Button::new()
                .on_press(move |_| {
                    *count.write() -= 1;
                })
                .child("Decrease"),
        );

    rect().child(counter).child(actions)
}
  • .horizontal() - Arranges children in a horizontal row
  • .spacing(8.0) - Adds 8 pixels of space between children
  • Button::new() - Creates a button component
  • .on_press(move |_| { ... }) - Event handler called when button is clicked
  • *count.write() += 1 - Gets mutable access to state and modifies it
  • The move keyword captures count so it can be used in the closure
  • Both buttons can use the same count state because State implements Copy

Step 6: Final Touches

Let’s add window configuration for a better initial size:
fn main() {
    launch(
        LaunchConfig::new()
            .with_window(
                WindowConfig::new(app)
                    .with_size(500., 450.)
            )
    )
}
Here’s the complete final code:
#![cfg_attr(
    all(not(debug_assertions), target_os = "windows"),
    windows_subsystem = "windows"
)]

use freya::prelude::*;

fn main() {
    launch(
        LaunchConfig::new()
            .with_window(
                WindowConfig::new(app)
                    .with_size(500., 450.)
            )
    )
}

fn app() -> impl IntoElement {
    let mut count = use_state(|| 4);

    let counter = rect()
        .width(Size::fill())
        .height(Size::percent(50.))
        .center()
        .color((255, 255, 255))
        .background((15, 163, 242))
        .font_weight(FontWeight::BOLD)
        .font_size(75.)
        .shadow((0., 4., 20., 4., (0, 0, 0, 80)))
        .child(count.read().to_string());

    let actions = rect()
        .horizontal()
        .width(Size::fill())
        .height(Size::percent(50.))
        .center()
        .spacing(8.0)
        .child(
            Button::new()
                .on_press(move |_| {
                    *count.write() += 1;
                })
                .child("Increase"),
        )
        .child(
            Button::new()
                .on_press(move |_| {
                    *count.write() -= 1;
                })
                .child("Decrease"),
        );

    rect().child(counter).child(actions)
}

Run Your App

cargo run
Congratulations! You’ve built your first Freya application. Click the buttons to increment and decrement the counter.

Key Concepts Learned

Components

Functions that return UI elements. The app function is your root component.

State Management

use_state creates reactive state that triggers re-renders on changes.

Layout

Use rect() with properties like .horizontal(), .center(), and sizing to arrange UI.

Event Handling

Use .on_press(), .on_mouse_up(), etc. to respond to user interactions.

Next Steps

Enhance Your Counter

Try these modifications to deepen your understanding:
.child(
    Button::new()
        .on_press(move |_| {
            *count.write() = 0;
        })
        .child("Reset"),
)
Button::new()
    .on_press(move |_| {
        let current = *count.read();
        if current > 0 {
            *count.write() -= 1;
        }
    })
    .child("Decrease")
let bg_color = if *count.read() > 0 {
    (15, 163, 242)
} else {
    (242, 15, 15)
};

rect()
    // ... other properties
    .background(bg_color)
#[derive(PartialEq)]
struct CounterButton {
    label: &'static str,
    on_click: fn(&mut i32),
}

impl Component for CounterButton {
    fn render(&self) -> impl IntoElement {
        let mut count = use_consume::<State<i32>>();
        
        Button::new()
            .on_press(move |_| (self.on_click)(&mut count.write()))
            .child(self.label)
    }
}

Explore More

Component Examples

Browse examples of all built-in components like switches, sliders, and dropdowns

Animation Guide

Learn how to add smooth animations to your UI

State Management

Deep dive into local and global state patterns

API Reference

Complete documentation of all Freya APIs

Common Patterns

Using Multiple State Values

fn app() -> impl IntoElement {
    let mut count = use_state(|| 0);
    let mut step = use_state(|| 1);

    rect()
        .child(format!("Count: {}", count.read()))
        .child(
            Button::new()
                .on_press(move |_| {
                    *count.write() += *step.read();
                })
                .child("Add")
        )
}

Conditional Rendering

fn app() -> impl IntoElement {
    let mut show = use_state(|| true);

    rect()
        .child(
            Button::new()
                .on_press(move |_| *show.write() = !*show.read())
                .child("Toggle")
        )
        .child(
            if *show.read() {
                "Visible!".into_element()
            } else {
                "Hidden!".into_element()
            }
        )
}

Lists and Iteration

fn app() -> impl IntoElement {
    let items = vec!["Apple", "Banana", "Cherry"];

    rect()
        .children(
            items.iter().map(|item| {
                label().text(*item).into()
            })
        )
}

Troubleshooting

Make sure you’re calling .read() on your state. Components only re-render when they subscribe to state by reading it.
Add move keyword to your closure and ensure the state type is Copy. Freya’s State type is Copy by default.
Make sure you have the #![cfg_attr(...)] attribute at the top of your main.rs file.

Get Help

If you get stuck: Happy building with Freya! 🦀

Build docs developers (and LLMs) love