Install the "freya" agent skill from https://github.com/marc2332/freya/tree/main/plugins/freya/skills/freya into .claude/skills/freya/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "freya", then confirm the skill loads.
Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
Type this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
skills CLI
$ npx skills add marc2332/freya --skill freya -a codex
Project install goes to .agents/skills/; add -g for ~/.codex/skills/.
Install the "freya" agent skill from https://github.com/marc2332/freya/tree/main/plugins/freya/skills/freya into .agents/skills/freya/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "freya", then confirm the skill loads.
Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
skills CLI
$ npx skills add marc2332/freya --skill freya -a cursor
Project install goes to .agents/skills/; add -g for ~/.cursor/skills/.
Install the "freya" agent skill from https://github.com/marc2332/freya/tree/main/plugins/freya/skills/freya into .cursor/skills/freya/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "freya", then confirm the skill loads.
Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
skills CLI
$ npx skills add marc2332/freya --skill freya -a gemini-cli
Project install goes to .agents/skills/; add -g for ~/.gemini/skills/.
Install the "freya" agent skill from https://github.com/marc2332/freya/tree/main/plugins/freya/skills/freya into .gemini/skills/freya/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "freya", then confirm the skill loads.
Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
GitHub CLI
$ gh skill install marc2332/freya freya
Installs for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
skills CLI
$ npx skills add marc2332/freya --skill freya -a github-copilot
Project install goes to .agents/skills/; add -g for ~/.copilot/skills/.
Install the "freya" agent skill from https://github.com/marc2332/freya/tree/main/plugins/freya/skills/freya into .github/skills/freya/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "freya", then confirm the skill loads.
GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
skills CLI
$ npx skills add marc2332/freya --skill freya -a opencode
OpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
Install the "freya" agent skill from https://github.com/marc2332/freya/tree/main/plugins/freya/skills/freya into .opencode/skills/freya/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "freya", then confirm the skill loads.
OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
Facts
Skill name
freya
GitHub stars
3.2k
Token cost
~9.1k tokens
SKILL.md length
2,660 words
Files
1
Skills in repo
1
Repo updated
First seen
Licence
MIT
At a glance
Freya Rust GUI framework best practices, patterns, and conventions.
Works in 6 steps: Only call hooks at the top level of… → Hooks must be called in the same order… → Never call hooks inside event handlers -… → …
Writing Freya components
SKILL.md covers Components, Elements, Element Builder Pattern and Events, plus 6 more sections
Calls node; reaches freyaui.dev
What it does
Freya is an agent skill from marc2332/freya. Freya Rust GUI framework best practices, patterns, and conventions. Use when writing Freya components, hooks, elements, or working on a Freya project.
Its SKILL.md is about 9.1k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Mobile. It works with Rust and Android. The repository describes itself as: Cross-platform native GUI library for 🦀 Rust. The licence is MIT.
When your agent uses it
Writing Freya components
Working on a Freya project
Example prompts
“/freya”
Workflow steps
6 steps, taken from the first numbered list in SKILL.md.
1Only call hooks at the top level of render - never inside conditionals, loops, or closures.
2Hooks must be called in the same order on every render.
3Never call hooks inside event handlers - call them at the top of render and capture the values in move closures instead.
4Never call hooks inside loops - the number of hook calls must be constant across renders.
5Never call hooks outside of components - hooks only work inside a render method or a function component.
6Never call hooks inside async tasks - spawn callbacks are async and cannot call hooks; capture state before spawning. Some hooks have…
What it can do on your machine
Read from SKILL.md and the folder at commit 4c52087. It shows what the files ask for, not the result of running them.
Tool permissions
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Runs code
Shell commands in SKILL.md call:
node
From the folder's file list and the shell code blocks in SKILL.md.
Network
Hosts in commands or code, which the agent is likely to contact:
freyaui.dev
Also links to:
projectfluent.org
From URLs in SKILL.md, links to its own repository left out.
Credentials
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Context cost
Freya loads about 9.1k tokens when it runs. Until then it costs about 39 tokens; SKILL.md has 2,660 words of instructions outside code blocks.
Always· name and description, kept in context so the agent knows when to use it
~39
When it runs· the whole SKILL.md, loaded when a task matches
~9.1k
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
Safety
Auto-check passed
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
#[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.
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.
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)).
.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) */ })
Global (no hit-test; use sparingly): global_pointer_up, 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_up, 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_up that would follow).
Callback props on custom components
Use EventHandler<T> for callback props; closures convert via .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:
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 render and capture the values in move closures 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 render method or a function component.
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_consume → consume_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_press(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:
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.
RadioSlice → Readable<T>; RadioSliceMut → Readable<T> or Writable<T>.
Writable<T> → Readable<T> via From (downgrade write access).
Plain owned T → Readable<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.
Show full SKILL.md (1,037 more words)Show less
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:
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:
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:
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(...):
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:
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")
HTML
Enable with features = ["html"]. Renders an HTML + CSS document with Blitz:
rust
use freya::html::*;
// The view is driven by a handle, which also exposes history navigation
// (back/forward) and the current URL. Sources can be remote or inline.
let mut handle = use_html(|| HtmlSource::url("https://example.com"));
HtmlView::new(handle).expanded();
handle.back();
handle.navigate("https://freyaui.dev");
use_html(|| HtmlSource::html("<h1>Hello</h1>"));
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.
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"] }
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)
html
Render HTML + CSS via Blitz (freya-html)
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
metrics
Metrics overlay plugin (freya-metrics-plugin)
hotpath
Hot-path optimization
all
All of the above (except devtools/metrics/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.
Freya next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
A skill your agent uses when preparing, installing, diagnosing, or explaining the host Android Emulator environment for flutterrustbridge local runtime validation, including Android SDK command-line…
Prepare or publish a Whisker release across Rust crates, Android SDK, Gradle plugin, and SwiftPM; recover a partial release or diagnose a missing SDK pin.
Freya Rust GUI framework best practices, patterns, and conventions. Freya is an agent skill from marc2332/freya. Freya Rust GUI framework best practices, patterns, and conventions.
When should I use Freya?
Freya fits situations like: writing Freya components; working on a Freya project.
How do I install Freya in Claude Code?
Run `npx skills add marc2332/freya --skill freya -a claude-code`. Or copy the skill folder (plugins/freya/skills/freya in marc2332/freya) into .claude/skills/freya in your project. Claude Code loads it when a task matches its description.
How do I install Freya in Codex?
Run `npx skills add marc2332/freya --skill freya -a codex`. Or copy the skill folder (plugins/freya/skills/freya in marc2332/freya) into .agents/skills/freya in your project. Codex loads it when a task matches its description.
Can I use Freya in Cursor, Gemini CLI or GitHub Copilot?
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add marc2332/freya --skill freya -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/freya, .gemini/skills/freya, .github/skills/freya and .opencode/skills/freya in your project.
What does Freya need to run?
Going by SKILL.md and its folder, Freya needs the command-line tools its instructions call (node).
Does Freya access the network?
SKILL.md names 2 domains. In commands or code: freyaui.dev; the agent is likely to contact it when it follows the instructions. As links in the text: projectfluent.org. This is read from the text; nothing was executed.
Is Freya safe to install?
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
What licence does Freya use?
Freya is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
How many tokens does Freya use?
About 9.1k tokens (SKILL.md is roughly 36k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
What are the alternatives to Freya?
Skills that share tags, products or a category with Freya: Frb Android Emulator Prepare (fzyzcjy/flutter_rust_bridge, 5.4k stars), Exeify Web2exe (44886/Exeify, 178 stars), Dpis Hyperos Smoke (Kwensiu/DPIS, 108 stars) and Release Whisker (whiskerrs/whisker, 140 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
Who maintains Freya?
marc2332 (a GitHub user) maintains it in marc2332/freya, which has 3,208 GitHub stars. The repository was last updated on October 7, 2026.
Source: marc2332/freya on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.