11 releases
| new 0.2.1 | Nov 15, 2025 |
|---|---|
| 0.2.0 | Nov 7, 2025 |
| 0.1.10 | Sep 30, 2025 |
#180 in Game dev
385KB
8K
SLoC
Bevy UI Builders
Before using bevy-ui-builders
commands.spawn((
Button,
Node {
width: Val::Px(200.0),
height: Val::Px(65.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(2.0)),
margin: UiRect::all(Val::Px(10.0)),
padding: UiRect::all(Val::Px(10.0)),
..default()
},
BackgroundColor(Color::srgb(0.15, 0.15, 0.15)),
BorderColor::all(Color::BLACK),
)).with_children(|parent| {
parent.spawn((
Text::new("Click Me"),
TextFont {
font_size: 40.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
));
});
// Plus 30+ more lines for hover system...
After using bevy-ui-builders
use bevy_ui_builders::*;
ButtonBuilder::new("Click Me")
.style(ButtonStyle::Primary)
.build(parent);
That's it! Hover effects, styling, and interaction handling included.
What's New in v0.2.0
Bevy 0.17 Migration
- Updated to Bevy 0.17 with full compatibility
- Auto-includes
DefaultPickingPluginsfor Interaction components - Breaking changes handled internally
New Builders (3 Added)
- CheckboxBuilder - Interactive checkboxes with toggle states
- Styles: Primary, Success, Danger, Default
- Configurable size and label positioning
- DropdownBuilder - Dropdown select menus
- Click-outside-to-dismiss functionality
- Proper z-index layering (fully opaque overlay)
- NumberInputBuilder - Validated number inputs
- Auto-validates against min/max range
- Shows red border when out of bounds
Universal Validation System
- Composable validation rules work with ANY input (not just forms)
TextInputBuilder.with_validation(Vec<ValidationRule>)method- Visual feedback: red border on error, default border when valid
- ValidationRule types: Required, MinLength, MaxLength, Range, Email, Pattern, Custom
ValidationStateandValidatedECS components
Bug Fixes
- Fixed slider cursor offset bug
- Fixed button hover/press visual feedback
- Fixed scrollbar drag unfocusing when mouse exits window
Complete Builder Count
Now includes 13 builders: Button, Dialog, TextInput, Form, Slider, ProgressBar, Panel, Label, Separator, ScrollView, Checkbox, NumberInput, Dropdown
Quick Start
Add to your Cargo.toml:
[dependencies]
bevy = "0.17"
bevy-ui-builders = "0.2"
Add the plugin:
use bevy::prelude::*;
use bevy_ui_builders::UiBuilderPlugin;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(UiBuilderPlugin) // Auto-includes DefaultPickingPlugins
.run();
}
In your code, import the prelude for convenient access to all builders:
use bevy_ui_builders::prelude::*;
fn build_ui(mut commands: Commands) {
commands.spawn(Camera2d);
// All builders and types available
ButtonBuilder::new("Click Me")
.style(ButtonStyle::Primary)
.build(&mut commands.spawn(Node::default()).commands());
}
Complete Builder Catalog
1. ButtonBuilder - Styled Interactive Buttons
// Simple button
ButtonBuilder::new("Click Me").build(parent);
// Styled button with all options
ButtonBuilder::new("Submit")
.style(ButtonStyle::Primary) // Primary, Secondary, Success, Danger, Warning, Ghost
.size(ButtonSize::Large) // Small, Medium, Large, XLarge
.with_marker(SubmitButton) // Add your own marker component
.margin(UiRect::all(Val::Px(10.0))) // Custom margins
.height(Val::Px(50.0)) // Custom height
.enabled(true) // Enable/disable state
.build(parent); // Or use .build_in(parent) alias
// Convenience functions
primary_button("Save").build(parent);
danger_button("Delete").build(parent);
ghost_button("Cancel").build(parent);
2. DialogBuilder - Modal Dialogs with Overlays
// Basic dialog
DialogBuilder::new(DialogType::Custom)
.title("Delete Item?")
.body("This action cannot be undone.")
.danger_button("Delete")
.cancel_button("Cancel")
.dismissible(false) // Can't click outside to close
.z_index(1000) // Layer above other UI
.build(commands);
// NEW: Get button entities for custom markers (v0.1.7)
let (dialog, buttons) = DialogBuilder::new(DialogType::Custom)
.title("Confirm")
.confirm_button("Yes")
.cancel_button("No")
.build_with_buttons(commands);
// Add your own markers to dialog buttons
if let Some(btn) = buttons.get(&DialogButtonMarker::Confirm) {
commands.entity(*btn).insert(MyCustomMarker);
}
// Helper for single marker
DialogBuilder::new(DialogType::Custom)
.danger_button("Delete")
.build_and_mark(commands, DialogButtonMarker::Confirm, DeleteMarker);
// Preset dialogs - static methods
DialogBuilder::error(commands, "File not found!");
DialogBuilder::unsaved_changes(commands);
DialogBuilder::confirm(commands, "Delete?", "This cannot be undone.");
3. TextInputBuilder - Native Text Editing with Full Features
// Full-featured text input with native implementation
TextInputBuilder::new()
.with_placeholder("Enter email...")
.with_filter(InputFilter::Alphanumeric) // Allow letters and numbers
.with_max_length(100)
.with_width(Val::Px(300.0)) // Set width
.with_height(Val::Px(40.0)) // Set height
.with_focus_group(FocusGroupId::LoginForm) // Tab navigation
.with_clear_button() // X button to clear
.with_marker(EmailInput)
.build(parent);
// Text input with validation (v0.2.0+)
TextInputBuilder::new()
.with_placeholder("Enter email")
.with_validation(vec![
ValidationRule::Required,
ValidationRule::Email,
ValidationRule::MinLength(5),
])
.build(parent);
// Password input with masking
text_input()
.with_placeholder("Password")
.password() // Masks input with bullets
.build(parent);
// Numeric input
text_input()
.numeric_only() // Only allows 0-9
.with_value("0")
.build(parent);
Native Features:
- Full cursor rendering with blinking
- Text selection (keyboard Shift+arrows & mouse drag)
- Clipboard operations (Ctrl+C/V/X)
- Undo/Redo support (Ctrl+Z/Shift+Z)
- Proper Tab navigation between inputs
- Auto-scroll when text overflows
- Validation support with visual feedback (v0.2.0+)
4. FormBuilder - Complete Forms with Validation
// Simple login form
FormBuilder::new("login_form")
.title("Sign In")
.text_field("username", "Username")
.required()
.placeholder("Enter username")
.password_field("password", "Password")
.required()
.placeholder("Enter password")
.checkbox_field("remember", "Remember me")
.submit_text("Login")
.cancel_text("Cancel")
.width(Val::Px(350.0))
.build(parent);
// Complex registration with multiple field types
FormBuilder::new("registration_form")
.title("Create Account")
.text_field("first_name", "First Name")
.required()
.validate(ValidationRule::MinLength(2))
.email_field("email", "Email Address")
.required()
.password_field("password", "Password")
.required()
.validate(ValidationRule::MinLength(8))
.help_text("Must be at least 8 characters")
.dropdown_field("country", "Country", vec![
"United States".to_string(),
"Canada".to_string(),
"Other".to_string(),
])
.checkbox_field("terms", "I agree to the terms")
.required()
.submit_text("Register")
.width(Val::Px(400.0))
.build(parent);
Key API methods:
FormBuilder::new(id: &str)- Constructor takes form ID.text_field(name, label)- Text input field.email_field(name, label)- Email input with validation.password_field(name, label)- Password input (hidden text).checkbox_field(name, label)- Checkbox field.dropdown_field(name, label, options)- Dropdown selection.slider_field(name, label, min, max)- Numeric slider.number_field(name, label, min, max)- Number input with range.required()- Mark previous field as required (chainable).validate(rule)- Add validation rule to previous field (chainable).placeholder(text)- Set placeholder for previous field (chainable).help_text(text)- Add help text below previous field (chainable).submit_text(text)- Set submit button text.cancel_text(text)- Set cancel button text.width(Val)- Set form width.layout(FormLayout)- Set layout (Vertical, Horizontal, Grid)
5. SliderBuilder - Value Sliders with Formatting
// Percentage slider
SliderBuilder::new(0.5, 0.0..=1.0)
.width(Val::Px(200.0))
.with_label(ValueFormat::Percentage) // Shows "50%"
.with_marker(VolumeSlider)
.build(parent);
// Custom format with new methods
slider(75.0, 0.0..=100.0)
.with_label(ValueFormat::Custom(|v| format!("{:.0}°C", v)))
.with_format(ValueFormat::Integer) // Alternative to format() method
.with_marker(TemperatureSlider) // Add custom marker component
.build_in(parent); // Or use build() alias
6. ProgressBarBuilder - Progress Indicators
// Basic progress bar
ProgressBarBuilder::new(0.75) // 75% complete
.style(ProgressBarStyle::Thick)
.with_label() // Shows "75%"
.animated() // Smooth transitions
.build(parent);
// Custom styled
progress(loading_progress)
.fill_color(Color::srgb(0.2, 0.8, 0.2))
.track_color(Color::srgb(0.1, 0.1, 0.1))
.with_label_text("Loading assets...")
.build(parent);
7. ScrollViewBuilder - Responsive Scrollable Containers
// Basic scrollable container with viewport-based sizing
ScrollViewBuilder::new()
.width(Val::Percent(100.0)) // Set width
.height(Val::Percent(100.0)) // Set height
.max_height(Val::Vh(80.0)) // 80% of viewport height
.padding_vw(2.0) // 2% viewport width padding
.padding_vh(1.0) // 1% viewport height padding
.gap(Val::Vh(2.0)) // 2% viewport height gap between children
.margin(UiRect::all(Val::Px(10.0))) // Margin around scroll view
.background_color(Color::srgb(0.1, 0.1, 0.1)) // Background color
.auto_scroll(true) // Auto-scroll to focused elements
.scrollbar_visibility(ScrollbarVisibility::AutoHide { timeout_secs: 2.0 })
.build_with_children(parent, |scroll| {
// Add content that might overflow
for i in 0..100 {
LabelBuilder::new(format!("Item {}", i)).build(scroll);
}
});
// Horizontal scrolling
scroll_view()
.direction(ScrollDirection::Horizontal)
.max_width(Val::Vw(90.0)) // 90% viewport width
.build(parent);
// Simple build without children (add content later)
let scroll_entity = ScrollViewBuilder::new()
.width(Val::Px(400.0))
.height(Val::Px(600.0))
.build(parent);
Features:
- Viewport-relative sizing (Val::Vh/Vw) for responsive design
- Mouse wheel scrolling support
- Visual scrollbars with auto-hide timeout or always visible
- Auto-scroll to focused text inputs
- Smooth scroll animations
- Draggable scrollbar thumb
- Keyboard scrolling support
Key methods:
.width(Val)- Set container width.height(Val)- Set container height.padding_vh(f32)- Viewport height padding.padding_vw(f32)- Viewport width padding.margin(UiRect)- Set margins.background_color(Color)- Background color.scrollbar_visibility(ScrollbarVisibility)- Always, AutoHide, or Hidden.direction(ScrollDirection)- Vertical (default) or Horizontal.auto_scroll(bool)- Auto-scroll to focused inputs
8. PanelBuilder - Flexible Container Panels
// Card panel with all options
PanelBuilder::new()
.style(PanelStyle::Card)
.width(Val::Px(400.0))
.padding(UiRect::all(Val::Px(24.0)))
.border_color(Color::srgb(0.3, 0.3, 0.3)) // Custom border
.flex_grow(1.0) // Flexbox growth
.flex_shrink(0.0) // Flexbox shrink
.with_title("Settings")
.build_with_children(parent, |panel| {
// Add any children here
LabelBuilder::new("Audio Settings").build(panel);
SeparatorBuilder::new().build(panel);
// More UI...
});
// Transparent overlay panel
panel()
.style(PanelStyle::Transparent)
.position_type(PositionType::Absolute)
.build(parent);
9. LabelBuilder - Consistent Text Labels
// Styled labels
LabelBuilder::new("Welcome!")
.style(LabelStyle::Title) // Title, Heading, Body, Caption
.text_align(JustifyContent::Center)
.margin(UiRect::bottom(Val::Px(20.0)))
.build(parent);
// Status labels
label("Error: File not found")
.style(LabelStyle::Error) // Error, Success, Warning, Muted
.build(parent);
10. SeparatorBuilder - Visual Dividers
// Horizontal separator
SeparatorBuilder::new()
.style(SeparatorStyle::Dashed)
.margin(UiRect::vertical(Val::Px(20.0)))
.build(parent);
// Vertical divider
separator_vertical()
.style(SeparatorStyle::Thick)
.length(Val::Percent(80.0))
.build(parent);
11. CheckboxBuilder - Interactive Checkboxes
// Simple checkbox
CheckboxBuilder::new()
.checked(false)
.with_label("Remember me")
.build(parent);
// Styled checkbox with custom size
CheckboxBuilder::new()
.checked(true)
.style(CheckboxStyle::Primary) // Primary, Success, Danger, Default
.with_label("I agree to the terms")
.label_on_right(true) // true = right (default), false = left
.size(24.0) // Checkbox size in pixels (default: 20.0)
.build(parent);
Features:
- Visual states: unchecked (empty box) and checked (blue fill with white "X")
- Styles: Default, Primary, Success, Danger
- Configurable checkbox size
- Label positioning (left or right)
- Click to toggle state
12. NumberInputBuilder - Validated Number Inputs
// Basic number input
NumberInputBuilder::new()
.min(0.0)
.max(100.0)
.default_value(50.0)
.build(parent);
// Number input with validation
NumberInputBuilder::new()
.min(8.0)
.max(24.0)
.step(0.5)
.default_value(16.0)
.width(Val::Px(200.0))
.with_placeholder("Enter font size")
.build(parent);
Features:
- Automatic range validation (shows red border if out of range)
- Accepts decimal values
- Configurable min/max bounds
- Step size for increment/decrement
- Integrates with universal validation system
- Shows hint text: "Range: min-max"
13. DropdownBuilder - Dropdown Select Menus
// Simple dropdown
DropdownBuilder::new(vec![
"Option 1".to_string(),
"Option 2".to_string(),
"Option 3".to_string(),
])
.build(parent);
// Dropdown with initial selection
DropdownBuilder::new(vec![
"Light".to_string(),
"Dark".to_string(),
"Auto".to_string(),
])
.placeholder("Select theme")
.selected_index(Some(2)) // "Auto" pre-selected
.width(Val::Px(250.0))
.build(parent);
Features:
- Click-to-open menu overlay
- Click outside to dismiss
- Hover highlights options
- Displays selected value in button
- Fully opaque menu with proper z-index layering
- Only one option highlighted at a time
Cleanup
Generic cleanup system:
// Before: 15 lines of cleanup per UI system
fn cleanup_menu(mut commands: Commands, query: Query<Entity, With<MenuRoot>>) {
for entity in &query {
commands.entity(entity).despawn();
}
}
// After: One line, works for ANY component!
app.add_systems(OnExit(GameState::Menu), despawn_ui_entities::<MenuRoot>);
Note: In Bevy 0.16+, despawn() is automatically recursive - it will despawn all children. The deprecated despawn_recursive() is no longer needed.
Relationship Auto-Cleanup
UI components use Bevy's relationship system with linked_spawn for automatic cleanup:
// When you close a dialog, all related elements (buttons, overlays) are automatically cleaned up
DialogBuilder::new(DialogType::Confirmation)
.title("Delete?")
.build(commands);
// Relationship components like BelongsToDialog with linked_spawn handle cleanup
// No manual cleanup needed!
Related entities are automatically despawned when the parent is despawned, thanks to the linked_spawn attribute on relationship targets.
Complete Example
Showcasing multiple builders working together:
use bevy::prelude::*;
use bevy_ui_builders::prelude::*;
#[derive(Component)]
struct SettingsForm;
fn setup_ui(mut commands: Commands) {
commands.spawn(Camera2d);
// ScrollView with responsive layout
commands.spawn(Node::default()).with_children(|parent| {
ScrollViewBuilder::new()
.width(Val::Percent(100.0))
.height(Val::Percent(100.0))
.scrollbar_visibility(ScrollbarVisibility::AutoHide { timeout_secs: 2.0 })
.build_with_children(parent, |scroll| {
// Panel container
PanelBuilder::new()
.style(PanelStyle::Card)
.width(Val::Px(450.0))
.padding(UiRect::all(Val::Px(32.0)))
.with_title("Settings")
.build_with_children(scroll, |panel| {
// Dropdown for theme selection
LabelBuilder::new("Theme")
.style(LabelStyle::Body)
.margin(UiRect::bottom(Val::Px(8.0)))
.build(panel);
DropdownBuilder::new(vec![
"Dark".to_string(),
"Light".to_string(),
"Auto".to_string(),
])
.placeholder("Select theme")
.width(Val::Percent(100.0))
.build(panel);
SeparatorBuilder::new()
.style(SeparatorStyle::Invisible)
.margin(UiRect::vertical(Val::Px(16.0)))
.build(panel);
// Number input with validation
LabelBuilder::new("Font Size (8-24)")
.style(LabelStyle::Body)
.margin(UiRect::bottom(Val::Px(8.0)))
.build(panel);
NumberInputBuilder::new()
.min(8.0)
.max(24.0)
.default_value(16.0)
.width(Val::Percent(100.0))
.build(panel);
SeparatorBuilder::new()
.style(SeparatorStyle::Invisible)
.margin(UiRect::vertical(Val::Px(16.0)))
.build(panel);
// Email with validation
LabelBuilder::new("Email")
.style(LabelStyle::Body)
.margin(UiRect::bottom(Val::Px(8.0)))
.build(panel);
TextInputBuilder::new()
.with_placeholder("Enter email...")
.with_validation(vec![
ValidationRule::Required,
ValidationRule::Email,
])
.width(Val::Percent(100.0))
.build(panel);
SeparatorBuilder::new()
.margin(UiRect::vertical(Val::Px(16.0)))
.build(panel);
// Checkboxes
CheckboxBuilder::new()
.checked(true)
.style(CheckboxStyle::Primary)
.with_label("Enable notifications")
.build(panel);
CheckboxBuilder::new()
.checked(false)
.with_label("Auto-save settings")
.build(panel);
SeparatorBuilder::new()
.style(SeparatorStyle::Invisible)
.margin(UiRect::vertical(Val::Px(24.0)))
.build(panel);
// Slider for volume
LabelBuilder::new("Volume")
.style(LabelStyle::Body)
.margin(UiRect::bottom(Val::Px(8.0)))
.build(panel);
SliderBuilder::new(0.0..=100.0)
.value(75.0)
.width(Val::Percent(100.0))
.with_label(ValueFormat::Percentage)
.build(panel);
SeparatorBuilder::new()
.style(SeparatorStyle::Invisible)
.margin(UiRect::vertical(Val::Px(24.0)))
.build(panel);
// Progress bar
ProgressBarBuilder::new(0.65)
.style(ProgressBarStyle::Thick)
.with_label()
.build(panel);
SeparatorBuilder::new()
.style(SeparatorStyle::Invisible)
.margin(UiRect::vertical(Val::Px(24.0)))
.build(panel);
// Action buttons
ButtonBuilder::new("Save Settings")
.style(ButtonStyle::Primary)
.size(ButtonSize::Large)
.width(Val::Percent(100.0))
.margin(UiRect::bottom(Val::Px(10.0)))
.build(panel);
ButtonBuilder::new("Reset to Defaults")
.style(ButtonStyle::Secondary)
.width(Val::Percent(100.0))
.build(panel);
});
});
});
}
This example demonstrates:
- ScrollView with auto-hide scrollbar
- Panel with Card style
- Dropdown for theme selection
- NumberInput with range validation (8-24)
- TextInput with email validation
- Checkboxes for boolean settings
- Slider with percentage formatting
- ProgressBar with label
- Labels and Separators for layout
- Buttons for actions
All 13 builders working together!
Universal Validation System
The crate provides a composable validation system that works across all input types (not just forms).
ValidationRule Types
use bevy_ui_builders::prelude::*;
// Available validation rules
ValidationRule::Required // Field cannot be empty
ValidationRule::MinLength(usize) // Minimum character length
ValidationRule::MaxLength(usize) // Maximum character length
ValidationRule::Range { min: f32, max: f32 } // Numeric range (for numbers)
ValidationRule::Email // Basic email format validation
ValidationRule::Pattern(String) // Simple pattern matching
ValidationRule::Custom(fn(&str) -> Result<(), String>) // Custom validator
Adding Validation to Inputs
// Validate any TextInput
TextInputBuilder::new()
.with_placeholder("Enter email")
.with_validation(vec![
ValidationRule::Required,
ValidationRule::Email,
ValidationRule::MinLength(5),
])
.build(parent);
// NumberInputBuilder has automatic validation
NumberInputBuilder::new()
.min(8.0)
.max(24.0) // Automatically creates ValidationRule::Range { min: 8, max: 24 }
.build(parent);
// Custom validation function
TextInputBuilder::new()
.with_validation(vec![
ValidationRule::Custom(|value| {
if value.contains("admin") {
Ok(())
} else {
Err("Must contain 'admin'".to_string())
}
})
])
.build(parent);
Visual Feedback
When validation fails:
- Input border turns red (
BORDER_ERRORcolor) ValidationStatecomponent stores error messageValidatedcomponent holds validation rules
When validation passes:
- Input border returns to default color
ValidationState.is_valid = true
How It Works
The validation system uses Bevy ECS components:
#[derive(Component)]
pub struct Validated {
pub rules: Vec<ValidationRule>,
}
#[derive(Component)]
pub struct ValidationState {
pub is_valid: bool,
pub error_message: Option<String>,
}
A system monitors Changed<TextBuffer> and runs all validation rules:
- Checks each rule in order
- Stops at first error
- Updates
ValidationState - Changes border color
Integration with Forms
FormBuilder fields automatically use validation:
FormBuilder::new("my_form")
.text_field("username", "Username")
.required() // Adds ValidationRule::Required
.validate(ValidationRule::MinLength(3)) // Adds custom rule
.email_field("email", "Email")
.required() // Adds Required + Email rules
.build(parent);
Feature Flags
Control which builders are included:
# Include everything (default)
bevy-ui-builders = "0.2"
# Or pick specific builders
bevy-ui-builders = { version = "0.2", default-features = false, features = ["button", "dialog"] }
Available features:
button- ButtonBuilderdialog- DialogBuildertext_input- TextInputBuilderform- FormBuilderslider- SliderBuilderprogress- ProgressBarBuilderpanel- PanelBuilderlabel- LabelBuilderseparator- SeparatorBuildercheckbox- CheckboxBuildernumber_input- NumberInputBuilder (depends on text_input)dropdown- DropdownBuildercleanup- Generic cleanup systems
Why Choose bevy-ui-builders?
We Fill The Gap
| Crate | Problem |
|---|---|
bevy_ui_builder |
Too basic, no text inputs or forms |
sickle_ui |
Complex macro system, steep learning curve |
bevy_dioxus |
Requires React/web knowledge |
bevy_egui |
Immediate mode, not native Bevy |
| bevy-ui-builders | Just right! Simple, powerful, pure Bevy |
Unique Features
- Automatic cleanup system - Generic
despawn_ui_entities<T> - Consistent styling - Centralized colors and dimensions
- Rich text inputs - Filtering, validation, focus management
- Type-safe markers - Add your own components to any builder
- Gateway architecture - Clean module boundaries
Bevy Version Support
| bevy-ui-builders | Bevy |
|---|---|
| 0.2 | 0.17 |
License
Dual-licensed under either:
- MIT license (LICENSE-MIT)
- Apache License, Version 2.0 (LICENSE-APACHE)
at your option.
Contributing
Contributions welcome! Please check CONTRIBUTING.md for development setup, coding standards, and how to submit pull requests.
Credits
Created by Noah Sabaj, the creator of bevy-plugin-builder, and bevy-test-suite.
Bottom Line
// From 45 lines to 3. That's the power of builders.
ButtonBuilder::new("Play Game")
.style(ButtonStyle::Primary)
.build(parent);
Dependencies
~66–105MB
~2M SLoC