// Specialized guide for creating new Ribir UI widgets. Use this skill when asked to create, implement, or refactor a widget in the Ribir framework, especially when the work should include a gallery showcase or a hands-on UI walkthrough.
Specialized guide for creating new Ribir UI widgets. Use this skill when asked to create, implement, or refactor a widget in the Ribir framework, especially when the work should include a gallery showcase or a hands-on UI walkthrough.
Ribir Widget Creation Guide
This guide ensures that all Ribir widgets generated by agents are structurally sound, well-designed, and properly integrate with the framework's reactive UI paradigm. It draws upon the best practices established in components like NavigationRail and InteractiveLayers, including the live gallery workflow used by examples/gallery/src/sections/navigation_rail.rs.
0. Core Principles
Ribir widgets follow the Single Source of Truth and Unidirectional Data Flow principles.
Basic Implementation: See Custom Widgets Tutorial for how to structure widgets using Compose, ComposeChild, and Template.
Pipe is Authority: UI state is a projection of data.
Strict Control: Interaction events (Path B) represent user intent and should not directly update state unless via confirmed Path A or optimistic Path C.
Scope Triage
Start by classifying scope and auditing existing surfaces:
New public widget: widget + tests + gallery showcase + route + walkthrough.
Existing public widget, user-facing change: update the current showcase and rerun the walkthrough. Add a new route/page only if none exists.
Internal/private widget: gallery work only when explicitly requested.
Before adding files or routes, check the widget implementation, theme classes, current showcase, route tables, and tests. Extend the current showcase when possible.
Delivery Definition of Done
For a new public widget, or a substantial user-facing expansion of one, done means:
widget implementation + supporting tests
gallery showcase added or updated
route added if the widget was not already exposed in gallery
walkthrough run for user-facing layout, interaction, or navigation changes
Do not create duplicate gallery pages or routes for small fixes when an existing showcase already covers the widget.
1. Defining the Widget
Ribir widgets are primarily struct definitions accompanied by the #[declare] macro, which automatically implements the builder pattern for properties and events.
1.1 The #[declare] Macro
Use #[declare] on your widget's struct. It generates the infrastructure needed for the DSL to construct the widget.
Defaults: Use #[declare(default)] when a field has a sensible default value.
Setters & Events: You can define custom setters and bind events directly in the macro. This enables uncontrolled/TwoWay binding behavior.
Smart Pointers: For public text properties and event payloads, prefer CowArc<str> over String for performance and memory sharing. Use String for internal mutable scratch data only when it genuinely makes the logic clearer.
#[derive(Clone)]#[declare]pubstructMyWidget {
#[declare(default)]pub title: CowArc<str>,
/// A business selection state#[declare(default, setter = set_selected, event = MySelectEvent::to.clone())]pub selected: CowArc<str>,
}
1.2 Defining Class Names
Use the class_names! macro to declare stable stylistic identifiers for your widget and its sub-components. Base classes and mode/state classes should be cleanly separated.
class_names! {
/// Root container class
MY_WIDGET,
/// Class for selected items
MY_WIDGET_SELECTED,
/// Class for the icon
MY_WIDGET_ICON,
}
2. Managing Children with Templates
For widgets that contain varying child components, use #[derive(Template)]. This allows the DSL to destructure and partition incoming widgets smoothly, supporting optional nodes and varying quantities.
2.1 Struct Templates
When a widget expects specific distinct parts (e.g., an icon and an optional label).
You can process these by iterating over children: Vec<MyWidgetChild<'c>> and partitioning them as needed before composing the main visual tree.
3. Composing the UI
The heart of a widget is its ComposeChild implementation, which bridges the declarative struct and the actual widget tree.
3.1 Implementing ComposeChild
Always implement ComposeChild<'c> to define the structure, unless the widget acts completely transparently without children (where Compose might be used).
Leverage BuildCtx and Provider::of to conditionally render elements based on external context (such as disabling interactive layers or knowing if a rail is expanded).
letctx = BuildCtx::get();
if Provider::of::<DisableInteractiveLayer>(ctx).is_some() {
return child; // Escape early if interactivity isn't needed
}
letexpanded = Variant::<RailExpanded>::new_or_default(ctx);
If your widget needs to distribute state to its descendants, use @Providers { ... } or the providers! macro.
To avoid unnecessary re-renders or layout jitter (like breaking width smoothing transitions), keep the base class stable. Switch only the active/mode class dynamically when state changes using class_list!.
Use @FatObj { ... } to wrap generic widget elements when attaching events, setting dynamic properties (visible), or applying interactive visual layers via StateLayer or FocusIndicator. It safely merges interactivity to existing child trees.
If your widget serves as a container that tracks nested interactions, handle bubbling logic internally. Keep business routing external by emitting custom events.
Define a custom event type alias on top of CustomEvent.
Catch events from children via on_action or on_tap.
Bubble events upwards to expose them cleanly without deeply coupling business state logic inside the view hierarchy.
Use TestWindow methods to simulate user input. For events involving position (like clicks), use map_to_global to get the correct coordinates for a specific WidgetId.
#[test]fnwidget_click_updates_state() {
reset_test_env!();
let (selected, w_selected) = split_value(false);
letwnd = TestWindow::from_widget(fn_widget! {
@MyWidget {
on_tap: move |_| *$write(w_selected) = true,
}
});
wnd.draw_frame();
// Simulate a click at the center of the windowletpos = Point::new(10., 10.);
wnd.process_cursor_move(pos);
wnd.process_mouse_press(Box::new(DummyDeviceId), MouseButtons::PRIMARY);
wnd.process_mouse_release(Box::new(DummyDeviceId), MouseButtons::PRIMARY);
wnd.draw_frame();
assert!(*selected.read());
}
6.3 Advanced Layout Assertions
For precision layout testing, use epsilon-based comparisons for floating-point values and verify the full semantic hierarchy.
Use the widget_image_tests! macro from ribir_dev_helper to perform visual regression testing. This macro automatically runs the test across different themes and backends.
use ribir_core::test_helper::*;
use ribir_dev_helper::*;
widget_image_tests!(
my_widget_test_name,
WidgetTester::new(fn_widget! {
@MyWidget { ... }
})
.with_wnd_size(Size::new(200., 200.))
);
Golden Images: Images are stored in the test_cases/ folder at the workspace root, relative to the test source path.
Updating Images: Run tests with RIBIR_IMG_TEST=overwrite cargo test to update the golden images when intentional visual changes are made.
6.5 Gallery Showcase Integration
When scope triage says the widget should have a public gallery surface, use the NavigationRail delivery as the reference pattern.
Typical file touch-list for a new widget showcase:
widgets/src/<widget>.rs (or the widget's main implementation file)
examples/gallery/src/sections/<widget>.rs
examples/gallery/src/sections.rs
examples/gallery/src/sections/widgets.rs
examples/gallery/src/app.rs when route metadata, breadcrumbs, redirects, or shell selection logic need updates
Before creating a new gallery page, first check whether the widget already has a showcase that should be extended instead.
Create a dedicated gallery section in examples/gallery/src/sections/<widget>.rs.
Expose it as pub fn page_<widget>() -> Widget<'static>.
Wrap the page with section_page(...) from examples/gallery/src/sections/common.rs.
Register the new section module in examples/gallery/src/sections.rs so the page is actually reachable by the gallery.
Add a Widgets landing entry in examples/gallery/src/sections/widgets.rs.
The widget should have its own card/tile/entry that explains the demo and navigates to the showcase route.
Use the gallery's shared visual helpers and styles instead of inventing one-off chrome.
Wire the route under /widgets/*.
Add the route in examples/gallery/src/sections/widgets.rs.
Update examples/gallery/src/app.rs when breadcrumbs, redirects, or top-level rail selection need to know about the new nested widget route.
Prefer explicit path constants and breadcrumb tables so nested widget routes remain discoverable and testable.
Make the showcase stateful and realistic.
Expose the widget's major states, modes, and slots instead of showing only one static pose.
If the widget has structural toggles (for example optional slots or child topology changes), it is acceptable to rebuild the widget for those combinations.
Keep ordinary control state and data state reactively bound so the demo behaves like a real app rather than a static pose with disconnected controls.
Prefer representative usage over synthetic micro-demos.
Show the widget in a believable application context when possible.
Reuse shared page scaffolding from examples/gallery/src/sections/common.rs and gallery classes from examples/gallery/src/styles.rs.
Name important targets for debugging.
Add debug_name to the showcase root and important interactive controls when it helps the walkthrough target them reliably.
6.6 Gallery Route & Showcase Regression Tests
When a widget has a gallery showcase, test that wiring too — not just the widget internals.
Route metadata tests:
If examples/gallery/src/app.rs contains logic like top-level section mapping, redirects, or breadcrumb tables, add/update tests for the new widget route.
The NavigationRail route tests are the reference pattern here.
Showcase smoke tests:
If the new gallery page contains structural toggles, control panels, or multiple major layout branches, add smoke tests that mount the showcase and exercise those branches.
Non-panicking branch coverage is valuable for demos because the showcase itself is user-facing product surface, not disposable sample code.
Test the real composition path:
Prefer mounting the same gallery page composition that users open, rather than only testing tiny helper fragments in isolation.
7. Manual Walkthrough & Walkthrough with MCP (Confirming User Experience)
When the widget has a public showcase, automated tests are not enough. Run an MCP walkthrough to confirm the real user experience (UX).
When you create or substantially change a public widget surface, run the walkthrough before declaring the task done. If MCP or gallery launch is unavailable, report that blocker explicitly.
7.1 Using the MCP Inspector for UX Verification
Launch the Gallery Showcase:
Prefer using the examples/gallery demo as the walkthrough surface for public widgets.
Open the widget through the same gallery route that end users will use, not through an isolated scratch snippet unless the widget is explicitly internal-only.
Verify Navigation Into the Demo:
Confirm the Widgets landing entry opens the correct showcase route.
Confirm gallery rail selection and breadcrumbs remain correct for the nested route.
Visual Consistency Check:
Use the inspector to verify the WidgetTree, layout bounds, hierarchy, margins, and alignments.
Compare the default state and the main alternate states (expanded/collapsed, selected/unselected, empty/populated, etc.).
Confirm Interactive "Feel":
Trigger the important user interactions yourself (tap, hover, keyboard focus, toggles, scrolling, mode switches, and slot combinations as applicable).
Verify that ripples, focus rings, state layers, transitions, and visibility changes feel responsive and intentional.
Contextual Awareness:
Verify the widget reacts correctly to providers, parent layout constraints, and realistic content sizes.
If the gallery demo exposes control-panel toggles like NavigationRail, exercise each major branch at least once.
Fix Findings Before Exit:
If the walkthrough reveals spacing glitches, clipping, incorrect transitions, broken selection logic, or confusing states, fix them before ending the task whenever practical.
7.2 UX Walkthrough Checklist
Gallery Entry Exists: The widget has a discoverable entry in the gallery Widgets page.
Route Works: The showcase opens through its /widgets/... route and keeps gallery navigation state correct.
Visual Polish: Spacing, colors, and shadows create a modern, high-quality aesthetic.
Interaction Feedback: Every user action provides clear, immediate, and platform-appropriate visual feedback.
Adaptive Integrity: Mode transitions and structural toggles remain smooth and visually coherent.
Logical Consistency: State transitions (selection, activation, expansion, empty/data-loaded states, etc.) align with user expectations and business logic.
Overflow & Layout: Long labels, dense content, scrolling areas, and narrow/wide states do not clip, jump, or misalign.
Accessibility Verification: Focus indicators are visible and the semantic structure remains sensible for assistive technologies.
Self-Run Verification: You actually performed this walkthrough, rather than merely recommending that someone else should.