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
KeyExtandChildrenExtwhen 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.
rustfn app() -> impl IntoElement { rect().child("Hello, World!") }
To pass data from main into the root, use the App trait:
ruststruct MyApp { number: u8 } impl App for MyApp { fn render(&self) -> impl IntoElement { label().text(self.number.to_string()) } }
Utility Functions (stateless, no hooks)
rustfn 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 foruse_editable.image(handle)- raster image;handlefromstatic_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
rustrect() .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 theOptionisSome, passing the inner value.maybe_child(Option<impl IntoElement>)- appends a child only whenSome
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.
rustrect() .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. inon_mouse_up, suppresses theon_pointer_pressandon_global_pointer_pressthat 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()matchesPointerorKeyboard.
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:
- Only call hooks at the top level of
render- never inside conditionals, loops, or closures. - Hooks must be called in the same order on every render.
- Never call hooks inside event handlers - call them at the top of
renderand capture the values inmoveclosures instead. - Never call hooks inside loops - the number of hook calls must be constant across renders.
- Never call hooks outside of components - hooks only work inside a
rendermethod or a function component. - Never call hooks inside async tasks -
spawncallbacks 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_consume→consume_context()).
Capture hook values in move closures for event handlers:
rustlet 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
rustlet 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:
rustimpl 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:
rustimpl 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>viaIntoWritable/IntoReadable.Memo<T>→Readable<T>viaIntoReadable.RadioSlice→Readable<T>;RadioSliceMut→Readable<T>orWritable<T>.Writable<T>→Readable<T>viaFrom(downgrade write access).- Plain owned
T→Readable<T>viaFrom(non-reactive; tests, defaults).
API surface:
Readable<T>:read()(subscribes),peek()(no subscription).Writable<T>:read(),peek(),write(), plusWritableUtilshelpers (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:
rustlet 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:
rustlet 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:
rustuse_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:
rustlet 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)):
rustlet 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:
rustfn 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:
rustfn 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:
rustfn 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:
rustdefine_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 theThemeas the default.StatusBadgeTheme- resolved concrete values, returned byget_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:
rusttheme.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:
rustStatusBadge::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:
rustVirtualScrollView::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:
rustuse 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:
rustlet 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:
rustuse 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:
rustuse 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:
rustlet 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:
rustplot(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:
rustuse freya::material_design::*; Button::new().ripple().child("Click me")
WebView
Enable with features = ["webview"]. Embeds a browser view into your UI:
rustuse freya::webview::*; WebView::new("https://example.com").expanded()
Terminal
Enable with features = ["terminal-pty"]. Spawns a PTY process and renders it as a terminal:
rustuse 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:
rustuse 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.
rustuse 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:
tomlfreya = { 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) |
terminal-pty | Built-in PTY backend for the terminal, implies terminal |
code-editor | Code editing APIs (freya-code-editor) |
camera | Webcam capture (freya-camera) |
tray | System tray support |
titlebar | Custom window titlebar component |
borderless | BorderlessPlugin helpers for undecorated windows (freya-borderless-plugin) |
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 asCLAUDE.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.

