freya

star 2.8k

Freya Rust GUI framework best practices, patterns, and conventions. Use when writing Freya components, hooks, elements, or working on a Freya project.

marc2332 By marc2332 schedule Updated 6/15/2026

name: freya description: Freya Rust GUI framework best practices, patterns, and conventions. Use when writing Freya components, hooks, elements, or working on a Freya project. user-invocable: true

Freya Best Practices

Freya is a cross-platform, native, declarative GUI library for Rust.

General rules:

  • Do not write unnecessary comments in code.
  • Do not use em dashes.
  • Be simple, elegant, and concise.

Start by asking the user what they would like to do:

  • Lint and improve existing code
  • Refactor existing code
  • Add a new feature

Components

Struct Components (for stateful UI)

#[derive(PartialEq)]
struct Counter {
    initial: i32,
}

impl Component for Counter {
    fn render(&self) -> impl IntoElement {
        let mut count = use_state(|| self.initial);
        label()
            .on_mouse_up(move |_| *count.write() += 1)
            .text(format!("Count: {}", count.read()))
    }
}
  • #[derive(PartialEq)] is required - Freya uses it to skip re-rendering unchanged subtrees.
  • Implement KeyExt and ChildrenExt when the component can be keyed or accept children.

ComponentOwned (when render needs to own self)

Component::render takes &self, so moving fields into closures forces let foo = self.foo.clone(); boilerplate. ComponentOwned::render takes self by value (the framework clones it for you), letting you move self directly. Requires #[derive(Clone)]. Reach for it only when you'd otherwise clone self (or several of its fields) inside render.

#[derive(PartialEq, Clone)]
struct Item { state: State<Vec<i32>>, i: usize }

impl ComponentOwned for Item {
    fn render(mut self) -> impl IntoElement {
        Button::new()
            .on_press(move |_| { self.state.write().remove(self.i); })
            .child("Remove")
    }
}

Function Components (app root only)

The app root is a plain function. Hooks like use_init_theme, use_init_radio_station, use_provide_context belong here.

fn app() -> impl IntoElement {
    rect().child("Hello, World!")
}

To pass data from main into the root, use the App trait:

struct MyApp { number: u8 }

impl App for MyApp {
    fn render(&self) -> impl IntoElement {
        label().text(self.number.to_string())
    }
}

Utility Functions (stateless, no hooks)

fn colored_label(color: Color, text: &str) -> impl IntoElement {
    label().color(color).text(text.to_string())
}

Reusable UI that uses hooks or props MUST be a Component (struct + impl Component). Plain functions are only for the app root and stateless helpers. Functions with hooks won't benefit from diffing/memoization and can't be keyed or accept reactive props cleanly.

Elements

Built-in element constructors:

  • rect() - layout primitive (direction, alignment, sizing, background, borders, corners, shadows, padding, scroll).
  • label() - single-line text.
  • paragraph() - multi-line / rich text via .text_span(...) children; also the target for use_editable.
  • image(holder) - raster image; holder from static_bytes(...), dynamic_bytes(...), or asset loaders.
  • svg(bytes) - vector image.

&str / String implement Into<Label>, so prefer rect().child("Hi") over rect().child(label().text("Hi")).

Element Builder Pattern

Elements use a fluent builder API. Never store an element in a variable to modify it later - chain all methods directly or use .maybe / .map.

// Good
rect()
    .background((255, 0, 0))
    .width(Size::fill())
    .height(Size::px(100.))
    .center()       // centers children both axes
    .expanded()     // fills available space in parent's main axis
    .horizontal()   // sets layout direction to horizontal
    .maybe(is_active, |el| el.child("Active"))
    .map(some_value, |el, v| el.child(v.to_string()))

// Bad - storing to modify later
let mut element = rect();

Common layout shorthands: .center() centers children on both axes; .expanded() makes the element fill all remaining space along the parent's main axis (equivalent to flex: 1 in CSS); .horizontal() and .vertical() set the layout direction (prefer them over .direction(Direction::Horizontal/Vertical)).

Conditional and Dynamic Rendering

rect()
    .maybe(show_badge, |el| el.child("New"))          // bool condition
    .map(large_size, |el, size| el.height(size))      // Option<T>, passes value
    .maybe_child(optional_element)                    // Option<impl IntoElement>
  • .maybe(bool, |el| el) - applies the callback when the condition is true
  • .map(Option<T>, |el, val| el) - applies the callback when the Option is Some, passing the inner value
  • .maybe_child(Option<impl IntoElement>) - appends a child only when Some

Name the element argument el in .maybe / .map callbacks (not r, rect, e, etc.).

Prefer one outer .maybe / .map over several consecutive .maybe_child calls gated on the same condition - it keeps the gating in one place and avoids re-evaluating the same predicate.

// Good, single .maybe wraps all conditional children
rect()
    .maybe(show, |el| {
        el.child(Title::new("Hi"))
            .child(Content::new().child("Hello"))
            .child(Footer::new())
    })

// Bad, same predicate repeated per child
rect()
    .maybe_child(show.then(|| Title::new("Hi")))
    .maybe_child(show.then(|| Content::new().child("Hello")))
    .maybe_child(show.then(|| Footer::new()))

Events

Attach handlers via builder methods on any element. Handlers receive Event<T>; use move closures to capture state.

rect()
    .on_press(move |_| { /* left click, tap, or Enter/Space when focused */ })
    .on_key_down(move |e: Event<KeyboardEventData>| { /* only while focused */ })
    .on_wheel(move |e| { /* scroll delta */ })
    .on_pointer_enter(move |_| { /* hover begin (mouse or touch) */ })

Catalog (all prefixed on_):

  • Press: press (left/tap/Enter/Space), all_press (any mouse button), secondary_down (right-click).
  • Mouse: mouse_up, mouse_down, mouse_move.
  • Pointer (mouse + touch unified): pointer_press, pointer_down, pointer_move, pointer_enter, pointer_leave, pointer_over, pointer_out.
  • Keyboard (require focus): key_down, key_up.
  • Wheel: wheel.
  • Touch: touch_start, touch_end, touch_move, touch_cancel.
  • File drop: file_drop.
  • Layout: sized (measured size changed).
  • Global (no hit-test; use sparingly): global_pointer_press, global_pointer_down, global_pointer_move, global_key_down, global_key_up, global_file_hover, global_file_hover_cancelled.
  • Capture (run before regular handlers): capture_global_pointer_press, capture_global_pointer_move.

Prefer on_press over raw mouse/pointer events for interactive elements: it covers click, tap, and keyboard activation, so accessibility comes free. Use on_mouse_* / on_pointer_* only when you need pointer-specific behavior (drag handles, canvas tools).

Event<T> has .stop_propagation() to cancel bubbling and .map(...) / .try_map(...) to transform inner data.

Callback props on custom components

Use EventHandler<T> for callback props; closures convert via .into().

#[derive(PartialEq)]
struct Confirm { on_accept: EventHandler<()> }

impl Component for Confirm {
    fn render(&self) -> impl IntoElement {
        let on_accept = self.on_accept;
        Button::new().on_press(move |_| on_accept.call(())).child("OK")
    }
}

Confirm { on_accept: (move |()| println!("yes")).into() }

EventHandler<T> is Copy; capture directly in move closures.

Focus and Accessibility

Focusable elements need a stable AccessibilityId from use_a11y() (one per focusable node), attached via .a11y_id(...) and .a11y_focusable(true). Track focus state with use_focus(id).

#[derive(PartialEq)]
struct FocusableBox;

impl Component for FocusableBox {
    fn render(&self) -> impl IntoElement {
        let a11y_id = use_a11y();
        let focus = use_focus(a11y_id);

        rect()
            .a11y_id(a11y_id)
            .a11y_focusable(true)
            .a11y_role(AccessibilityRole::Button)
            .on_press(move |_| println!("activated"))
            .maybe(focus() == Focus::Keyboard, |el| {
                el.border(Border::new().fill(Color::BLUE).width(2.))
            })
            .child("Click or Tab to me")
    }
}

Focus variants:

  • Focus::Not - not focused.
  • Focus::Pointer - focused by mouse/touch (no focus ring needed).
  • Focus::Keyboard - focused via Tab (render a focus ring).
  • focus.is_focused() matches Pointer or Keyboard.

on_press already fires on Enter/Space when focused, so keyboard activation is free. For raw key handling, KeyboardEventExt::is_press_event(&event) detects the OS activation gesture (Enter/Space; Ctrl+Alt+Space on macOS with VoiceOver).

Other a11y builders: .a11y_role(...), .a11y_alt("description"), .a11y_auto_focus(true), .a11y_member_of(other_id), .a11y_builder(|node| { /* raw accesskit::Node */ }).

Read the focused id globally via Platform::get().focused_accessibility_id.

Hooks

Hooks are prefixed with use_ (e.g. use_state, use_animation). Follow these rules:

  1. Only call hooks at the top level of render - never inside conditionals, loops, or closures.
  2. Hooks must be called in the same order on every render.
  3. Never call hooks inside event handlers - call them at the top of render and capture the values in move closures instead.
  4. Never call hooks inside loops - the number of hook calls must be constant across renders.
  5. Never call hooks outside of components - hooks only work inside a render method or a function component.
  6. Never call hooks inside async tasks - spawn callbacks are async and cannot call hooks; capture state before spawning. Some hooks have non-hook counterparts that are safe to call in async contexts (e.g. use_consumeconsume_context()).

Capture hook values in move closures for event handlers:

let mut state = use_state(|| false);
let on_click = move |_| state.set(true); // capture, not call inside handler
rect().on_mouse_up(on_click)

State Management

Local State

let mut count = use_state(|| 0);
*count.write() += 1;          // write
let n = *count.read();        // read
count.set(5);                 // convenience setter
count.set_if_modified(5);     // only writes (and notifies) if the new value differs

use_state returns a Copy type (State<T>). No .clone() needed when passing it around.

Avoid drop() on guards from .read()/.write(); guards release on scope exit. Prefer a smaller scope ({ let v = state.read(); ... }) or copying the value out (let n = *count.read();). Only use explicit drop(guard) when you must release a borrow before re-borrowing in the same scope.

Prefer set_if_modified over set when the new value may equal the current (syncing external/derived values, handlers that may fire unchanged). It skips the write and avoids waking subscribers. Requires T: PartialEq. set_if_modified_and_then(value, || { ... }) runs a callback only on actual change.

Pass local state to child components:

#[derive(PartialEq)]
struct Child(State<i32>);

Global State - Freya Radio

Use Freya Radio for large or deeply nested app state where you need surgical, fine-grained updates - only the components subscribed to a specific channel re-render when that channel changes. This makes it well-suited for complex UIs (e.g. a tab system where each tab has independent state, or a big data model where different parts of the UI subscribe to different slices).

Define your state and a channel enum that maps to the parts of the state that can change independently:

#[derive(Default, Clone)]
struct AppState {
    count: i32,
    name: String,
}

#[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
enum AppChannel {
    Count,
    Name,
}

impl RadioChannel<AppState> for AppChannel {}

Initialize once in the root component, then subscribe from any descendant:

// Root
use_init_radio_station::<AppState, AppChannel>(AppState::default);

// Any component - only re-renders when AppChannel::Count changes
let mut radio = use_radio(AppChannel::Count);
radio.read().count;
radio.write().count += 1;

For channels where a write to one should also notify subscribers of another, override derive_channel:

impl RadioChannel<AppState> for AppChannel {
    fn derive_channel(self, _state: &AppState) -> Vec<Self> {
        match self {
            // Writing to Count also notifies Name subscribers
            AppChannel::Count => vec![self, AppChannel::Name],
            AppChannel::Name => vec![self],
        }
    }
}

For complex state transitions, implement the reducer pattern with DataReducer:

impl DataReducer for AppState {
    type Channel = AppChannel;
    type Action = AppAction;

    fn reduce(&mut self, action: AppAction) -> ChannelSelection<AppChannel> {
        match action {
            AppAction::Increment => { self.count += 1; }
            AppAction::SetName(n) => { self.name = n; }
        }
        ChannelSelection::Current
    }
}

// Then in a component:
radio.apply(AppAction::Increment);

Readable / Writable (type-erased abstractions)

Readable<T> and Writable<T> are type-erased wrappers over any reactive value of type T. Use them as component props so the component works regardless of where the state lives: local State<T>, a Memo<T>, a radio slice, or a plain owned value. Both are PartialEq (always equal) and Clone, usable directly as component fields.

#[derive(PartialEq)]
struct NameInput { name: Writable<String> }

impl Component for NameInput {
    fn render(&self) -> impl IntoElement {
        Input::new(self.name.clone()) // Input is two-way bound to the Writable
    }
}

// Caller side: any source converts via `into_writable()` / `into_readable()`
NameInput { name: local_name.into_writable() }            // from State<String>
NameInput { name: name_slice.into_writable() }            // from a RadioSliceMut

Conversions:

  • State<T>Writable<T> / Readable<T> via IntoWritable / IntoReadable.
  • Memo<T>Readable<T> via IntoReadable.
  • RadioSliceReadable<T>; RadioSliceMutReadable<T> or Writable<T>.
  • Writable<T>Readable<T> via From (downgrade write access).
  • Plain owned TReadable<T> via From (non-reactive; tests, defaults).

API surface:

  • Readable<T>: read() (subscribes), peek() (no subscription).
  • Writable<T>: read(), peek(), write(), plus WritableUtils helpers (set, set_if_modified, with_mut, ...). Subscriptions and notifications route to the original source.

Prefer Readable<T> for read-only consumers; use Writable<T> only when the component must mutate.

Context API

Use context to make a value available to any descendant component without threading it through every prop. Prefer this over static variables, thread_local!, or global singletons - context is scoped to the component tree and plays well with Freya's reactivity.

// Provider: stores the value and makes it available to all descendants
fn app() -> impl IntoElement {
    use_provide_context(|| AppConfig { theme: Theme::Dark });
    rect().child(DeepChild {})
}

// Consumer: retrieve by type, walks up the tree until found
#[derive(PartialEq)]
struct DeepChild;
impl Component for DeepChild {
    fn render(&self) -> impl IntoElement {
        let config = use_consume::<AppConfig>();
        format!("Theme: {:?}", config.theme)
    }
}

Use use_try_consume::<T>() when the context may not be present. If context is not found, use_consume panics.

Context values are identified by type, so each distinct type gets its own slot. Providing the same type again in a deeper component shadows the ancestor's value for that subtree.

Context is the right tool for dependency injection (e.g. passing a DB client, config, or theme down the tree). For reactive shared state use Freya Radio; for passing state between a parent and immediate children, plain props or State<T> are simpler.

Choosing state type

  • use_state - component-local state
  • Context API - dependency injection and non-reactive shared values across the tree; prefer over statics
  • Freya Radio - large/nested state, surgical per-channel updates, multi-window
  • Readable/Writable - reusable components that don't care about backing storage

Derived State and Side Effects

For simple derived values, compute them directly in render - no hook needed:

let doubled = *count.read() * 2;

For expensive computations that should only re-run when their dependencies change, use use_memo. It subscribes to any State read inside the callback and caches the result:

let expensive = use_memo(move || {
    let n = *count.read(); // subscribed - reruns when count changes
    compute_something(n)
});
let value = expensive.read();

For side effects that should re-run when state changes (e.g. logging, triggering external systems), use use_side_effect. Do not use it to sync one state into another - derive values directly or use use_memo instead:

use_side_effect(move || {
    let value = *count.read(); // subscribed
    println!("count changed: {value}");
});

Async

Spawning tasks

Use Freya's spawn() (not tokio::spawn) for async work that updates the UI. Tasks spawned with spawn() are tied to Freya's reactivity system and can safely write to component state:

let mut data = use_state(|| None);

use_hook(move || {
    spawn(async move {
        let result = fetch_something().await;
        data.set(Some(result));
    });
});

use_hook runs once on mount, making it the right place for one-shot side effects. spawn returns a TaskHandle you can cancel if needed.

Async functions in components

Components and hooks are synchronous - you cannot await inside render. Prefer use_future for typical async work (see below). Only reach for use_hook + spawn when you need fine-grained control over the task lifecycle:

// Only when you need manual control
use_hook(move || {
    spawn(async move {
        let s = some_async_fn().await;
        result.set(s);
    });
});

use_future

use_future wraps this pattern: it starts an async task on mount and exposes its state as FutureState<D> (Pending, Loading, Fulfilled(D)):

let task = use_future(|| async {
    fetch_user(42).await
});

match &*task.state() {
    FutureState::Pending | FutureState::Loading => "Loading...",
    FutureState::Fulfilled(user) => user.name.as_str(),
}

Call task.start() to restart and task.cancel() to stop it.

freya-query (cached async data)

For data that should be cached, deduplicated, and automatically refetched, use freya-query (features = ["query"]):

// Define the query
#[derive(Clone, PartialEq, Hash, Eq)]
struct FetchUser;

impl QueryCapability for FetchUser {
    type Ok = String;
    type Err = String;
    type Keys = u32;

    async fn run(&self, user_id: &u32) -> Result<String, String> {
        Ok(format!("User {user_id}"))
    }
}

// Use it in a component
impl Component for UserProfile {
    fn render(&self) -> impl IntoElement {
        let query = use_query(Query::new(self.0, FetchUser));

        match &*query.read().state() {
            QueryStateData::Pending => "Loading...",
            QueryStateData::Settled { res, .. } => res.as_deref().unwrap_or("Error"),
            QueryStateData::Loading { .. } => "Refreshing...",
        }
    }
}

Multiple components using the same (capability, keys) pair share one cache entry. Invalidate with query.invalidate() or QueriesStorage::<FetchUser>::invalidate_all().await.

For write operations, use use_mutation + MutationCapability. The on_settled callback is the right place to invalidate related queries after a mutation.

Prefer freya-query over manual use_future + state when you need caching, background refetch, or deduplication.

Tokio integration

Freya has its own async runtime. To use Tokio-ecosystem crates (reqwest, sqlx, etc.), enter a Tokio runtime context in main before launching:

fn main() {
    let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
    let _guard = rt.enter(); // keep alive for the whole program

    launch(LaunchConfig::new().with_window(WindowConfig::new(app)))
}

Use Freya's spawn() for UI updates. tokio::spawn runs on the Tokio runtime and cannot update component state.

Theming

Use the theming system to centralize component styling. Prefer it over hand-rolled structs of Color/size constants or scattered hardcoded values.

A Theme bundles a ColorsSheet (the app palette) and per-component *ThemePreference entries indexed by string key. Components read their theme via get_theme! and resolve color references against the active ColorsSheet.

Provide a theme

Initialize a theme in the root component. The returned State<Theme> is reactive - writing to it switches the theme app-wide:

fn app() -> impl IntoElement {
    let mut theme = use_init_theme(light_theme); // or dark_theme, or your own
    rect()
        .theme_background()
        .theme_color()
        .expanded()
        .center()
        .child(Button::new().on_press(move |_| theme.set(dark_theme())).child("Dark"))
}

Use use_init_root_theme to register at the root scope. To follow the OS preference, convert Platform::get().preferred_theme via the FromPreference::to_theme extension.

Element theme extensions

Built-in elements expose helpers that read the active theme - prefer these over hardcoded colors:

  • rect().theme_background(), rect().theme_color()
  • label().theme_color(), paragraph().theme_color()
  • svg(...).theme_color() / .theme_accent_color() / .theme_fill() / .theme_stroke() / .theme_accent_fill() / .theme_accent_stroke()

Custom themes

Start from light_theme() / dark_theme() and override what you need. Use LIGHT_COLORS / DARK_COLORS as base palettes with struct update syntax:

fn brand_theme() -> Theme {
    let mut theme = dark_theme();
    theme.name = "brand";
    theme.colors = ColorsSheet {
        primary: Color::from_rgb(37, 52, 63),
        secondary: Color::from_rgb(255, 155, 81),
        tertiary: Color::from_rgb(81, 155, 255),
        ..DARK_COLORS
    };
    theme
}

ColorsSheet covers brand (primary/secondary/tertiary), status (success/warning/error/info), surfaces (background, surface_primary/secondary/tertiary, surface_inverse[_secondary|_tertiary]), borders (border, border_focus, border_disabled), text (text_primary/secondary/placeholder/inverse/highlight), interaction states (hover, focus, active, disabled), and utility (overlay, shadow).

Defining a theme for your component

Use define_theme! to generate the theme types for a component. This replaces hand-rolled "props struct of colors and sizes" patterns:

define_theme! {
    %[component]
    pub StatusBadge {
        %[fields]
        background: Color,
        color: Color,
        corner_radius: CornerRadius,
        padding: Gaps,
    }
}

#[derive(PartialEq)]
struct StatusBadge {
    theme: Option<StatusBadgeThemePartial>,
}

impl Component for StatusBadge {
    fn render(&self) -> impl IntoElement {
        let StatusBadgeTheme { background, color, corner_radius, padding } =
            get_theme!(&self.theme, StatusBadgeThemePreference, "status_badge");

        rect()
            .background(background)
            .corner_radius(corner_radius)
            .padding(padding)
            .child(label().text("Active").color(color).font_size(12.))
    }
}

The macro generates three structs:

  • StatusBadgeThemePartial - Option<Preference<T>> per field, used for per-instance overrides.
  • StatusBadgeThemePreference - Preference<T> per field, registered in the Theme as the default.
  • StatusBadgeTheme - resolved concrete values, returned by get_theme!.

get_theme!(&self.theme, StatusBadgeThemePreference, "status_badge") looks up the registered preference, applies any ThemePartial override on the instance, and resolves it against the active ColorsSheet.

Register the default preference on your custom theme:

theme.set(
    "status_badge",
    StatusBadgeThemePreference {
        background: Preference::Reference("secondary"),    // resolves from ColorsSheet
        color: Preference::Reference("text_inverse"),
        corner_radius: CornerRadius::new_all(99.).into(),  // Specific via Into
        padding: Gaps::new(4., 10., 4., 10.).into(),
    },
);

Preference::Reference("...") looks up the named color in the active ColorsSheet so the component automatically follows theme switches. Only Color fields support references - Size, Gaps, CornerRadius, f32, and Duration must use Preference::Specific(v) (or v.into()).

The macro also generates a StatusBadgeThemePartialExt trait implemented on the component, giving callers per-field builder methods:

StatusBadge::new()
    .background(Color::from_rgb(123, 123, 123))   // from the generated Ext trait
    .corner_radius(CornerRadius::new_all(4.))

For components that store the partial under a non-default field name (e.g. theme_colors alongside theme_layout), pass for = ...; theme_field = ...; to point the generated builder at the right field. Examples: Button, Card, Switch, Input in crates/freya-components/.

When to define a theme

  • The component is reusable and could be styled differently across apps or theme switches: use define_theme!.
  • You currently have a struct of Color/size constants that callers tweak: replace it with a theme.
  • One-off internal helpers with no styling variation: plain values are fine, no theme needed.

Keying

Use .key(id) on elements in dynamic lists to ensure correct reconciliation on reorders:

VirtualScrollView::new(|i, _| {
    rect()
        .key(i)
        .child(format!("Item {i}"))
        .into()
})
.length(items.len())

Missing .key() in dynamic lists causes element misidentification during reorders.

Internationalization (freya-i18n)

Enable with features = ["i18n"]. Uses Fluent (.ftl files) for translations.

1. Define .ftl files:

# en-US.ftl
hello_world = Hello, World!
hello = Hello, { $name }!

2. Initialize once in the root component:

use freya::i18n::*;

let mut i18n = use_init_i18n(|| {
    I18nConfig::new(langid!("en-US"))
        .with_locale(Locale::new_static(langid!("en-US"), include_str!("../i18n/en-US.ftl")))
        .with_locale(Locale::new_static(langid!("es-ES"), include_str!("../i18n/es-ES.ftl")))
        .with_fallback(langid!("en-US"))
});

3. Translate in any descendant component:

// t! panics if key missing, te! returns Result, tid! falls back to the key string
t!("hello_world")                  // "Hello, World!"
t!("hello", name: {"Alice"})       // "Hello, Alice!"
te!("hello_world")                 // Ok("Hello, World!")
tid!("missing-key")                // "message-id: missing-key should be translated"

4. Switch language at runtime:

let mut i18n = I18n::get(); // retrieve from any descendant
i18n.set_language(langid!("es-ES"));

For multi-window apps, create with I18n::create_global in main and share with use_share_i18n.

Animations

Use use_animation for manual control and use_animation_transition to animate between two values reactively:

// Manual: call .start() / .reverse() yourself
let mut anim = use_animation(|_| AnimColor::new((240, 240, 240), (200, 80, 80)).time(400));
rect().background(&*anim.read()).on_press(move |_| anim.start())

// Transition: re-runs automatically when the tracked value changes
let color = use_animation_transition(is_active, |from, to| AnimColor::new(from, to).time(300));
rect().background(&*color.read())

Animate colors (AnimColor), sizes, positions, and other numeric properties. Easing functions and sequencing are supported.

Routing

Enable with features = ["router"]. Define routes with #[derive(Routable)], render them with router::<Route>(), place the current page with outlet::<Route>(), and navigate with Link or RouterContext::get().replace(...):

#[derive(Routable, Clone, PartialEq)]
enum Route {
    #[route("/")]
    Home,
    #[route("/settings")]
    Settings,
}

fn app() -> impl IntoElement {
    router::<Route>(|| RouterConfig::default())
}

Headless Testing

freya-testing lets you test components without a window. Use TestingRunner to mount a component, simulate interactions, and assert on state:

use freya_testing::prelude::*;

let (mut runner, state) = TestingRunner::new(app, (300., 300.).into(), |r| {
    r.provide_root_context(|| State::create(0))
}, 1.);

runner.sync_and_update();
runner.click_cursor((15., 15.));
assert_eq!(*state.peek(), 1);

Call runner.render_to_file("out.png") to snapshot the current UI.

Icons

Enable with features = ["icons"]. Uses Lucide icons rendered as SVGs:

use freya::icons;

svg(icons::lucide::antenna()).color((120, 50, 255)).expanded()

Rich Text Editing

Use use_editable to manage a text editor with cursor, selection, keyboard shortcuts, and virtualization. Wire it to a paragraph() element's event handlers and feed EditableEvents from mouse/keyboard events. See examples/ for full wiring.

Code Editor

Enable with features = ["code-editor"]. CodeEditorData holds a Rope-backed buffer with tree-sitter syntax highlighting. Pass it to the CodeEditor component:

let editor = use_state(|| {
    let mut e = CodeEditorData::new(Rope::from_str(src), LanguageId::Rust);
    e.parse();
    e.measure(14., "Jetbrains Mono");
    e
});
CodeEditor::new(editor, focus.a11y_id())

Plotting

Enable with features = ["plot"]. Use the plot() element with a RenderCallback and draw into it using the Plotters API via PlotSkiaBackend:

plot(RenderCallback::new(|ctx| {
    let backend = PlotSkiaBackend::new(ctx.canvas, ctx.font_collection, size).into_drawing_area();
    // ... Plotters drawing code
})).expanded()

Material Design

Enable with features = ["material-design"]. Adds style modifiers like .ripple() to built-in components:

use freya::material_design::*;
Button::new().ripple().child("Click me")

WebView

Enable with features = ["webview"]. Embeds a browser view into your UI:

use freya::webview::*;
WebView::new("https://example.com").expanded()

Terminal

Enable with features = ["terminal"]. Spawns a PTY process and renders it as a terminal:

use freya::terminal::*;
let mut cmd = CommandBuilder::new("bash");
cmd.env("TERM", "xterm-256color");
let handle = TerminalHandle::new(TerminalId::new(), cmd, None).ok();
// Render with Terminal::new(handle) and forward keyboard events via handle.write_key()

Camera

Enable with features = ["camera"]. Streams frames from a webcam into reactive state:

use freya::camera::*;
let camera = use_camera(CameraConfig::default);
CameraViewer::new(camera)

On macOS, call freya::camera::init() from main to request authorization before launching.

Developer Tools

Enable with features = ["devtools"]. Adds a real-time component tree inspector. Run the devtools app alongside your app to examine layout, props, and state.

Crate Features

Add to your Cargo.toml as needed:

freya = { version = "...", features = ["router", "radio"] }
Feature What it enables
router Page routing (freya-router)
i18n Internationalization via Fluent (freya-i18n)
remote-asset Load images/assets from remote URLs
radio Global state management (freya-radio)
query Async data fetching with caching (freya-query)
sdk Generic utility APIs (freya-sdk)
plot Chart/plotting via Plotters (freya-plotters-backend)
gif Animated GIF support in GifViewer
calendar Calendar date-picker component
markdown Markdown renderer component
icons SVG icon library via Lucide (freya-icons)
material-design Material Design theme (freya-material-design)
webview Embed a WebView (freya-webview)
terminal Terminal emulator (freya-terminal)
code-editor Code editing APIs (freya-code-editor)
camera Webcam capture (freya-camera)
tray System tray support
titlebar Custom window titlebar component
devtools Developer tools overlay
performance Performance monitoring plugin
hotpath Hot-path optimization
all All of the above (except devtools/performance/hotpath)

Further Reference

  • AGENTS.md (also symlinked as CLAUDE.md) in the repo root - authoritative dev workflow and Rust conventions for working on Freya itself.
  • crates/freya/src/_docs/ - in-source documentation for hooks, state management, components, routing, animations, and more.
  • examples/ - 150+ working examples covering every feature.
Install via CLI
npx skills add https://github.com/marc2332/freya --skill freya
Repository Details
star Stars 2,779
call_split Forks 122
navigation Branch main
article Path SKILL.md
More from Creator