Bevy UI Patterns
Contents
- HUD Layout
- Main Menu
- Health Bar / Progress Bar
- bevy_egui
Common UI Patterns
Main menu with navigation:
#![allow(unused)] fn main() { #[derive(States, Debug, Clone, PartialEq, Eq, Hash, Default)] enum MenuState { #[default] Main, Settings, Credits, } #[derive(Component)] enum MenuButton { Play, Settings, Quit, } fn spawn_main_menu(mut commands: Commands) { commands .spawn(( StateScoped(MenuState::Main), Node { width: Val::Percent(100.0), height: Val::Percent(100.0), flex_direction: FlexDirection::Column, justify_content: JustifyContent::Center, align_items: AlignItems::Center, row_gap: Val::Px(16.0), ..default() }, )) .with_children(|parent| { for (label, action) in [ ("Play", MenuButton::Play), ("Settings", MenuButton::Settings), ("Quit", MenuButton::Quit), ] { parent .spawn(( Button, action, Node { width: Val::Px(250.0), height: Val::Px(55.0), justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() }, BackgroundColor(Color::srgb(0.2, 0.2, 0.2)), )) .with_children(|btn| { btn.spawn(( Text::new(label), TextFont { font_size: 24.0, ..default() }, TextColor(Color::WHITE), )); }); } }); } fn handle_menu_buttons( query: Query<(&Interaction, &MenuButton), Changed<Interaction>>, mut next_state: ResMut<NextState<MenuState>>, mut exit: EventWriter<AppExit>, ) { for (interaction, button) in &query { if *interaction == Interaction::Pressed { match button { MenuButton::Play => { /* transition to GameState::Playing */ } MenuButton::Settings => next_state.set(MenuState::Settings), MenuButton::Quit => { exit.write(AppExit::Success); } } } } } }
StateScoped despawns the entity (and its children) automatically when leaving that state — no manual cleanup needed.
HUD overlay (health bar + score):
#![allow(unused)] fn main() { #[derive(Component)] struct HealthBar; #[derive(Component)] struct ScoreText; fn spawn_hud(mut commands: Commands) { // Root container pinned to top of screen commands .spawn(Node { width: Val::Percent(100.0), height: Val::Px(40.0), justify_content: JustifyContent::SpaceBetween, align_items: AlignItems::Center, padding: UiRect::horizontal(Val::Px(16.0)), ..default() }) .with_children(|parent| { // Health bar: background + fill parent .spawn(Node { width: Val::Px(200.0), height: Val::Px(20.0), ..default() }) .insert(BackgroundColor(Color::srgb(0.3, 0.0, 0.0))) .with_children(|bar_bg| { bar_bg.spawn(( HealthBar, Node { width: Val::Percent(100.0), height: Val::Percent(100.0), ..default() }, BackgroundColor(Color::srgb(0.0, 0.8, 0.0)), )); }); // Score text parent.spawn(( ScoreText, Text::new("Score: 0"), TextFont { font_size: 20.0, ..default() }, TextColor(Color::WHITE), )); }); } fn update_health_bar( player: Query<&Health, With<Player>>, mut bar: Query<&mut Node, With<HealthBar>>, ) { if let (Ok(health), Ok(mut node)) = (player.single(), bar.single_mut()) { node.width = Val::Percent(health.current as f32 / health.max as f32 * 100.0); } } }
Loading screen with progress bar:
#![allow(unused)] fn main() { #[derive(Resource, Default)] struct LoadingProgress { loaded: usize, total: usize, } fn update_loading_bar( progress: Res<LoadingProgress>, mut bar: Query<&mut Node, With<LoadingBar>>, ) { if let Ok(mut node) = bar.single_mut() { let pct = if progress.total > 0 { progress.loaded as f32 / progress.total as f32 * 100.0 } else { 0.0 }; node.width = Val::Percent(pct); } } }
bevy_egui — Debug and Editor UI
Use bevy_egui for debug panels, inspector tools, and editor UI. Use bevy_ui for in-game UI that ships to players.
When to choose bevy_egui:
- Rapid prototyping — egui is immediate-mode, faster to iterate
- Debug overlays, entity inspectors, level editors
- You need text input fields, sliders, collapsible panels, drag-and-drop
When to choose bevy_ui:
- Final in-game UI (menus, HUD, dialogue boxes)
- You need pixel-perfect control, custom rendering, animations
- Performance-sensitive UI (bevy_ui is integrated with the render pipeline)
Setup:
# Cargo.toml
[dependencies]
bevy_egui = "0.34" # Match your Bevy version
#![allow(unused)] fn main() { use bevy_egui::{egui, EguiContexts, EguiPlugin}; app.add_plugins(EguiPlugin); fn debug_ui(mut contexts: EguiContexts) { egui::Window::new("Debug").show(contexts.ctx_mut(), |ui| { ui.label("Hello from egui"); if ui.button("Click me").clicked() { // handle click } }); } }