Building Reusable Components
This guide covers patterns for creating composable, reusable UI components in Blinc.
Use #[derive(BlincComponent)] on a named struct to generate a companion
NameProps type. Its constructor and fluent field setters accept
IntoReactive<T>, including Signal and Computed inputs. Forward these fields
to bindings and expose documented classes for CSS restyling. See the
derive guide for constructors, signal hooks and examples.
Component Patterns
Simple Function Components
The simplest pattern - a function returning an element:
#![allow(unused)] fn main() { fn card(title: &str) -> Div { div() .p(16.0) .rounded(12.0) .bg(Color::rgba(0.15, 0.15, 0.2, 1.0)) .child( text(title) .size(18.0) .weight(FontWeight::SemiBold) .color(Color::WHITE) ) } // Usage div().child(card("My Card")) }
Components with Children
Accept generic children with impl ElementBuilder:
#![allow(unused)] fn main() { fn card_with_content<E: ElementBuilder>(title: &str, content: E) -> Div { div() .p(16.0) .rounded(12.0) .bg(Color::rgba(0.15, 0.15, 0.2, 1.0)) .flex_col() .gap(12.0) .child( text(title) .size(18.0) .weight(FontWeight::SemiBold) .color(Color::WHITE) ) .child(content) } // Usage card_with_content("Settings", div() .flex_col() .gap(8.0) .child(text("Option 1")) .child(text("Option 2")) ) }
Stateful Components
For components needing interactive state or reactive updates:
#![allow(unused)] fn main() { use blinc::layout::stateful::stateful; fn counter_card(count: State<i32>) -> impl ElementBuilder { stateful::<ButtonState>() .p(16.0) .rounded(12.0) .bg(Color::rgba(0.15, 0.15, 0.2, 1.0)) .flex_col() .gap(12.0) .deps([count.signal_id()]) .on_state(move |_ctx| { let current = count.get(); div().child(text(&format!("Count: {}", current)).color(Color::WHITE)) }) .child(increment_btn(count)) } fn increment_btn(count: State<i32>) -> impl ElementBuilder { stateful::<ButtonState>() .px(16.0) .py(8.0) .rounded(8.0) .on_state(|ctx| { let bg = match ctx.state() { ButtonState::Idle => Color::rgba(0.3, 0.5, 0.9, 1.0), ButtonState::Hovered => Color::rgba(0.4, 0.6, 1.0, 1.0), _ => Color::rgba(0.3, 0.5, 0.9, 1.0), }; div().bg(bg) }) .on_click(move |_| { count.update(|v| v + 1); }) .child(text("+").color(Color::WHITE)) } }
Animated Components
Use motion() for components with spring animations:
#![allow(unused)] fn main() { use blinc::layout::motion::motion; #[derive(BlincComponent)] struct AnimatedCard { #[animation] scale: f32, #[animation] opacity: f32, } fn animated_card(ctx: &WindowedContext, title: &str) -> impl ElementBuilder { let scale = AnimatedCard::use_scale(ctx, 1.0, SpringConfig::snappy()); let opacity = AnimatedCard::use_opacity(ctx, 1.0, SpringConfig::gentle()); let hover_scale = Arc::clone(&scale); let leave_scale = Arc::clone(&scale); // motion() is a container - apply transforms to it, style the child motion() .scale(scale.lock().unwrap().get()) .opacity(opacity.lock().unwrap().get()) .on_hover_enter(move |_| { hover_scale.lock().unwrap().set_target(1.05); }) .on_hover_leave(move |_| { leave_scale.lock().unwrap().set_target(1.0); }) .child( div() .p(16.0) .rounded(12.0) .bg(Color::rgba(0.15, 0.15, 0.2, 1.0)) .child(text(title).color(Color::WHITE)) ) } }
Note: For hover-only visual effects without animations, prefer stateful::<S>() instead - it's more efficient as it doesn't require continuous redraws.
Stateful Components
Use stateful::<S>() for components with visual states:
#![allow(unused)] fn main() { use blinc::layout::stateful::stateful; fn interactive_card(title: &str) -> impl ElementBuilder { stateful::<ButtonState>() .p(16.0) .rounded(12.0) .on_state(|ctx| { let bg = match ctx.state() { ButtonState::Idle => Color::rgba(0.15, 0.15, 0.2, 1.0), ButtonState::Hovered => Color::rgba(0.18, 0.18, 0.25, 1.0), ButtonState::Pressed => Color::rgba(0.12, 0.12, 0.16, 1.0), _ => Color::rgba(0.15, 0.15, 0.2, 1.0), }; div().bg(bg) }) .child(text(title).color(Color::WHITE)) } }
Builder Pattern
For highly configurable components:
#![allow(unused)] fn main() { pub struct CardBuilder { title: String, subtitle: Option<String>, icon: Option<String>, bg_color: Color, on_click: Option<Box<dyn Fn()>>, } impl CardBuilder { pub fn new(title: impl Into<String>) -> Self { Self { title: title.into(), subtitle: None, icon: None, bg_color: Color::rgba(0.15, 0.15, 0.2, 1.0), on_click: None, } } pub fn subtitle(mut self, text: impl Into<String>) -> Self { self.subtitle = Some(text.into()); self } pub fn icon(mut self, path: impl Into<String>) -> Self { self.icon = Some(path.into()); self } pub fn bg(mut self, color: Color) -> Self { self.bg_color = color; self } pub fn build(self) -> Div { let mut card = div() .p(16.0) .rounded(12.0) .bg(self.bg_color) .flex_col() .gap(8.0); if let Some(icon_path) = self.icon { card = card.child( svg(&icon_path).w(24.0).h(24.0).tint(Color::WHITE) ); } card = card.child( text(&self.title) .size(18.0) .weight(FontWeight::SemiBold) .color(Color::WHITE) ); if let Some(sub) = self.subtitle { card = card.child( text(&sub) .size(14.0) .color(Color::rgba(0.6, 0.6, 0.7, 1.0)) ); } card } } // Usage CardBuilder::new("Settings") .subtitle("Manage your preferences") .icon("icons/settings.svg") .build() }
Component Libraries
Organize related components in modules:
#![allow(unused)] fn main() { // src/components/cards.rs pub mod cards { use blinc::app::prelude::*; pub fn simple_card(title: &str) -> Div { // ... } pub fn image_card(title: &str, image_url: &str) -> Div { // ... } pub fn action_card<F: Fn() + 'static>(title: &str, on_action: F) -> Div { // ... } } // src/components/mod.rs pub mod cards; pub mod buttons; pub mod inputs; // Usage use crate::components::cards::*; }
Prop Structs
For components with many parameters:
#![allow(unused)] fn main() { pub struct NotificationProps { pub title: String, pub message: String, pub variant: NotificationVariant, pub dismissible: bool, pub on_dismiss: Option<Box<dyn Fn()>>, } pub enum NotificationVariant { Info, Success, Warning, Error, } pub fn notification(props: NotificationProps) -> Div { let (bg, icon) = match props.variant { NotificationVariant::Info => (Color::rgba(0.2, 0.4, 0.8, 1.0), "info.svg"), NotificationVariant::Success => (Color::rgba(0.2, 0.7, 0.4, 1.0), "check.svg"), NotificationVariant::Warning => (Color::rgba(0.8, 0.6, 0.2, 1.0), "warning.svg"), NotificationVariant::Error => (Color::rgba(0.8, 0.3, 0.3, 1.0), "error.svg"), }; div() .p(16.0) .rounded(8.0) .bg(bg) .flex_row() .gap(12.0) .items_center() .child(svg(icon).w(20.0).h(20.0).tint(Color::WHITE)) .child( div() .flex_1() .flex_col() .gap(4.0) .child(text(&props.title).weight(FontWeight::SemiBold).color(Color::WHITE)) .child(text(&props.message).size(14.0).color(Color::rgba(1.0, 1.0, 1.0, 0.8))) ) } }
Best Practices
-
Keep components focused - One component, one responsibility.
-
Use
impl ElementBuilder- For maximum flexibility in return types. -
Document public components - Add doc comments explaining usage.
-
Consistent naming - Use descriptive names that indicate the component's purpose.
-
Default sensible styles - Provide good defaults, allow overrides.
-
Separate stateless and stateful - Pure components are easier to test and reuse.
-
Use BlincComponent for reactive props and owned signals - Generated
NamePropsaccepts values, Signal and Computed; keyed hooks persist mutable Signal data. -
Declare interaction styling in CSS - Use
:hover,:active,:focus-visibleand bound classes or attributes for appearance. -
Use motion() for animated values - Wrap animated content in
motion()for proper redraws.