bevy-ui
Use when building Bevy 0.20 UI with `Node`, `ui_widgets::Button`, `Hovered` / `Pressed` / `Activate`, `children![]`, `TextFont` with `FontSource` and `FontSize`, `BorderRadius` / `CornerRadius`, `BackgroundColor`, `BorderColor`, `InputFocus`, or `AccessibleLabel`, or when `ui::Interaction` and `Changed<Interaction>` show deprecation warnings.
How do I install this agent skill?
npx skills add https://github.com/chrisgliddon/bevy-skills --skill bevy-uiIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides comprehensive documentation and code examples for building user interfaces with Bevy 0.20. It covers layout, interaction, text styling, and accessibility using modern Bevy patterns. No security risks were identified.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
Bevy 0.20 — UI
When to use this skill
- Spawning any
Node-based UI element (panels, buttons, text labels, overlays). - Reacting to a button:
Hovered,Has<Pressed>, and theActivateevent. - Styling text with
TextFont { font: FontSource, font_size: FontSize, .. },TextColor,TextShadow. - Setting
BackgroundColor,BorderColor, orNode::border_radius(BorderRadiusofCornerRadius). - Using palette constants from
bevy::color::palettes. - Wiring
InputFocus,TabIndex, orAccessibleLabelfor keyboard and screen-reader support. - Nesting child entities with
children![]or.with_children(...). - Migrating code that used
ui::Interaction,Changed<Interaction>,font_size: 33.0, orfont: handle(0.18 forms).
Canonical pattern
Headless ui_widgets::Button, styled by polling Hovered and Pressed, clicked via Activate. Mirrors examples/ui/widgets/button.rs in Bevy 0.20.1.
use bevy::{
color::palettes::basic::*,
picking::hover::Hovered,
prelude::*,
ui::Pressed,
ui_widgets::{observe, Activate, Button},
};
fn main() {
App::new()
// DefaultPlugins already includes UiWidgetsPlugins and InputFocusPlugin.
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, update_button_appearance)
.run();
}
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
commands.spawn(Camera2d);
commands.spawn((
// Full-screen flex container that centres the button.
Node {
width: percent(100),
height: percent(100),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
children![(
button(&assets),
// `Button` fires `Activate` on a completed click (or Enter/Space when focused).
observe(|_activate: On<Activate>| {
info!("Button clicked!");
}),
)],
));
}
fn button(asset_server: &AssetServer) -> impl Bundle {
(
Button, // headless widget: tracks `Pressed`, emits `Activate`
Hovered::default(), // picking backend keeps this up to date
Node {
width: px(150),
height: px(65),
border: UiRect::all(px(5)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border_radius: BorderRadius::MAX, // pill
..default()
},
BorderColor::all(Color::BLACK),
BackgroundColor(NORMAL_BUTTON),
children![(
Text::new("Button"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), // Handle -> FontSource
font_size: FontSize::Px(33.0),
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
TextShadow::default(),
)],
)
}
/// Poll `Hovered` + `Has<Pressed>` every frame and pick a look.
fn update_button_appearance(
mut buttons: Query<
(&Hovered, Has<Pressed>, &mut BackgroundColor, &mut BorderColor, &Children),
With<Button>,
>,
mut text_query: Query<&mut Text>,
) {
for (hovered, pressed, mut color, mut border_color, children) in &mut buttons {
let Ok(mut text) = text_query.get_mut(children[0]) else { continue };
match (hovered.get(), pressed) {
(_, true) => {
**text = "Press".to_string();
*color = PRESSED_BUTTON.into();
border_color.set_all(RED);
}
(true, false) => {
**text = "Hover".to_string();
*color = HOVERED_BUTTON.into();
border_color.set_all(WHITE);
}
(false, false) => {
**text = "Button".to_string();
*color = NORMAL_BUTTON.into();
border_color.set_all(BLACK);
}
}
}
}
Topics
| Topic | Reference |
|---|---|
Node, Val (px, percent, em, rem, vw, vh), flex recipes | references/layout.md |
Text::new, TextFont, FontSource, FontSize, TextLayout::justify | references/text.md |
ui_widgets::Button, Hovered, Pressed, Activate, observers vs polling | references/interaction.md |
BackgroundColor, BorderColor, BorderRadius / CornerRadius | references/colors-and-borders.md |
bevy::color::palettes::{basic,css,tailwind} | references/palettes.md |
InputFocus, FocusCause, TabIndex, AccessibleLabel | references/accessibility.md |
children![] vs .with_children(...) | references/children-macro.md |
| Cross-cutting traps and stale-LLM output | references/gotchas.md |
Gotchas
ui::Interactionandui::widget::Buttonare deprecated in 0.20. Both still sit inbevy::prelude, soButtonfrom the prelude is the old one. Importbevy::ui_widgets::Buttonexplicitly. ReplaceChanged<Interaction>matches with&Hovered+Has<Pressed>(both are not in the prelude:bevy::picking::hover::Hovered,bevy::ui::Pressed).- Clicks are an event, not a state.
Activatefires once per completed click;Pressedis only present while the pointer is held. Attachobserve(|_: On<Activate>| ..)in the bundle orcommands.entity(e).observe(..). button.set_changed()is gone with the old button.ui_widgets::ButtonrequiresAccessibilityNode(Role::Button); addAccessibleLabel("...")for the spoken name. No manual dirty-marking.InputFocusis inserted byDefaultPlugins(0.19+). Do notinit_resourceit. Fields are private:get(),set(entity, FocusCause::Pressed),clear().FocusCausegainedAutoin 0.20; exhaustive matches must add it.TextFontfields changed in 0.19.font: Handle<Font>becameFontSource(handle.into(),FontSource::family("Fira Sans"),FontSource::sans_serif());font_size: f32becameFontSize::Px(f32).TextFont::default()isFontSize::Rem(1.)in 0.20 (20 px viaRemSize), so setFontSize::Px(20.)for a fixed size.FontSource::SansSerif(0.19) isFontSource::sans_serif()in 0.20.BorderRadiusfields areCornerRadiusin 0.20.BorderRadius::MAXandBorderRadius::px(..)are unchanged;BorderRadius::all(px(6))works viaInto<CornerRadius>; literal structs needCornerRadius::circular(px(10.)).Val::Em/Val::Remare new in 0.20.Val::resolvetakesEmSizeandRemSizeas two extra arguments.- Flat pointer events in 0.20.
On<Pointer<Click>>isOn<PointerClick>;press.pointer_location.positionispress.pointer.position. uiis not implied by2d/3dsince 0.19. Withdefault-features = false, add theuifeature collection (it also carriesbevy_ui_widgetsandui_picking).TextLayout::new_with_justifyisTextLayout::justify(0.19).Justifyis in the prelude.
See also
bevy-migration-0-19-to-0-20— full list of the button, pointer-event, andValchanges.bevy-cameras— spawn aCamera2dalongside any UI scene.bevy-ecs-queries—Has<T>,Changed<T>, and query filters used above.bevy-fluent— localized UI text viaFluentText<T>.bevy-cargo-features— theuicollection and what it enables.
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/chrisgliddon/bevy-skills/bevy-ui">View bevy-ui on skillZs</a>