Freya logo

Freya

CommunityPopular
marc2332
freya

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

Overview

Publishermarc2332
Repositoryfreya
Skill namefreya
Stars
3.2K
Forks
137
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by marc2332 on GitHub. Read the source before you install it.

Installation

Install the Freya AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/marc2332/freya.git /tmp/freya
mkdir -p .claude/skills
cp -r /tmp/freya/plugins/freya/skills/freya .claude/skills/freya
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Freya in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Freya on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Freya is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

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)

rust
#[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.

rust
#[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.

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

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

rust
struct MyApp { number: u8 }

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

Utility Functions (stateless, no hooks)

rust
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(handle) - raster image; handle from static_bytes(...), dynamic_bytes(...), or asset loaders.

&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.

rust
// 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

rust
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.

rust
// 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.

rust
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 two cancellation methods, plus .map(...) / .try_map(...) to transform inner data:

  • .stop_propagation(): don't bubble this event to ancestor handlers. No effect on events that don't bubble (move/enter/leave, capture, global).
  • .prevent_default(): don't fire the follow-up events this one triggers (e.g. in on_mouse_up, suppresses the on_pointer_press and on_global_pointer_press that would follow).

Callback props on custom components

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

rust
#[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).

rust
#[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:

rust
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

rust
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:

rust
#[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:

rust
#[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:

rust
// 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:

rust
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:

rust
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.

rust
#[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.

rust
// 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:

rust
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:

rust
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:

rust
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:

rust
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:

rust
// 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)):

rust
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"]):

rust
// 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:

rust
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:

rust
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.

Theme extensions

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

  • rect().theme_background(), rect().theme_color()
  • label().theme_color(), paragraph().theme_color()
  • SvgViewer::new(...).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:

rust
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:

rust
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:

rust
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:

rust
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:

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

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

A custom component can expose the same .key method by implementing KeyExt over a stored DiffKey and forwarding it from render_key, so it reconciles like a built-in element:

rust
#[derive(PartialEq)]
struct Task {
    title: String,
    key: DiffKey,
}

impl Task {
    fn new(title: String) -> Self {
        Self {
            title,
            key: DiffKey::None,
        }
    }
}

impl KeyExt for Task {
    fn write_key(&mut self) -> &mut DiffKey {
        &mut self.key
    }
}

impl Component for Task {
    fn render(&self) -> impl IntoElement {
        label().text(self.title.clone())
    }

    fn render_key(&self) -> DiffKey {
        self.key.clone().or(self.default_key())
    }
}

fn app(ids: Vec<u64>) -> impl IntoElement {
    rect().children(
        ids.iter()
            .map(|id| Task::new(format!("Task {id}")).key(*id).into()),
    )
}

Internationalization (freya-i18n)

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

1. Define .ftl files:

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

2. Initialize once in the root component:

rust
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:

rust
// 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:

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

The instance lives in the root context shared by all windows, so calling use_init_i18n in each window reuses the same state.

Animations

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

rust
// 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(...):

rust
#[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:

rust
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:

rust
use freya::icons;

SvgViewer::new(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. You bring your own tree-sitter grammar and highlights query via EditorLanguage, so any language can be supported (add the grammar crate, e.g. tree-sitter-rust, as a dependency). Pass None to disable highlighting. Then pass the data to the CodeEditor component:

rust
let editor = use_state(|| {
    let language = EditorLanguage::new(
        tree_sitter_rust::LANGUAGE,
        tree_sitter_rust::HIGHLIGHTS_QUERY,
    );
    let mut e = CodeEditorData::new(Rope::from_str(src), language);
    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:

rust
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:

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

WebView

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

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

Terminal

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

rust
use freya::terminal::*;
let mut cmd = CommandBuilder::new("bash");
cmd.env("TERM", "xterm-256color");
let handle = TerminalHandle::new(TerminalId::new(), PtyBackend::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:

rust
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.

Borderless Windows

Enable with features = ["borderless"]. For windows using .with_decorations(false), register BorderlessPlugin (crate freya-borderless-plugin). Combine with TitlebarButton (titlebar feature) and rect().window_drag() for a custom titlebar.

rust
use freya::borderless::BorderlessPlugin;

launch(
    LaunchConfig::new()
        .with_plugin(BorderlessPlugin::new().with_corner_radius(12.))
        .with_window(
            WindowConfig::new(app)
                .with_decorations(false)
                .with_transparency(true)
                .with_background(Color::TRANSPARENT),
        ),
)

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:

toml
freya = { version = "...", features = ["router", "radio"] }
FeatureWhat it enables
routerPage routing (freya-router)
i18nInternationalization via Fluent (freya-i18n)
remote-assetLoad images/assets from remote URLs
radioGlobal state management (freya-radio)
queryAsync data fetching with caching (freya-query)
sdkGeneric utility APIs (freya-sdk)
plotChart/plotting via Plotters (freya-plotters-backend)
gifAnimated GIF support in GifViewer
calendarCalendar date-picker component
markdownMarkdown renderer component
iconsSVG icon library via Lucide (freya-icons)
material-designMaterial Design theme (freya-material-design)
webviewEmbed a WebView (freya-webview)
terminalTerminal emulator (freya-terminal)
terminal-ptyBuilt-in PTY backend for the terminal, implies terminal
code-editorCode editing APIs (freya-code-editor)
cameraWebcam capture (freya-camera)
traySystem tray support
titlebarCustom window titlebar component
borderlessBorderlessPlugin helpers for undecorated windows (freya-borderless-plugin)
devtoolsDeveloper tools overlay
performancePerformance monitoring plugin
hotpathHot-path optimization
allAll 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.

Frequently asked questions

What does the Freya AI skill do?

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

Why use Freya on TypingMind?

Because you install it once and use it with any model. Freya is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Freya in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/marc2332/freya/tree/main/plugins/freya/skills/freya. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Freya?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Freya?

As many as you like. As long as a model supports skills, you can use Freya with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Freya AI skill free?

Yes. It is published on GitHub by marc2332 under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇