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:

Rust
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.

Rust
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:

Rust
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.

Rust
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.

Rust
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.

Rust
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.

Rust
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.