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." The full flow looks like this:
component conversion creates a Button and gives it an on_press action.
The user presses the button with a pointer, keyboard, or accessibility action.
The runtime dispatches that action.
The bound reducer runs and updates GlobalState.
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
| | | Notes and default behavior |
|---|
| | Stable identity for this button node. | Defaults to None. Set it when tests, diagnostics, or related runtime features need a predictable identity. |
| | The visible content inside the button. | Defaults to None. In real apps, provide a clear text label, icon, or both. |
| | 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. |
| | Accessibility metadata override. | Defaults to None. Most text-labeled buttons can use the default role; icon-only buttons need an explicit label. |
| | Whether pointer activation takes focus. | Defaults to FocusOnPointer. Use PreserveCurrentOnPointer for editor toolbars that must not steal the current text focus. |
| | Fixed width in layout points. | Defaults to None. Use sparingly; many buttons size better from content or parent layout. |
| | Fixed height in layout points. | Defaults to None. When omitted, the themed button height becomes the minimum height. |
| | Minimum width constraint. | Defaults to None. Useful for action rows where buttons should not collapse too narrowly. |
| | Maximum width constraint. | Defaults to None. Useful when a long label should wrap elsewhere instead of producing an oversized control. |
| | 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. |
| | 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. |
| | 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. |
| | | Defaults to ButtonVariant::Filled. Choose a variant that matches the importance of the action. |
| | Design-system density slot. | Defaults to Md; the default package defines Sm, Md, Lg, and Xl. |
| | Manual background fill override. | Defaults to None. Best used when a theme-level choice is not enough. |
| | Manual text color override. | Defaults to None. Applied automatically to direct Text children; custom child trees handle their own text styling. |
| | 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. |
| | Whether the button is currently unavailable. | Defaults to false. Disabled buttons keep their visual role but do not dispatch on_press. |
| | Optional hover, press, scale, or ripple motion. | Defaults to None, so a button has no button-owned animation unless you opt in. |
| 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.