Design Principles
Guidance for building consistent HeroGPUI interfaces in Rust.
Use these ten principles when choosing components, structuring state and shaping an interface. They cover the decisions that keep a HeroGPUI application clear as it grows.
1. Semantic intent over visual style
Choose variants by the action they represent. Use primary for the main action, secondary for an alternative, tertiary for a low-emphasis action, and danger for destructive work. The names communicate hierarchy without relying on color alone:
1// Hierarchy, not appearance.
2Button::new("save").label("Save") // primary
3Button::new("edit").label("Edit").variant(Variant::Secondary)
4Button::new("cancel").label("Cancel").variant(Variant::Tertiary)
5Button::new("del").label("Delete").variant(Variant::Danger)2. Accessibility as foundation
Design keyboard and focus behavior into every interactive flow. Test the behavior users operate directly:
Test the behavior that users operate directly: Escape to dismiss, arrow keys through a menu, and typing into a date-field segment.
3. Composition over configuration
Compose parts through named builder slots: ModalCloseTrigger, CardHeader and InputGroup::prefix attach behavior to the parent. Use the typed component form when a slot carries behavior so the parent can still configure it.
1// The same parts, as slots. `input` takes an `Input`, not an
2// element, so the group can strip the field's own chrome.
3let amount = cx.new(|cx| InputState::new(cx));
4InputGroup::new()
5 .prefix(InputAddon::new("$"))
6 .input(Input::new(amount).placeholder("0.00"))
7 .suffix(InputAddon::new("USD"))4. Progressive disclosure
Start with the constructor and add options only when the interface needs them. The three buttons below show increasing levels of configuration:
1// Level 1
2Button::new("go").label("Click me")
3
4// Level 2
5Button::new("go").size(Size::Lg).child(check).child("Submit")
6
7// Level 3
8Button::new("go").label("Submitting").is_pending(true)5. Predictable behaviour
Keep shared props consistent across the application. size uses sm/md/lg, is_disabled has the same meaning on each control, and callbacks use the component's documented signature.
1// The same three props, on three different components.
2Button::new("b").size(Size::Lg).is_disabled(true)
3Chip::new().size(Size::Lg).child(ChipLabel::new().child("c"))
4Avatar::new("a").size(Size::Lg)
5
6// And one callback shape everywhere.
7.on_change(|value: &str, _window, _cx| { /* ... */ })6. Type safety first
Prefer the Rust types over stringly-typed configuration. A variant is an enum, so a typo is a compile error rather than a silently unstyled control, and an exhaustive match over Variant covers every case.
1// A variant is an enum, so this does not compile:
2// Button::new("b").variant(Variant::Round)
3// ^^^^^ no variant named `Round`
4//
5// and an exhaustive match cannot miss one:
6match variant {
7 Variant::Primary => ..,
8 Variant::Secondary => ..,
9 Variant::Tertiary => ..,
10 Variant::Outline => ..,
11 Variant::Ghost => ..,
12 Variant::Danger => ..,
13 Variant::DangerSoft => ..,
14}7. Separation of styles and logic
Keep shared vocabulary, theme tokens and component implementations separate. herogpui-core provides shared types and color math, herogpui-theme provides tokens, and herogpui-components provides the components. The theme crate has no component code, so other widgets can read the same tokens.
1herogpui-core // Color, Variant, Size, oklch(), mix_oklab()
2herogpui-theme // the tokens + ThemeProvider (no component code)
3herogpui-components // the components
4herogpui // umbrella re-export
5
6// Read a token without touching a component:
7let accent = cx.role(Color::Accent).color;
8let radius = herogpui::components::util::field_radius(cx);8. Developer experience
Use rustdoc for builder-level API details and the gallery for runnable examples. The gallery has one page per component and includes the documented examples.
9. Complete customization
Start from a built-in theme and override a base token when your application needs a different value. Derived colors follow through the same color-mix rules used by the theme.
1// Override one base token; every derived value follows.
2let violet = Theme::builder("violet", Theme::light())
3 .accent(oklch(0.55, 0.23, 295.0))
4 .build();
5
6// `accent.hover()` and `accent.soft()` are the same color-mix
7// expressions, so they move with the base color.10. Open and extensible
The tokens, color math and motion curves are public. A component outside this crate can read cx.colors(), use util::field_radius(cx) for its corners and animate with Motion::LIST_IN.
Use the supported vocabulary
Use semantic roles, surfaces and the builder methods documented for each component. For example, isPending is spelled is_pending in Rust; the component reference is the source of truth for every available option.