Skip to content

Instantly share code, notes, and snippets.

@viridia
Created April 23, 2026 18:29
Show Gist options
  • Select an option

  • Save viridia/c9af2cd6c7227e197d675e5d00ccd879 to your computer and use it in GitHub Desktop.

Select an option

Save viridia/c9af2cd6c7227e197d675e5d00ccd879 to your computer and use it in GitHub Desktop.
Bevy Feathers Design Language

Bevy Feathers Design Language

A formal specification of the design language embodied by the Bevy Editor mockups and the bevy_feathers widget crate. This document exists to make the implicit rules of the design explicit, so that future contributors can add widgets, themes, and layouts that feel cohesive without needing to reverse-engineer the original designer's intuition.

Where the mockups and the existing code disagree, the mockups take precedence. Where both are silent, this document fills the gap and flags it as a new rule.


1. Philosophy

Feathers is a toolkit for building editors, inspectors, and other dense productivity tools — not games. The aesthetic is:

  • Functional over decorative. Controls announce their function clearly and don't compete with the user's content (3D viewports, code, data).
  • Dense but not cramped. Every pixel does work; whitespace exists to group, not to breathe.
  • Consistent over customizable. One way to do each thing, applied everywhere.
  • Dark by default. The reference theme is dark because editors are used for extended sessions and often alongside bright content.

Any proposed addition to the design language should be justifiable in these terms. If a widget needs four spacing values and three new colors to look right, the widget is probably wrong, not the language.


2. Design Tokens

Tokens are the atoms of the design language. Every visual property of every widget should resolve through a token rather than a literal value. This is how a theme swap, a density change, or an accessibility adjustment can propagate through the whole system.

2.1 Spacing Scale

All spacing — padding, gaps, margins — is a multiple of 6 pixels. This is the base rhythm of the entire UI.

Token Value Typical use
space::XS 6px Gap between icon and label, inner padding of tight controls
space::SM 12px Gap between rows, padding inside panels
space::MD 18px Gap between grouped sections
space::LG 24px Margin around panel contents
space::XL 30px Separation between major regions

Rule: if you reach for a spacing value not on this scale, you are either doing something wrong or discovering a new rule. In the second case, propose an addition to the scale rather than adding a one-off literal.

Note on the mockups: the reference image contains values like 13, 15, 22, 25, 26. With the exception of ROW_HEIGHT (26, see §2.3), these are treated as drift from a strict scale and should be normalized to the nearest 6-multiple unless a specific optical adjustment is documented.

2.2 Border Geometry

Token Value Use
BORDER_RADIUS 5px All rounded corners, universally
BORDER_THICKNESS 1px All borders, universally

One radius, one thickness. No exceptions without a design review.

2.3 Sizing

Token Value Use
ROW_HEIGHT 26px Height of every interactive control: buttons, inputs, tabs, dropdowns, sliders
ICON_SIZE 18px All icons, square
CHECKBOX_SIZE 16px Width and height of a checkbox (existing)
RADIO_SIZE 16px Width and height of a radio button (existing)
TOGGLE_WIDTH Width of a toggle switch (existing)
TOGGLE_HEIGHT Height of a toggle switch (existing)

ROW_HEIGHT is the single most important number in the system. It is what makes a row of controls align regardless of which controls they are. Every interactive atom respects it.

With 18px icons inside 26px rows, vertical padding is 4px — this is an intentional off-scale value (the scale is 6-based) that produces correct optical centering. It is the one documented exception to the 6px rule.

2.4 Typography

Token Value Use
FONT_SIZE_BODY 12px Labels, values, most UI text
FONT_SIZE_HEADING 12px Panel headers (currently same as body; may diverge)

The design uses one visual size and distinguishes hierarchy through weight, color, and placement rather than size. FONT_SIZE_HEADING is reserved as a separate token so it can diverge later without a migration.

Font asset paths live in constants::fonts.

2.5 Color Palette

The palette is the lowest color layer — literal OKLCH/hex values with no semantic meaning. Widgets never reference the palette directly; they reference tokens (§2.6), which reference the palette.

Using OKLCH means perceived lightness can be adjusted predictably across any hue. State derivations (hover, pressed, disabled) are defined as lightness-channel shifts so they remain consistent regardless of the base color.

Palette entry Role description
GRAY_0 Window background (darkest surface)
GRAY_1 Pane background
GRAY_2 Item background (inputs, buttons)
GRAY_3 Item background, active
WARM_GRAY_1 Borders
LIGHT_GRAY_1 Primary text
LIGHT_GRAY_2 Secondary / dimmed text
WHITE Button label text (on accent)
BLACK Reserved
ACCENT Call-to-action and selection
X_AXIS Red, for X-axis inputs
Y_AXIS Green, for Y-axis inputs
Z_AXIS Blue, for Z-axis inputs

Surfaces form a four-step hierarchy getting lighter as you nest: window → pane → item → item-active. Nesting a fifth level is a signal that the layout is wrong, not that a fifth color is needed.

Missing from today's palette, proposed additions:

Palette entry Purpose
DANGER Error states, destructive action
WARNING Validation warnings
SUCCESS Confirmations, healthy status
INFO Informational highlights

These are needed the moment widgets handle validation, build output, or async operations. Specifying them now prevents one-off reds and yellows scattered through the codebase later.

2.6 Semantic Tokens

Tokens assign palette colors to specific visual roles. This is the layer widgets consume. Existing tokens follow the pattern {WIDGET}_{PROPERTY}[_STATE], e.g. BUTTON_BG_HOVER, CHECKBOX_BORDER_DISABLED.

Existing coverage: globals (WINDOW_BG, TEXT_MAIN, TEXT_DIM, FOCUS_RING), plus full state sets for button, checkbox, radio, slider, toggle switch, and color plane.

Proposed additions (all required by the mockup but currently absent or under-specified):

Surface hierarchy:

  • PANE_BG — background of a docked panel
  • ITEM_BG, ITEM_BG_HOVER, ITEM_BG_ACTIVE — background of individual rows in trees, lists, asset grids
  • PANEL_HEADER_BG — the darker strip at the top of Node / Scene Tree / Reflectable Component panels

Structure:

  • BORDER_DEFAULT — the standard 1px border
  • BORDER_FOCUS — border when a container owns keyboard focus
  • SELECTION_OUTLINE — persistent selection indicator (distinct from momentary focus ring)

Text:

  • TEXT_HEADING — panel titles, section labels (may alias TEXT_MAIN initially)
  • TEXT_DISABLED — explicit disabled text token (currently handled per-widget)

Status:

  • STATUS_ERROR, STATUS_ERROR_BG
  • STATUS_WARNING, STATUS_WARNING_BG
  • STATUS_SUCCESS, STATUS_SUCCESS_BG
  • STATUS_INFO, STATUS_INFO_BG

Tree and list rows:

  • TREE_ROW_BG, TREE_ROW_BG_HOVER, TREE_ROW_BG_SELECTED

Axis-tinted inputs (Vec2/Vec3/Vec4):

  • VEC_X_ACCENT, VEC_Y_ACCENT, VEC_Z_ACCENT — resolve to the palette X_AXIS/Y_AXIS/Z_AXIS but named by role so Vec widgets don't reach into the palette directly

3. Interaction States

Every interactive widget supports the same set of states. This is non-negotiable: missing a state is a bug, not a choice.

State Trigger Visual treatment
Default Idle Base color from token
Hover Pointer over Base color lightened by ~5% in OKLCH L channel
Focus Keyboard-focused FOCUS_RING outline, 1px, outside border
Pressed Mouse/touch held down Base color darkened slightly, or swapped to pressed token
Selected Persistent selection after click SELECTION_OUTLINE (accent color), replaces focus ring
Disabled Not interactive Desaturated tokens; hover and press are no-ops
Invalid Failed validation (inputs only) Border swapped to STATUS_ERROR; icon or message nearby

Rules:

  • Hover and Focus are independent. A control can be both hovered and focused; both visuals apply.
  • Selected outranks Focus for display. If a control is selected, show the selection outline rather than the focus ring. The ring reappears if focus moves to a second selected item (multi-select).
  • Disabled is terminal. A disabled control shows no hover, no press, no focus. The 70% opacity rule from the mockup is a visual fallback; the preferred approach is explicit disabled tokens (*_DISABLED) with colors chosen for legibility.
  • Invalid applies to inputs only. Buttons and toggles don't have an invalid state; their parent form or row does.

Derivation rule for hover colors: a hover token is the base token's color with L (lightness) in OKLCH increased by 0.05. For existing statically-defined hover tokens, this rule was applied by hand. For new additions, apply it the same way and document the source base. A future refactor may derive hover tokens programmatically from base tokens at theme-construction time; the static definitions remain the source of truth until then.


4. Sizing Behavior

Every container widget exposes three sizing modes on each axis, matching the Figma auto-layout model:

Mode Meaning
Fill Stretch to fill the parent along this axis
Hug Shrink to fit content along this axis
Auto Context-dependent default (usually Fill horizontally, Hug vertically)

Modes are specified per-axis, so a panel can be Fill horizontally and Hug vertically. The default for most containers is Auto.

Controls (buttons, inputs, etc.) are always Hug vertically at ROW_HEIGHT. Horizontal sizing varies by context: Fill in property rows, Hug in toolbars.


5. Widget Anatomy

The mockup's recurring structural patterns. New widgets should compose from these primitives rather than invent new ones.

5.1 PropertyRow

The workhorse of every inspector. A horizontal row with:

  • A fixed-width label on the left (TEXT_DIM, FONT_SIZE_BODY, right-aligned)
  • A space::XS gap
  • A control on the right, sized Fill horizontally, Hug vertically at ROW_HEIGHT

All rows within a single panel share the same label-column width, so controls align vertically down the panel.

5.2 CollapsiblePanel

A container with:

  • A header strip at PANEL_HEADER_BG, ROW_HEIGHT tall, containing: chevron (▸/▾), optional icon, title text (TEXT_HEADING), optional trailing controls (overflow , visibility eye, etc.)
  • A body below the header, PANE_BG, with space::SM internal padding
  • Smooth expand/collapse; collapsed state shows header only

Sections within a panel can nest, but the header background alternates or indents to preserve the four-step surface hierarchy.

5.3 Pane

A top-level dockable region. PANE_BG, 1px BORDER_DEFAULT, BORDER_RADIUS corners (only on free edges, square where docked). Contains CollapsiblePanels or direct content.

5.4 Toolbar

A horizontal strip of icon buttons and segmented controls, ROW_HEIGHT tall, space::XS gaps between items, space::SM gaps between groups.

5.5 Tab Bar

Horizontal row of tabs at ROW_HEIGHT. Active tab uses ITEM_BG_ACTIVE; others use ITEM_BG. A trailing + adds a new tab.

5.6 Tree View

Vertical list of expandable rows. Each row:

  • ROW_HEIGHT tall
  • Left indent proportional to depth (space::SM per level)
  • Chevron → icon → label → optional trailing controls
  • TREE_ROW_BG default, TREE_ROW_BG_HOVER on hover, TREE_ROW_BG_SELECTED when selected

Multi-select is supported; selected rows all share the selected background, and the focused row additionally gets SELECTION_OUTLINE.

5.7 Asset Grid

Grid of tiles with icon on top, label below. Tile size is consistent within a grid; space::SM gap between tiles. Selection uses the same outline rules as tree rows.


6. Container Hierarchy

Surfaces nest in this order, each step one palette level lighter:

Window    (GRAY_0, WINDOW_BG)
 └─ Pane  (GRAY_1, PANE_BG)
     └─ Panel      (header: PANEL_HEADER_BG, body: PANE_BG)
         └─ Row    (ITEM_BG on hover/active; transparent otherwise)
             └─ Control  (ITEM_BG for inputs; BUTTON_BG for buttons; etc.)

Going deeper than Control means you're building a new widget, not nesting further. The hierarchy is four levels because the palette supplies four surface colors; adding a fifth is a design-review question.


7. Widget Inventory

Tracking what exists and what the mockups imply.

Implemented in bevy_feathers today

  • Button (regular + primary variants, full state set)
  • Checkbox
  • Radio button
  • Slider
  • Toggle switch
  • Color plane, color slider, color swatch

Implied by the mockup, not yet implemented

  • Inputs: text input, numeric input (with type hints I32/F32/etc.), Vec2/Vec3/Vec4 input (axis-tinted), enum dropdown, file picker, bool (beyond checkbox — the mockup shows a row-styled bool)
  • Containers: Pane, CollapsiblePanel, PropertyRow, Toolbar, TabBar, MenuBar, StatusBar
  • Navigation / data: TreeView, AssetGrid, Breadcrumb
  • Special: ViewportGizmo (3D nav cube), reflection-driven component inspector (the "Reflectable Component" card)

Each of these should be implemented as a composition of the primitives in §5, using only tokens from §2.6 and states from §3. If an implementation needs a token that doesn't exist, that's a proposal for §2.6, not a reason to hardcode.


8. Adding to the Design Language

When you need something new:

  1. Check if it already exists in a token, a primitive, or a widget. Most needs are covered.
  2. If it's a new color role, add a token in §2.6 referencing an existing palette entry. Only add a palette entry (§2.5) if no existing color works.
  3. If it's a new spacing, check the scale in §2.1 first. New scale steps require a design review.
  4. If it's a new state, it should apply to all interactive widgets — not just yours. Propose it for §3.
  5. If it's a new primitive, document it in §5 with anatomy before building downstream widgets on it.
  6. If it's a new widget, add it to §7 and verify it composes from §5 primitives using §2.6 tokens only.

Pull requests that hardcode colors, spacings, radii, or sizes should be rejected on sight. The whole point of the token system is that these values live in exactly one place.


9. Open Questions

Things this document has taken a position on that may warrant further discussion:

  • Hover derivation: static vs algorithmic. Today's tokens hardcode each hover color. The +5% OKLCH-L rule could be applied programmatically at theme-construction, which would eliminate a whole class of drift but adds a build-time dependency on color-space math.
  • Font size divergence. Reserving FONT_SIZE_HEADING separate from FONT_SIZE_BODY even though they're equal today — is the intent that they stay equal, or eventually diverge?
  • Status palette. Adding DANGER/WARNING/SUCCESS/INFO is proposed here but not in the mockups. The designer should weigh in on hues so they sit correctly alongside ACCENT and the axis colors.
  • Off-scale ROW_HEIGHT. 26px breaks the 6-based scale. Changing to 24px would restore the scale but requires redrawing every control. Documented as an intentional exception; worth revisiting if the scale ever changes.
  • Mockup drift. Values in the reference image at 13, 15, 22, 25 — normalize to 6-multiples, or preserve as intentional optical adjustments? This document assumes normalize; confirm with the designer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment