Feedback Components

Alerts, badges, progress indicators and toasts report application status. Install cn_bundle() as described in Getting Started. Examples use these imports and the application builder's ctx:

#![allow(unused)]
fn main() {
use blinc::cn::prelude::*;
use blinc::layout::prelude::{div, text};
}

Alert

alert(message) displays a single message. alert_box() adds a title and description:

#![allow(unused)]
fn main() {
alert("Your changes are ready to save.")
}
#![allow(unused)]
fn main() {
alert_box()
    .title("Heads up!")
    .description("Review the details before continuing.")
}

Variants

Both accept Default, Success, Warning or Destructive:

#![allow(unused)]
fn main() {
div().flex_col().gap(2.0)
    .child(alert("Saved").variant(AlertVariant::Success))
    .child(alert("Low disk space").variant(AlertVariant::Warning))
    .child(alert_box()
        .variant(AlertVariant::Destructive)
        .title("Upload failed")
        .description("Check your connection and try again."))
}

Messages, titles and descriptions can bind to Signals or Computeds. The root is .cn-alert or .cn-alert-box, with a variant class such as .cn-alert--success or .cn-alert--error. Restyle those rules rather than picking colours in the component builder.

Badge

A badge has a semantic variant and a visual treatment:

#![allow(unused)]
fn main() {
div().flex_row().gap(2.0)
    .child(badge("New"))
    .child(badge("3").variant(BadgeVariant::Secondary))
    .child(badge("Error").variant(BadgeVariant::Destructive))
    .child(badge("Beta").style(BadgeStyle::Outline))
}

Variants are Default, Secondary, Success, Warning and Destructive. Styles are Soft (the default), Solid and Outline:

#![allow(unused)]
fn main() {
badge("Done").variant(BadgeVariant::Success).style(BadgeStyle::Solid)
}

With an Icon

.icon(..) accepts an element builder:

#![allow(unused)]
fn main() {
badge("Verified")
    .variant(BadgeVariant::Success)
    .icon_position(IconPosition::Start)
    .icon(icon(icons::CHECK).size_px(12.0))
}

A Bound Label

Convert a Signal or Computed with IntoReactive:

#![allow(unused)]
fn main() {
use blinc::core::reactive::computed;
use blinc::layout::binding::IntoReactive;

let count = ctx.use_signal(3usize);
let shown = computed(move |graph| format!("{} new", graph.get(count).unwrap_or(0)));

badge("").reactive_label(shown.into_reactive())
}

The root is .cn-badge. Its fill, border and text colour come from .cn-badge--{style}-{variant}, such as .cn-badge--soft-success. A custom class supports application overrides:

#![allow(unused)]
fn main() {
badge("Done").variant(BadgeVariant::Success).class("my-badge")
}
.my-badge { border-radius: 4px; }
.cn-badge--soft-success { background: #052e16; border-color: #16a34a; color: #4ade80; }

Progress

Values are in the 0..=100 range:

#![allow(unused)]
fn main() {
progress(75.0)
}

Bound to a Signal

An f32, Signal<f32> or Computed<f32> can drive the bar. Bound values change in place and ease by the transition on .cn-progress-bar:

#![allow(unused)]
fn main() {
let pct = ctx.use_signal(0.0_f32);

div()
    .child(progress(pct).w(300.0))
    .child(button("Advance").on_click(move |_| {
        pct.update(|value| (value + 10.0).min(100.0));
    }))
}

Indeterminate

For work of unknown length:

#![allow(unused)]
fn main() {
progress_indeterminate().w(300.0)
}

The track is .cn-progress, with data-size and, when applicable, data-indeterminate. The bar is .cn-progress-bar. Their colours read --cn-progress-track and --cn-progress-bar; the transition and cn-progress-pulse keyframes control motion.

With a Label

#![allow(unused)]
fn main() {
use blinc::core::reactive::computed;

let pct = ctx.use_signal(0.0_f32);
let shown = computed(move |graph| format!("{}%", graph.get(pct).unwrap_or(0.0) as i32));

div().flex_col().gap(1.0)
    .child(div().flex_row().justify_between()
        .child(label("Uploading"))
        .child(label(shown)))
    .child(progress(pct))
}

Spinner

#![allow(unused)]
fn main() {
spinner().size(SpinnerSize::Medium)
}

Sizes are Small (16px), Medium (24px, the default) and Large (32px). The track is .cn-spinner__track; .cn-spinner__arc turns by the cn-spinner-turn keyframes. Set speed through CSS:

.slow-spinner .cn-spinner__arc { animation-duration: 2000ms; }

Beside a Busy Button

A button binds its label and disabled prop. Compose a spinner while it is busy:

#![allow(unused)]
fn main() {
use blinc::core::reactive::computed;

let busy = ctx.use_signal(false);
let caption = computed(move |graph| {
    if graph.get(busy).unwrap_or(false) { "Saving" } else { "Save" }.to_string()
});

div().flex_row().items_center().gap(2.0)
    .child(button(caption).disabled(busy).on_click(move |_| busy.set(true)))
    .show(busy, || div().child(spinner().size(SpinnerSize::Small)))
    .child(button("Finish").on_click(move |_| busy.set(false)))
}

In an application, the request completion callback clears busy; the Finish button makes that completion explicit in this example. There is no button .loading(...) prop.

Skeleton

A skeleton pulses by the cn-skeleton-pulse keyframes:

#![allow(unused)]
fn main() {
div().flex_col().gap(2.0)
    .child(skeleton().w(200.0).h(20.0))
    .child(skeleton_circle(48.0))
}

The root is .cn-skeleton; a circle also has .cn-skeleton--circle. Override the animation through a custom class:

.my-skeleton { animation-duration: 3000ms; }

Card Skeleton

#![allow(unused)]
fn main() {
card().child(card_content().child(
    div().flex_col().gap(2.0)
        .child(skeleton().w(150.0).h(24.0))
        .child(skeleton().w(200.0).h(16.0))
        .child(skeleton().w_full().h(100.0)),
))
}

Toast

Show notifications from an event or application callback:

#![allow(unused)]
fn main() {
button("Notify").on_click(|_| {
    toast("Success")
        .description("Your changes have been saved.")
        .show();
})
}

toast_success, toast_warning and toast_error select variants directly. .duration_ms(..) sets the lifetime; .persistent() waits for dismissal. .corner(..) sets the tray corner. .show() returns a handle whose .dismiss() method removes that toast.

With an Action

#![allow(unused)]
fn main() {
let undone = ctx.use_signal(false);

button("Create event").on_click(move |_| {
    toast("Event created")
        .description("The event is on your calendar.")
        .action("Undo", move || undone.set(true))
        .duration_ms(5000)
        .show();
})
}

Styling and Motion

The root is .cn-toast with data-variant: default, success, warning or destructive. Parts include .cn-toast-icon, .cn-toast-title, .cn-toast-description and .cn-toast-close. Set --cn-toast-bg and --cn-toast-border to override their defaults.

The wrapper is .cn-toast-motion-right or .cn-toast-motion-left, depending on its corner. It carries data-motion="enter" or "exit" and remains mounted until the exit completes:

.cn-toast-motion-right[data-motion="exit"] {
    animation: cn-slide-to-right 400ms var(--ease-in) forwards;
}

Examples

Loading State

Use a conditional region so content follows the Signal after construction:

#![allow(unused)]
fn main() {
let loading = ctx.use_signal(true);

div().flex_col().gap(3.0)
    .child(button("Toggle loading").on_click(move |_| loading.update(|value| !value)))
    .show_or(loading,
        || div().flex_col().items_center().gap(2.0)
            .child(spinner().size(SpinnerSize::Large))
            .child(text("Loading")),
        || div().child(text("Content is ready")),
    )
}

Form Submission Feedback

The application owns request state. Bind the button to it, and mount feedback through a computed condition. The completion button stands in for a request callback in this interactive example:

#![allow(unused)]
fn main() {
use blinc::core::reactive::computed;

let submitting = ctx.use_signal(false);
let message = ctx.use_signal(String::new());
let has_message = computed(move |graph| !graph.get(message).unwrap_or_default().is_empty());

div().flex_col().gap(3.0)
    .child(button("Submit").disabled(submitting).on_click(move |_| {
        message.set(String::new());
        submitting.set(true);
    }))
    .show(submitting, move || {
        div().flex_col().gap(2.0)
            .child(spinner())
            .child(button("Complete request").on_click(move |_| {
                submitting.set(false);
                message.set("Form submitted successfully.".to_string());
            }))
    })
    .show(has_message, move || {
        div().child(alert(message).variant(AlertVariant::Success))
    })
}

Signals update props or regions; neither example branches on a value read once at build time. Error feedback can use a separate error Signal and a Destructive alert, with the same binding pattern.

Notification Helpers

#![allow(unused)]
fn main() {
fn notify_success(message: &str) {
    blinc::cn::cn::toast_success("Success")
        .description(message)
        .duration_ms(5000)
        .show();
}

fn notify_error(message: &str) {
    blinc::cn::cn::toast_error("Error")
        .description(message)
        .duration_ms(10000)
        .show();
}
}