Button
A clear call to action, with bound labels, variants and keyboard focus.

use blinc_cn as cn;
use blinc_cn::button;
use blinc_core::reactive::{Signal, computed};
use blinc_layout::{binding::IntoReactive, prelude::*};
pub fn button_demo(clicks: Signal<u32>, disabled: Signal<bool>) -> Div {
div()
.class("demo-wide")
.class("demo-stack--center")
.child(
button("Click me")
.disabled(disabled)
.on_click(move |_| clicks.set(clicks.get() + 1))
.build_component()
.id("demo-action"),
)
.child(
bound_text(computed(move |graph| {
format!("Clicked {} times", graph.get(clicks).unwrap_or(0))
}))
.class("demo-caption"),
)
.child(
div()
.class("demo-row")
.child(
cn::button("Secondary")
.variant(cn::ButtonVariant::Secondary)
.disabled(disabled),
)
.child(
cn::button("Outline")
.variant(cn::ButtonVariant::Outline)
.disabled(disabled),
)
.child(cn::button("Ghost").variant(cn::ButtonVariant::Ghost).disabled(disabled)),
)
.child(
div()
.class("demo-row")
.child(
cn::button("Remove")
.variant(cn::ButtonVariant::Destructive)
.disabled(disabled),
)
.child(
cn::button("Documentation")
.variant(cn::ButtonVariant::Link)
.disabled(disabled),
),
)
}
fn bound_text(value: impl IntoReactive<String>) -> Text {
Text::bound(value.into_reactive())
}
@layer components {
.blinc-button {
background: var(--primary);
color: var(--text-inverse);
transition:
background-color var(--duration-fast) var(--ease-state),
color var(--duration-fast) var(--ease-state),
border-color var(--duration-fast) var(--ease-state);
}
.blinc-button:hover:not(:disabled) {
background: var(--primary-hover);
}
.blinc-button:active:not(:disabled) {
background: var(--primary-active);
}
.blinc-button:disabled {
background: var(--input-bg-disabled);
color: var(--text-tertiary);
}
.cn-button {
font-family: var(--font-sans);
}
.cn-button {
letter-spacing: var(--tracking-tight);
}
.cn-button {
border-radius: var(--radius-default);
cursor: pointer;
transition:
background-color var(--duration-fast) var(--ease-state),
color var(--duration-fast) var(--ease-state),
border-color var(--duration-fast) var(--ease-state),
box-shadow var(--duration-fast) var(--ease-state),
transform var(--duration-faster) var(--ease-state);
}
.cn-button:disabled {
cursor: not-allowed;
}
.cn-button:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
.cn-button:active:not(:disabled) {
transform: scale(0.97, 0.97);
}
.cn-button--link:active:not(:disabled) {
transform: none;
opacity: 0.8;
}
.cn-button--primary {
background: var(--primary);
color: var(--text-inverse);
box-shadow: var(--shadow-md);
}
.cn-button--primary:hover:not(:disabled) {
background: var(--primary-hover);
}
.cn-button--primary:active:not(:disabled) {
background: var(--primary-active);
}
.cn-button--secondary {
background: var(--secondary);
color: var(--text-inverse);
box-shadow: var(--shadow-md);
}
.cn-button--secondary:hover:not(:disabled) {
background: var(--secondary-hover);
}
.cn-button--secondary:active:not(:disabled) {
background: var(--secondary-active);
}
.cn-button--destructive {
background: var(--error);
color: var(--text-inverse);
box-shadow: var(--shadow-md);
}
.cn-button--destructive:hover:not(:disabled) {
background: color-mix(in srgb, var(--error) 90%, black);
}
.cn-button--destructive:active:not(:disabled) {
background: color-mix(in srgb, var(--error) 85%, black);
}
.cn-button--outline {
background: transparent;
color: var(--text-primary);
border: 1px solid var(--border);
box-shadow: var(--shadow-sm);
}
.cn-button--ghost {
background: transparent;
color: var(--text-primary);
}
.cn-button--outline:hover:not(:disabled),
.cn-button--ghost:hover:not(:disabled) {
background: color-mix(in srgb, var(--text-primary) 5%, transparent);
}
.cn-button--outline:active:not(:disabled),
.cn-button--ghost:active:not(:disabled) {
background: color-mix(in srgb, var(--text-primary) 10%, transparent);
}
.cn-button--link {
background: transparent;
color: var(--primary);
}
.cn-button:disabled {
background: var(--input-bg-disabled);
color: var(--text-tertiary);
border: 1px solid var(--border-secondary);
box-shadow: none;
transform: none;
}
.cn-button--sm {
border-radius: var(--radius-sm);
}
.cn-button--md {
border-radius: var(--radius-default);
}
.cn-button--lg {
border-radius: var(--radius-lg);
}
.cn-button--icon {
border-radius: var(--radius-default);
}
}
Usage and properties
The label can be a value, Signal or Computed. A disabled button ignores activation. Choose variants and sizes when building the component.
Use the installation guide to add blinc_cn and its component stylesheet. Create the fixture bindings once, and return the demo from your UI builder. Values marked Bound accept Signal/Computed sources where the API supports them; structural options configure the component at construction.
| Property | Type | Update model | This example |
|---|---|---|---|
label | String | Bound | Click me |
disabled | bool | Bound | false |
variant | ButtonVariant | Structural | Primary |
size | ButtonSize | Structural | Medium |
on_click | Fn(&EventContext) | Callback | Increment the click count |
Keyboard and interaction
:hover, :active, :disabled and :focus-visible. Tab reaches the button; Enter or Space activates it.
Customizing it
Add application CSS after the default component layer. These selectors are the restyling surface for this component:
.cn-button.cn-button--primary.cn-button--secondary.cn-button--outline.cn-button--ghost.cn-button--link.cn-button--destructive.cn-button--sm.cn-button--md.cn-button--lg.cn-button--icon
Theme and component variables: --primary, --primary-hover, --primary-active, --text-inverse, --focus-ring, --radius-default.
Built-in themes
The Live theme control installs Blinc's own theme bundles, including their colours, focus rings, corner shapes and motion tokens.
pub fn theme_bundle_for(name: &str) -> blinc_theme::ThemeBundle {
match name {
"restrained" => blinc_theme::RestrainedTheme::bundle(),
"expressive" => blinc_theme::ExpressiveTheme::bundle(),
_ => blinc_theme::HybridTheme::bundle(),
}
.with_css(blinc_cn::cn_styles::CN_STYLES)
}Download the default widget and component stylesheets. Sample assets and licenses are available in the asset guide.
Documentation
From the canonical component guide.
Buttons trigger actions.
Basic Usage
use blinc::cn::prelude::*;
button("Click me")
.on_click(|_| println!("Clicked!"))
The label takes a string or a signal, and is patched in place when it changes.
Variants
// Primary (default) - main actions
button("Save").variant(ButtonVariant::Primary)
// Secondary - alternative actions
button("Cancel").variant(ButtonVariant::Secondary)
// Destructive - dangerous actions
button("Delete").variant(ButtonVariant::Destructive)
// Outline - bordered
button("Edit").variant(ButtonVariant::Outline)
// Ghost - minimal
button("More").variant(ButtonVariant::Ghost)
// Link - looks like a link
button("Learn more").variant(ButtonVariant::Link)
Sizes
button("Small").size(ButtonSize::Small)
button("Default").size(ButtonSize::Medium)
button("Large").size(ButtonSize::Large)
// Icon only (square)
button("").size(ButtonSize::Icon).icon(icons::SETTINGS)
// A size of your own: width, height
button("Wide").size(ButtonSize::Custom(160.0, 48.0))
With Icons
use blinc::icons::icons;
button("Settings").icon(icons::SETTINGS)
// After the label
button("Next")
.icon(icons::ARROW_RIGHT)
.icon_position(IconPosition::End)
Disabled
disabled takes a bool or a signal, so a button can follow a state:
button("Disabled").disabled(true)
button("Save").disabled(&is_saving)
A disabled button does not click and takes the disabled look.
Styling
A button's look is stylesheet. Its fill, text colour, border and shadow, and
how they change on hover, press and while disabled, are the .cn-button
rules of CN_STYLES, with a transition, so a button eases between states.
Each variant and size has a class to hang your own rules on:
| Class | Matches |
|---|---|
.cn-button | every cn button |
.cn-button--primary, --secondary, --destructive, --outline, --ghost, --link | the variant |
.cn-button--sm, --md, --lg, --icon | the size |
/* A brand colour for primary buttons, eased on hover. Your rules win over
cn's whatever their specificity. */
.cn-button--primary { background: #6d28d9; }
.cn-button--primary:hover:not(:disabled) { background: #7c3aed; }
.cn-button--primary:active:not(:disabled) { background: #5b21b6; }
/* One button of your own */
.danger { background: #c0392b; transition: background-color 150ms ease; }
.danger:disabled { background: #555; color: #999; }
button("Delete").class("danger")
:hover:not(:disabled) keeps the rule off a disabled button. :active
matches while the pointer is down on the button or anything inside it, as on
the web. Text and icons inside take the button's color, so a rule that
changes it changes them too.
.color(..) on the builder fixes the text colour in every state; a fluent
value wins over cn's rules, as an inline style does on the web.
Button Groups
div()
.flex_row()
.gap(2.0)
.child(button("Save").variant(ButtonVariant::Primary))
.child(button("Cancel").variant(ButtonVariant::Outline))
gap takes 4px units, so gap(2.0) is 8px.
Examples
Icon Button
button("")
.size(ButtonSize::Icon)
.variant(ButtonVariant::Ghost)
.icon(icons::X)
.on_click(|_| close_dialog())
A Button That Follows a Signal
let saving = ctx.use_signal(false);
button("Save")
.disabled(saving)
.on_click(move |_| saving.set(true))
API Reference
| Method | Type | Default | Description |
|---|---|---|---|
variant | ButtonVariant | Primary | Visual style |
size | ButtonSize | Medium | Small, Medium, Large, Icon or Custom(w, h) |
disabled | bool or signal | false | Disable interaction |
icon | &str | none | An icon from blinc::icons::icons |
icon_position | IconPosition | Start | Start or End |
icon_size | f32 | label size + 2 | Icon size in px |
color | Color | by variant | Text colour in every state |
on_click | Fn(&EventContext) | none | Called when clicked |
class, id | &str | none | Selectors for your own rules |
Platforms and version
The component API is available in Blinc 0.6.0. The Live example runs in a dedicated worker and requires a compatible GPU surface. Preview and Code remain available when Live cannot start. The canvas has a descriptive label; a full screen-reader DOM mirror is not provided. The gallery integration is based on revision e2e55c24e078.
Recordings and live demos use the default Hybrid theme with bundled Noto Sans. The recording uses a 560 × 320 logical viewport at twice that resolution. Live follows its container and actual display density. Source base revision · Exact build source · Artifact versions and checksums · Font license.