A simple, fast, and modular UI library for Bevy, combining immediate mode ergonomics with Bevy ECS-powered retained UI.
- Write complex UI logic as simple Rust code.
- No macros, observers, triggers, events, signals.
- Focus on your UI, not the boilerplate.
- Immediate mode entity hierarchy management
Build interactive entity hierarchies with a clean API. - Fully compatible with Bevy
Heavy lifting is done by Bevy ECS andbevy_ui
retained mode UI. - Custom extension support
Add custom capabilities like.clicked()
,.selected(true)
,.hovered()
. Extension use integrated with rust type system for IDE and compile check support. - Inbuilt support for UI use case
Contains extensions that implement necessary logic for constructing UI. - Reusable widgets
Implement widgets using functional or bevy native style. - Fast
Only visits each entity once per tick and does minimal amount of changes. Heavy lifting is done by Bevy's retained UI. - Parallelizable
Minimal data access requirements allow systems to run in parallel with other systems without exclusive world access. - Simple
Define UI in straightforward functions, free from macro/observer/trigger boilerplate. - Modular
Extend the API with your own small capabilities and traits that encapsulate complex logic. - Integration-friendly
Works with other libraries (e.g., reloadable CSS style with bevy_flair). - Hot reloading support
Can be added via hot_lib_reloader.
bevy_immediate | bevy | MSRV |
---|---|---|
0.1 | 0.16 | 1.85 |
To use add bevy_immediate
to your project dependencies in Cargo.toml
file.
See CHANGELOG for changes between versions.
- Hello world - Minimal usage example
- Power user - Customized API for complex use cases
- Plain UI - Create your UI as a single system
- Reusable widgets
- Functional widget - Implement widgets as plain functions
- Native widget - Implement native Bevy-like widgets
- Widget use - Use functional and native widgets together
- Menu example - Build a simple menu with selectable buttons
- Extensions
- Extension implementation - Write your own capabilities (e.g.
.clicked()
or.selected(...)
) - Using extensions - Use a custom predefined set of extensions
- Extension implementation - Write your own capabilities (e.g.
- Style - Simple example how to apply custom styles to UI
Check out ./examples/
(cargo run --example demo
).

Example with code reuse and interactive elements:
pub struct MenuExamplePlugin;
impl bevy_app::Plugin for MenuExamplePlugin {
fn build(&self, app: &mut bevy_app::App) {
app.insert_resource(CurrentExample::WidgetUse);
app.add_plugins(BevyImmediateAttachPlugin::<CapsUi, MenuUiRoot>::new());
}
}
#[derive(Component)]
pub struct MenuUiRoot;
#[derive(SystemParam)]
pub struct Params<'w> {
current_example: ResMut<'w, CurrentExample>,
debug_options: ResMut<'w, UiDebugOptions>,
}
impl ImmediateAttach<CapsUi> for MenuUiRoot {
type Params = Params<'static>;
fn construct(ui: &mut Imm<CapsUi>, params: &mut Params) {
ui.ch()
.on_spawn_insert(|| Node {
flex_direction: FlexDirection::Column,
align_items: bevy_ui::AlignItems::Stretch,
..fill_parent_node()
})
.add(|ui| {
ui.ch()
.on_spawn_insert(styles::title_text_style)
.on_spawn_text("Demo");
ui.ch()
.on_spawn_insert(styles::text_style)
.on_spawn_text("bevy_immediate");
ui.ch().on_spawn_insert(|| Node {
height: Val::Px(10.),
..default()
});
for (example, title) in MENU_VARIANTS {
let mut button = ui
.ch()
.on_spawn_insert(styles::button_bundle)
.selected(example == *params.current_example)
.add(|ui| {
ui.ch()
.on_spawn_insert(styles::text_style)
.on_spawn_text(title);
});
if button.clicked() {
*params.current_example = example;
}
}
ui.ch().on_spawn_insert(|| Node {
flex_grow: 1.,
..default()
});
let mut button = ui
.ch()
.on_spawn_insert(button_bundle)
.selected(params.debug_options.enabled)
.add(|ui| {
ui.ch().on_spawn_insert(text_style).text("Debug");
});
if button.clicked() {
params.debug_options.enabled = !params.debug_options.enabled;
}
});
}
}
pub const MENU_VARIANTS: [(CurrentExample, &str); 4] = [
(CurrentExample::HelloWorld, "Hello World"),
(CurrentExample::WidgetUse, "Widget usage"),
(CurrentExample::ExtensionUse, "Extension usage"),
(CurrentExample::PowerUser, "Power user"),
];
#[derive(Resource, Hash, Clone, Copy, PartialEq, Eq)]
pub enum CurrentExample {
WidgetUse,
HelloWorld,
ExtensionUse,
PowerUser,
}
Here's a more advanced example where user has added their own API.
pub struct PowerUserExamplePlugin;
impl bevy_app::Plugin for PowerUserExamplePlugin {
fn build(&self, app: &mut bevy_app::App) {
// Initialize plugin with your widget root component
app.add_plugins(BevyImmediateAttachPlugin::<CapsUi, PowerUserExampleRoot>::new());
app.insert_resource(ShowHidden { show: false });
}
}
#[derive(Resource)]
struct ShowHidden {
show: bool,
}
#[derive(Component)]
pub struct PowerUserExampleRoot;
#[derive(SystemParam)]
pub struct Params<'w> {
show_hidden: ResMut<'w, ShowHidden>,
}
impl ImmediateAttach<CapsUi> for PowerUserExampleRoot {
type Params = Params<'static>;
fn construct(ui: &mut Imm<CapsUi>, params: &mut Params) {
ui.ch().my_title("Bevy power user example");
ui.ch()
.my_subtitle("Use helper functions to simplify and reuse code!");
ui.ch().my_subtitle("Show collapsible element");
ui.ch().my_row_container().add(|ui| {
for (text, state) in [("No", false), ("Yes", true)] {
let mut button = ui
.ch_id(("choice", state))
.my_button()
.selected(params.show_hidden.show == state)
.add(|ui| {
ui.ch().my_text(text);
});
if button.clicked() {
params.show_hidden.show = state;
}
}
});
if params.show_hidden.show {
ui.ch_id("yes_no").my_container_with_background().add(|ui| {
ui.ch().my_text("Lorem Ipsum!");
});
}
ui.ch().my_text("It is really simple!");
}
}
You can add new capabilities with just a few lines of code.
Here’s how .selected(...)
is implemented.
/// Implements capability to mark entities as selectable.
pub struct CapabilityUiSelectable;
impl ImmCapability for CapabilityUiSelectable {
fn build<Cap: CapSet>(app: &mut bevy_app::App, cap_req: &mut crate::ImmCapAccessRequests<Cap>) {
cap_req.request_component_write::<Selectable>(app.world_mut());
}
}
/// Marks component as being selectable
#[derive(bevy_ecs::component::Component)]
pub struct Selectable {
/// Is selectable component selected
pub selected: bool,
}
/// Implements methods to set entity selectable
pub trait ImmUiSelectable {
/// Insert [`Selected`] component with given boolean value
///
/// Useful for styling purposes
fn selected(self, selected: bool) -> Self;
}
impl<Cap> ImmUiSelectable for ImmEntity<'_, '_, '_, Cap>
where
Cap: ImplCap<CapabilityUiSelectable>,
{
fn selected(mut self, selected: bool) -> Self {
if let Ok(Some(mut comp)) = self.cap_get_component_mut::<Selectable>() {
if comp.selected == selected {
return self;
}
comp.selected = selected;
return self;
}
self.entity_commands().insert(Selectable { selected });
self
}
}
Make sure that you assign unique id using ch_id
for ui nodes that
can appear, disappear.
- Queries: Add
Without<ImmMarker<Caps>>
to your query filter. - Resources: Avoid direct conflicts, or use .ctx() / .ctx_mut() APIs to access resources used by capabilities.
Contributions are welcome!
- Add your improvements to examples
- Suggest or implement new capabilities useful for UI creation
Publish your own crate that is built using bevy_immediate
!
- Originally created for Settletopia
- Inspired by egui_taffy.
- Initial idea discussion
-
Easier definition of new capability sets
- Tried transitive capability implementation (works only inside one crate)
- Tried transitive trait implementation (works only inside one crate)
- Tried TupleList approach (conflicting trait implementations)
Therefore currently to define capability set users need to list all used capabilities.
-
Improve examples
- Add Example for hot reloading
-
Create reusable logic for:
- Scroll areas
- Tooltips
- Windows (like
egui::Window
)