# Fission App Guidelines
These instructions apply when building or reviewing a Fission-based app in this
tree.
## Source-Grounded Work
- Start from the real app entrypoint, then trace into screens, reusable widgets,
and lower-level render behavior before changing UI code.
- For UI reviews, group findings by visible region. Do not stop at a screenshot
description; trace the component and widget code that creates each issue.
- When a visual problem looks small, keep looking for related spacing,
alignment, overflow, typography, state, and target-specific issues before
reporting completion.
- Keep edits scoped to the component, widget, route, shell, or target behavior
being changed. Avoid broad cleanup unless it is required for the task.
## Product and UX Principles
- Design for users with limited attention, uncertainty, effort, and motivation.
Strong product UX is about reducing friction and helping users complete their
own goals, not only visual polish.
- Use smart defaults when the likely answer is predictable. Defaults should be
editable, visible, and chosen for the user's benefit rather than to quietly
advantage the business.
- Give users a truthful sense of progress. Credit real completed work, show what
remains, and avoid fake progress percentages.
- Demonstrate useful value before asking for account creation, payment, or other
commitment. The ask should preserve, extend, or personalize something the user
already understands.
- Move creation before administration where possible: choose, create, see a
result, then save. Do not manufacture busywork or tedious sunk costs.
- Make genuine consequences of inaction visible without inventing urgency,
scarcity, guilt, or risk. Prefer neutral labels such as "Continue without
backup" over shaming labels.
- Provide pricing and plan context with fair comparisons: total cost, cost per
active user, included usage, time saved, or risk covered. Do not hide the true
total behind a smaller unit.
## Product-Led Growth and Shareability
- Prefer features that can demonstrate themselves quickly. A good product moment
should be understandable in a few seconds without a long explanation.
- Build at least one memorable, repeatable product action that can appear inside
natural user stories, screenshots, short demos, or creator content.
- Let the product itself be the creative: show the problem, the action, and the
satisfying result directly in the UI.
- Create curiosity ethically. It is fine for a feature to invite "what app is
that?" moments; it is not fine to mislead users or hide essential information.
- Design shareable outputs, saved states, exports, or previews when they serve
the user's goal. Do not add viral mechanics that interrupt the core task.
## Visual Design and Progressive Disclosure
- Use a modern, calm visual design with clear hierarchy, strong typography, and
purposeful spacing.
- Use progressive disclosure. Show the next meaningful action first, then reveal
advanced options when the user needs them.
- Build responsive UIs by default. At minimum, design and verify mobile and
desktop layouts rather than assuming one fixed window size.
- Boxes are for grouping related things. If everything is inside a box, boxes
stop communicating structure and make the interface feel noisy.
- Prefer specific, outcome-oriented actions over generic labels. For example,
"View 12 results" is usually better than "Search" once the result count is
known.
- Empty states should explain what is missing, why it matters, and the smallest
next step the user can take.
## Widget Structure
- Keep Rust source files below 2,000 lines. Refactor into focused modules before
a file grows past that limit.
- Organize Rust code using production module boundaries: entrypoints, app state,
routes, screens, reusable widgets, effects/jobs/services, design-system code,
and test helpers should not collapse into one large file.
- Prefer the Fission CLI for project, target, packaging, and shell scaffolding
instead of hand-rolling platform boilerplate.
- For multi-platform apps, put the app state, reducers, routes, widgets,
services, design system, and i18n setup in a shared core. Keep each
feature-gated shell entrypoint thin: it should configure the shell and run the
shared core rather than duplicating app behavior per target.
- Prefer one reusable widget per file when introducing app UI.
- Model widgets as concrete structs and implement `From<YourWidget> for Widget`.
- Add semantics to every interactive widget and to semantic regions where they
improve navigation, testing, or accessibility.
- Use `#[fission_component]` for components that own retained local widget
state. Do not model retained UI state as ordinary mutable struct fields.
- Keep screen modules focused on app state, routing, effects, and composition.
Move reusable presentation pieces into widget modules.
- Avoid screen-level helper functions that build large `Widget` trees. If a UI
fragment is meaningful or reused, make it a named widget struct instead.
- Small private helper functions are acceptable for narrow formatting,
conversion, or leaf construction, but they should not hide reusable component
boundaries. Avoid functions that return `Widget` when a named widget struct
would represent the boundary better. Fission is a retained UI; building
meaningful UI fragments through anonymous functions can hide identity and
weaken optimizations.
Local-state component shape:
```rust
use fission::prelude::*;
#[fission_component]
pub struct DisclosureSection {
pub title: String,
#[local_state(default = false)]
open: bool,
}
#[fission_reducer(ToggleOpen)]
fn on_toggle_open(open: &mut bool) {
*open = !*open;
}
impl From<DisclosureSection> for Widget {
fn from(section: DisclosureSection) -> Widget {
let (ctx, _) = fission::build::current::<()>();
let open = section.open();
let toggle = ctx.bind_local(ToggleOpen, open.clone(), reduce!(on_toggle_open));
Column {
gap: Some(8.0),
children: widgets![
Button {
on_press: Some(toggle),
child: Some(Text::new(section.title).into()),
..Default::default()
},
if open.get() {
Text::new("The details are visible.").into()
} else {
Text::new("The details are hidden.").into()
},
],
..Default::default()
}
.into()
}
}
```
Use `fission::build::current::<()>()` only when the component is intentionally
state-agnostic and does not read app state. Use the concrete app state type when
the component reads `GlobalState`, reads environment values tied to that app, or
binds reducers that update app state.
## State, Reducers, Routing and Runtime Data
- Choose the smallest state bucket that matches the lifetime and ownership of
the data.
- Use `GlobalState` for durable app truth: product data, routing, sync,
persistence, user preferences, shared filters, and values read by distant
screens.
- Use `#[local_state]` for retained UI memory owned by one widget identity:
open/closed flags, isolated draft text, local selected tabs, hoverless
interaction state, or a counter-like local value.
- Use reducers on `GlobalState` when an action updates app data. Use
`ctx.bind_local(...)` when an action updates one local-state field.
- Use reducers for explicit state transitions. Match the dispatched action to the
reducer registration used by the widget.
- Do not store `BuildCtxHandle` or `ViewHandle` in structs, reducers, services,
async tasks, statics, or other long-lived places. They are build-scope handles.
- Do not mutate `GlobalState` during component conversion. Dispatch actions and
update state in reducers.
- Do not start network requests, file writes, or host operations during component
conversion. Request work through effects, jobs, services, capabilities, or
resources.
- For local-state components rendered from dynamic, reorderable, insertable, or
filterable data, assign stable widget identities with
`.id(WidgetId::explicit(...))`. Use a durable data id, not the list index.
- Do not pad production UI with fixture, mock, or demo data. Production paths
should render persisted data, an explicit empty state, or a clear error state.
- Keep fixture parsing, fixture environment variables, and test-only data inside
test infrastructure.
- When adding routed screens, verify the frontend route shape and any backend or
deep-link route shape intentionally match.
- Use Fission's native Router and RouterParams (https://fission.rs/reference/widgets/router/). This ensures routing behaves consistently across ios, android, windows, linux, mac, terminal, static and server rendered sites
## Design System
Fission supports Adobe's Design System Package (DSP). Use DSP JSON as the design
source, generate the Rust design-system type at build time, and read generated
tokens from the active Fission environment at runtime.
- Prefer a generated Fission design system and its tokens over hard-coded sizes,
colors, typography, spacing, radii, shadows, or component properties.
- Hard-coded visual values are acceptable only for narrow one-off geometry that
is not part of the app's design language.
Use these Fission defaults as the starting point for a custom design system:
- Copy `dsp.json` from
<https://github.com/fission-ui/fission/blob/main/crates/core/fission-theme/design/default/dsp.json>
into the app, for example at `design/dsp.json`.
- Copy `tokens.json` from
<https://github.com/fission-ui/fission/blob/main/crates/core/fission-theme/design/default/tokens.json>
into the app, for example at `design/tokens.json`.
- Customize the checked-in copies. Do not parse the upstream default files at
runtime.
- Follow the official design-system example:
<https://github.com/fission-ui/fission/tree/main/examples/todo-design-system>.
Add the code generator as a build dependency. Use the dependency source that
matches this repository, and keep it version-aligned with the Fission runtime.
Do not leave a path dependency in place unless that path exists in this checkout.
Outside the Fission source workspace, prefer the published crate pinned to the same Fission revision used by the app:
```toml
[build-dependencies]
fission-design-system-codegen = { version = "0.8.0" }
```
Generate a typed design system from the copied DSP file in `build.rs`:
```rust
use std::path::PathBuf;
fn main() {
println!("cargo:rerun-if-changed=design/dsp.json");
println!("cargo:rerun-if-changed=design/tokens.json");
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let dsp_path = manifest_dir.join("design/dsp.json");
fission_design_system_codegen::generate(fission_design_system_codegen::Config {
dsp_path,
out_file: "app_design_system.rs".into(),
type_name: "AppDesignSystem".into(),
crate_path: "fission::theme".into(),
})
.expect("failed to generate AppDesignSystem from design/dsp.json");
}
```
Include the generated type and install it on app startup:
```rust
use fission::prelude::*;
include!(concat!(env!("OUT_DIR"), "/app_design_system.rs"));
#[derive(Debug, Clone, PartialEq, Eq)]
struct AppState {
theme_mode: DesignMode,
}
impl Default for AppState {
fn default() -> Self {
Self {
theme_mode: DesignMode::Light,
}
}
}
impl GlobalState for AppState {}
fn main() -> anyhow::Result<()> {
DesktopApp::<AppState, _>::new(App)
.with_design_system::<AppDesignSystem>(DesignMode::Light)
.with_sync_env(|state: &AppState, env: &mut Env| {
env.theme = AppDesignSystem::theme(state.theme_mode);
})
.run()
}
```
Inside widgets, read colors, spacing, typography, radii, and component styling
from `view.env().theme`:
```rust
impl From<App> for Widget {
fn from(_app: App) -> Widget {
let (_ctx, view) = fission::build::current::<AppState>();
let tokens = &view.env().theme.tokens;
Container::new(Text::new("Dashboard"))
.bg(tokens.colors.background)
.padding_all(tokens.spacing.xl)
.into()
}
}
```
Do not guess token names. Inspect the generated Rust output or the copied DSP
and tokens files before wiring new design values.
## Theme and Locale Synchronization
Theme and locale are app-wide presentation inputs. Keep user-controlled choices
in `GlobalState`, then mirror them into `Env` with `.with_sync_env(...)`.
```rust
DesktopApp::<AppState, _>::new(App)
.with_env(create_env()?)
.with_design_system::<AppDesignSystem>(DesignMode::Light)
.with_sync_env(|state: &AppState, env: &mut Env| {
env.locale = state.locale.clone();
env.theme = AppDesignSystem::theme(state.theme_mode);
})
.run()
```
Use `.with_sync_env(...)` only for app-wide presentation inputs such as theme,
locale, and similar environment values. Do not use it for network loading, file
writes, job startup, or ordinary domain data.
## Internationalization
Follow the Fission theming and i18n guide:
<https://github.com/fission-ui/fission/blob/main/documentation/content/docs/guides/theming-and-i18n.mdx>.
- Include an i18n setup from the start with at least two languages. Always
include English. If the system default language is not English, add that
language too; if it is English, add Spanish as the second language.
- Do not hard-code user-facing text inside reusable widgets.
- Use stable translation keys based on meaning, not on the current layout.
- Keep translations in checked-in files and embed them with `include_str!` unless
the app has a stronger reason to load translations dynamically.
- Parse translation files into `HashMap<String, String>`, wrap each map in a
`TranslationBundle`, and add it to `env.i18n`.
- If the user can change language, store the current locale in `GlobalState` and
mirror it to `env.locale` in `.with_sync_env(...)`.
- Test long translated strings, not only short English labels.
Example translation files:
```yaml
# i18n/en-US.yaml
settings.title: "Settings"
settings.theme.light: "Light"
settings.theme.dark: "Dark"
settings.language.english: "English"
settings.language.spanish: "Spanish"
```
```yaml
# i18n/es-ES.yaml
settings.title: "Configuracion"
settings.theme.light: "Claro"
settings.theme.dark: "Oscuro"
settings.language.english: "Ingles"
settings.language.spanish: "Espanol"
```
Add a YAML parser if the app uses YAML translation files:
```toml
[dependencies]
serde_yaml = "0.9"
```
Load bundles into `Env`:
```rust
use std::collections::HashMap;
use fission::i18n::{Locale, TranslationBundle};
use fission::prelude::*;
fn load_bundle(locale: &str, raw_yaml: &str) -> anyhow::Result<TranslationBundle> {
let messages: HashMap<String, String> = serde_yaml::from_str(raw_yaml)?;
Ok(TranslationBundle {
locale: Locale::from(locale),
messages,
})
}
fn create_env() -> anyhow::Result<Env> {
let mut env = Env::default();
env.i18n
.add_bundle(load_bundle("en-US", include_str!("../i18n/en-US.yaml"))?);
env.i18n
.add_bundle(load_bundle("es-ES", include_str!("../i18n/es-ES.yaml"))?);
Ok(env)
}
```
Render translated text with keys instead of literals:
```rust
Text::new(TextContent::Key("settings.title".into()))
```
For SSR and Static site targets, seed `Env` with the same bundles. Resolve the
locale from request or build context in the shell instead of making individual
widgets guess.
## Source References
- Design-system example directory:
<https://github.com/fission-ui/fission/tree/main/examples/todo-design-system>
- Design-system example `build.rs`:
<https://github.com/fission-ui/fission/blob/main/examples/todo-design-system/build.rs>
- Design-system example `main.rs`:
<https://github.com/fission-ui/fission/blob/main/examples/todo-design-system/src/main.rs>
- Design-system guide:
<https://github.com/fission-ui/fission/blob/main/documentation/content/docs/guides/design-system.mdx>
- Theming and i18n guide:
<https://github.com/fission-ui/fission/blob/main/documentation/content/docs/guides/theming-and-i18n.mdx>
- State, handles, and providers guide:
<https://github.com/fission-ui/fission/blob/main/documentation/content/docs/guides/state-handles-and-providers.mdx>
- Default DSP package:
<https://github.com/fission-ui/fission/blob/main/crates/core/fission-theme/design/default/dsp.json>
- Default token file:
<https://github.com/fission-ui/fission/blob/main/crates/core/fission-theme/design/default/tokens.json>
## Validation
- Run formatting before handing off code changes.
- Run the narrowest compile or test command that exercises the changed app,
widget crate, route, or target shell.
- Use the Fission LiveTest API for end-to-end UI tests:
<https://fission.rs/docs/cookbook/write-a-live-ui-test/>.
- For visual UI work, use a client such as `curl` to send `/cmd` requests to the
LiveTest API when needed, drive the app like a user, take screenshots, and
inspect those screenshots before calling the UI production-ready.
- For UI changes, verify a real rendered target when possible, and check mobile
and desktop layouts when the screen is responsive.
- If docs or configuration target names are changed, keep terminology consistent
with Fission's public target names: `macOS`, `Windows`, `Linux`, `Web`,
`Android`, `iOS`, `Terminal`, `Static site`, and `SSR`.