Button

Button is Fission's standard pressable control for a single, explicit user intent.
Use it when the user is asking the app to do something discrete and meaningful: save a draft, open a dialog, submit a form, retry a failed request, or move to the next step in a flow. In Fission, a button does not perform that work by itself. Instead, it dispatches an action, the reducer updates app state, and the next conversion pass renders whatever the new state should look like.
That design is important for production user interface. It keeps buttons honest. A button is not a hidden bundle of business logic. It is the visible control that expresses intent and sends that intent into the normal action and reducer loop.
If the user should toggle a persistent true-or-false value, reach for Checkbox or Switch instead. If the user is navigating to another screen or location, a Link or router-driven control may fit better. A button is best when the interaction means "do this now."

How a button fits into the update loop

The full flow looks like this:
1.
component conversion creates a Button and gives it an on_press action.
2.
The user presses the button with a pointer, keyboard, or accessibility action.
3.
The runtime dispatches that action.
4.
The bound reducer runs and updates GlobalState.
5.
Fission converts the user interface again from the updated state.
The key term in the middle is ActionEnvelope. That is just Fission's packaged form of an action: the action type plus its encoded payload. Most app authors do not build an ActionEnvelope by hand. You usually create it with with_reducer!(...), which both:
•
registers the reducer that should handle the action, and
•
returns the packaged action value the button will dispatch.

Example

This example shows a realistic "Save draft" button from a compose screen. The important product rules are:
•
the user should not be able to save an empty draft
•
the user should not be able to submit the same save repeatedly while a save is already in flight
•
the button label should reflect the current state
use fission::prelude::*;

#[fission_action]
struct SaveDraft;

struct ComposerState {
    title: String,
    body: String,
    is_saving: bool,
}

fn on_save_draft(
    state: &mut ComposerState,
    _action: SaveDraft,
    _ctx: &mut ReducerContext<ComposerState>,
) {
    if state.is_saving || state.title.trim().is_empty() || state.body.trim().is_empty() {
        return;
    }

    state.is_saving = true;
}

struct SaveDraftButton;

impl From<SaveDraftButton> for Widget {
    fn from(component: SaveDraftButton) -> Self {
        let (ctx, view) = fission::build::current::<ComposerState>();
        let save_draft = with_reducer!(ctx, SaveDraft, on_save_draft);

        let can_save = !view.state().is_saving
            && !view.state().title.trim().is_empty()
            && !view.state().body.trim().is_empty();

        Button {
            child: Some(
                Text::new(if view.state().is_saving {
                    "Saving..."
                } else {
                    "Save draft"
                })
                .into(),
            ),
            on_press: Some(save_draft),
            variant: ButtonVariant::Filled,
            disabled: !can_save,
            ..Default::default()
        }
        .into()
    }
}
Even if you are new to Rust, the structure is more important than every keyword.
The #[fission_action] line is framework and serialization scaffolding. It tells Fission how to recognize and move the SaveDraft action through the runtime without making you repeat the same derives on every action type.
fn on_save_draft(...) is the reducer function. It receives mutable app state, the action value, and a ReducerContext. This reducer is deliberately small. It does not try to save to disk or call a server directly. It only updates state to say "a save is now in progress." That keeps the button path deterministic and easy to test.
impl From<SaveDraftButton> for Widget is Rust's way of saying "this type converts into authored UI." Inside that conversion, view.state() gives read-only access to the current state, and with_reducer!(...) creates the action the button will dispatch.
The with_reducer!(ctx, SaveDraft, on_save_draft) call connects the concrete action value to the reducer that handles it without forcing the example to spell out Handler<ComposerState, SaveDraft> by hand.
Finally, notice the disabled: !can_save line. That is good production behavior. The app does not wait until after a bad press to discover the action should be rejected. It communicates availability clearly in the user interface and prevents double submission while saving is already underway.

Field reference

Field
Type
Meaning
Notes and default behavior
id
Option<WidgetId>
Stable identity for this button node.
Defaults to None. Set it when tests, diagnostics, or related runtime features need a predictable identity.
child
Option<Widget>
The visible content inside the button.
Defaults to None. In real apps, provide a clear text label, icon, or both.
on_press
Option<ActionEnvelope>
Action dispatched when the user activates the button.
Defaults to None. Without it, the button is usually only a visual surface and does not expose normal button behavior.
semantics
Option<Semantics>
Accessibility metadata override.
Defaults to None. Most text-labeled buttons can use the default role; icon-only buttons need an explicit label.
focus_policy
FocusPolicy
Whether pointer activation takes focus.
Defaults to FocusOnPointer. Use PreserveCurrentOnPointer for editor toolbars that must not steal the current text focus.
width
Option<f32>
Fixed width in layout points.
Defaults to None. Use sparingly; many buttons size better from content or parent layout.
height
Option<f32>
Fixed height in layout points.
Defaults to None. When omitted, the themed button height becomes the minimum height.
min_width
Option<f32>
Minimum width constraint.
Defaults to None. Useful for action rows where buttons should not collapse too narrowly.
max_width
Option<f32>
Maximum width constraint.
Defaults to None. Useful when a long label should wrap elsewhere instead of producing an oversized control.
flex_grow
f32
How much extra space the button can take in a flex layout.
Defaults to 0.0. Set this when sibling buttons should stretch to fill a row.
flex_shrink
f32
How willing the button is to shrink in a tight flex layout.
Defaults to 1.0. This lets the button participate in tighter layouts unless you override it.
padding
Option<[f32; 4]>
Inner padding in [left, right, top, bottom] order.
Defaults to the theme's button padding. Note that this order is not cascading style sheets (CSS) shorthand order.
variant
ButtonVariant
Visual emphasis level.
Defaults to ButtonVariant::Filled. Choose a variant that matches the importance of the action.
size
ComponentSize
Design-system density slot.
Defaults to Md; the default package defines Sm, Md, Lg, and Xl.
background_fill
Option<Fill>
Manual background fill override.
Defaults to None. Best used when a theme-level choice is not enough.
text_color
Option<IrColor>
Manual text color override.
Defaults to None. Applied automatically to direct Text children; custom child trees handle their own text styling.
content_align
ButtonContentAlign
Horizontal alignment for the child content.
Defaults to ButtonContentAlign::Center. Use Start for button rows that should read more like list items or menu actions.
disabled
bool
Whether the button is currently unavailable.
Defaults to false. Disabled buttons keep their visual role but do not dispatch on_press.
motion
Option<ButtonMotion>
Optional hover, press, scale, or ripple motion.
Defaults to None, so a button has no button-owned animation unless you opt in.
style
Option<ButtonStyleOverride>
Targeted visual overrides applied after the active design-system recipe.
Can override fill, text color, border, width, maximum width, height, padding, radius, the complete shadow stack, and typography. Dedicated Button fields win where both APIs provide a value.

Anatomy and design-system behavior

The active design system is the authority for the button surface. Button resolves the selected hierarchy, size, and live interaction state from theme.components.button, then consumes the resulting background, text color, border, radius, dimensions, padding, typography, and outer or inset shadows. A direct Text child receives the resolved typography and color. A custom child tree keeps responsibility for styling its own content.
Filled, Outline, and Ghost remain concise compatibility names for primary, neutral-secondary, and neutral-tertiary treatment. The explicit hierarchy variants are Primary, SecondaryColor, SecondaryGray, TertiaryColor, TertiaryGray, LinkColor, LinkGray, and Destructive. Use hierarchy to communicate importance and consequence; do not use a destructive treatment as an arbitrary accent color.
The default, hover, active, focus, and disabled recipes are resolved from the same component definition. Focus is layered over the current pointer state, so a focused-and-hovered control keeps both pieces of feedback instead of losing one. ButtonStyleOverride is a local escape hatch for fill, text, border, geometry, shadows, and typography; shadows: Some(Vec::new()) explicitly asks for a flat surface. Dedicated Button fields such as width, height, padding, background_fill, and text_color take precedence where supplied.

Layout and interaction behavior

Layout-wise, the button builds a box with themed minimum height, optional explicit sizing, and internal padding. Its child is then aligned inside that box according to content_align. In practice, this means you usually choose button width from parent layout and choose alignment from product intent, instead of manually positioning the label yourself.
Interaction-wise, hover, press, and focus are runtime-owned state. You should not copy those transient details into GlobalState. What belongs in app state are product facts such as is_saving, has_unsaved_changes, or can_retry_upload. The button reads those facts and turns them into a label, a variant, or a disabled state.
When on_press is present, the button exposes a focusable button semantic node and a default action. Tab reaches it, and Enter or Space dispatches that action. A disabled button is omitted from focus traversal and has no default action. A button with neither an action nor custom semantics remains a visual surface rather than pretending to be interactive.

Responsive, target, and motion behavior

Button does not impose a fixed width. It takes its minimum height from the selected size recipe and otherwise participates in the parent layout. Prefer parent constraints for full-width mobile actions, and test long or translated labels at the narrowest supported width instead of fixing a width around one English label. ButtonContentAlign::Start and End are logical alignments and mirror with the active layout direction; paragraph bidi and shaping remain a separate text-direction choice.
The shared semantics and paint model lower across interactive targets. Static site and plain SSR output do not run client reducers without an interactive island. Button motion is opt-in through motion. Regardless of the selected preset, Env::motion_preference = MotionPreference::Reduced resolves its motion tracks immediately and suppresses the ripple layer. The Web canvas shell reads and follows prefers-reduced-motion unless an application sync_env callback overrides it; other interactive shells and tests honor an Env value supplied through with_env or with_sync_env. Static site and SSR output emit immediate final-state animation and transitions for the same browser preference.

Accessibility and production guidance

A strong production button is clear, specific, and honest.
Use labels that tell the user what will happen, such as "Save draft," "Delete file," or "Send invite," instead of vague text like "OK" when the action has a concrete effect. When the button has only an icon, add an explicit semantic label so screen readers do not encounter an unnamed control.
Disable buttons when the action is truly unavailable, not merely inconvenient. That is especially useful for incomplete forms, in-flight submissions, and destructive actions that should wait on a confirmation state. At the same time, do not use disabled buttons to hide validation forever. The surrounding user interface should still explain why the action is unavailable.
Avoid storing visual press state in your reducer. Fission already tracks focus, hover, and press at runtime. Your reducer should care about business state and side-effect requests, not whether the pointer is currently down.
Be careful with one-off style overrides. If every screen hand-tunes button color and padding, the product quickly becomes inconsistent. Prefer theme-level design decisions and use variant first. Reach for background_fill or text_color only when the product truly needs a special case.
Finally, guard repeated actions in both places that matter: in the user interface and in the reducer. The user interface should disable an unavailable button. The reducer should still refuse unsafe repeated actions if one somehow arrives. That combination gives you a calm interface and a robust state model.

Production checklist

For Button, review the fields that change behavior before treating the widget as finished: id, child, on_press, semantics, width, height. The goal is to make the product rule visible in state and actions, not hidden inside ad-hoc construction code.
•
Bind on_press to explicit reducer actions and test that the reducer handles unavailable, duplicate, or invalid input safely.
•
When child widgets are generated from data, give reordered or filtered rows an explicit WidgetId so retained local state and scroll behavior do not drift between items.
•
Set id only when identity must be stable across filtering, reordering, diagnostics, or tests; otherwise let Fission derive identity from structure.
•
Use semantics when visible content alone is not enough for assistive technology, especially for icon-only controls or custom surfaces.
•
Prefer parent layout and design-system sizing first; use explicit size or spacing fields when the product layout requires that constraint on this widget specifically.
•
Add at least one component or harness test that confirms the visible text, semantic role, action dispatch, and layout constraint that matter for this widget in context.
If a screen starts repeating the same Button setup, extract a named component around this widget. That keeps the reference API small while making product code easier to read and safer for generated code to copy.
Previous
ZStack