Component reference

July 15, 2026 · View on GitHub

One-line summary per component class. The showcase UXML (Assets/Showcase/Resources/DesignSystemShowcase.uxml) is the second source of truth — every class here appears there with its expected DOM structure and at least one rendered state.

Screen root

ClassUse
.ds-rootTopmost element of any screen. Cascades the tokens and paints --color-bg edge to edge.
.ds-root--hudCompose with .ds-root for UI that floats over gameplay — transparent background, content-sized.

Every screen needs ds-root: it is the scope the rest of the system hangs off — the text ramp, plus the scrollbar, focus-ring and transition families, which all select through it. (The tokens themselves are declared in a :root block in DesignTokens.uss; they cascade from the topmost element, which is where ds-root goes.) But ds-root also paints an opaque background, which is right for a full-screen menu and wrong for a health bar. A HUD, a minimap, a crosshair, a damage vignette — anything the game world must show through — carries class="ds-root ds-root--hud".

Buttons

ClassUse
.ds-btnBase class; carries 1 px transparent border so widths match across variants.
.ds-btn--primaryGreen CTA, on-accent (dark) text.
.ds-btn--secondaryBlue secondary action.
.ds-btn--tertiaryPurple tertiary action.
.ds-btn--ghostTransparent fill with a 1 px text-secondary border.
.ds-btn--dangerRed destructive action.
.ds-btn--icon36 × 36 square button for icon-only actions; child .ds-icon is auto-centred.
.ds-btn--icon-dangerRed-tinted icon button (e.g. trash).
.ds-btn--sm28 px height.
.ds-btn--lg44 px height.
.ds-btn--blockFull-width.
.ds-btn--pressedForce the :active look (locked state).

DOM:

<ui:Button text="Save" class="ds-btn ds-btn--primary" />
<ui:Button class="ds-btn ds-btn--icon"><ui:VisualElement class="ds-icon ds-icon--search" /></ui:Button>

Inputs

ClassUse
.ds-inputSingle-line <TextField>; matches every Unity 6 inner-class variant.
.ds-input.is-activeForce the focus look (locked state, e.g. for a placeholder demo).
.ds-input--errorRed border for validation errors.
.ds-searchWrapper that adds a leading-icon slot before a transparent inner field.
.ds-search__iconThe leading glyph (18 × 18).
.ds-search__fieldThe inner <TextField>; its box is stripped so it disappears into the wrapper.
.ds-search__clearTrailing clear-button slot (16 × 16).
.ds-dropdown<DropdownField> with chevron and value text styled.
.ds-textareaMultiline <TextField> with min-height 96 px.
.ds-textarea__wrapWrapper if you need a counter row below.
.ds-textarea__counterThe "0/120" caption.

Set placeholders via field.textEdition.placeholder = "..." in C#. Unity 6's API supports hidePlaceholderOnFocus = true to clear on focus.

<ui:DropdownField choices="Low,Medium,High,Ultra" index="3" class="ds-dropdown"/>

DropdownField.value is not a UXML attribute. Unity ignores it without a word and the field renders blank — the single most common way a Settings screen ships with empty dropdowns. index is zero-based and must be a valid position in choices.

The open menu (unity-base-dropdown__*) is parented by Unity as a sibling of .ds-root, not a descendant. Stylesheets a UXML <Style> tag imports scope to that UXML's subtree, so no design-system rule can reach the popup and no markup a screen author writes will change that. Popup chrome therefore ships as its own sheet, which the host attaches at panel scope, once per UIDocument, after the first layout pass — together with the token block its colours reference:

var panelScope = doc.rootVisualElement.parent
              ?? doc.rootVisualElement.panel?.visualTree;
foreach (var path in new[] { "UI/Styles/DesignSystem/DesignTokens",     // so var(--color-*) resolves...
                             "UI/Styles/DesignSystem/DropdownPopup" })  // ...for the chrome that uses it
{
    var sheet = Resources.Load<StyleSheet>(path);
    if (panelScope != null && sheet != null && !panelScope.styleSheets.Contains(sheet))
        panelScope.styleSheets.Add(sheet);
}

Every panel needs its own attach — a world-space UI is one panel per quad, not one for the scene. Skip it and dropdowns open Unity-grey with chunky default scrollbars. This is a host responsibility, not a screen bug.

Theming the popup. The chrome uses var(--color-*), and custom properties inherit, so defining the tokens at panel scope is what colours the popup — the panel root is its parent. To make it follow a theme, add that theme's stylesheet at panel scope too, exactly as you add it to the document root:

ThemeRuntime.Apply(doc.rootVisualElement, tokyo);   // the page
ThemeRuntime.Apply(panelScope, tokyo);              // ...and the popup, which is not inside the page

A ThemeData compiles to a pure {scope} { --token: value } block, which is why hoisting one is safe. Never hoist the full DesignSystem.uss: its component rules start matching Unity's internal popup elements and quietly break the popup's own layout.

Theming the popup when there is no stylesheet. The above is the whole story for any palette that exists as an asset. It does not work for a palette invented at runtime — Unity cannot set a custom property from C#, and a player build cannot compile a StyleSheet from a string, so there is nothing to hoist. Such a palette has to be stamped inline, and the popup is the one element a tree-walking stamper cannot reach: Unity builds it fresh under the panel root on every open and destroys it on close, so at the moment your stamp runs it does not exist. Subscribe instead:

DesignSystemEvents.DropdownPopupOpened += (field, menu) =>
{
    // Fires once per popup instance, after the design system has placed and sized it.
    // Nothing to unregister afterwards: the popup is transient.
};

Two things to know if you take this path. An inline background outranks the stylesheet, so the moment you stamp a row's resting colour you own its :hover and :checked too and must drive them from PointerEnter / PointerLeave yourself. And the checked row is items[field.index]: a DropdownField's menu holds one row per choice, in choices order, with no separators. The showcase's Randomize button is the worked example (CodigrateThemeApplier.StampPopup).

Tabs & filters

ClassUse
.ds-tabsContainer; flex-row segmented strip with surface-elev fill.
.ds-tabSingle tab (<Button>); pair with .is-active for the selected state.
.ds-tabpanelsContainer for the tab bodies; put it as a sibling right after .ds-tabs.
.ds-tabpanelOne tab's content; hidden unless .is-active.
.ds-view-toggleContainer for grid/list view switching.
.ds-view-toggle__btnSquare icon button inside the toggle.

A tab strip on its own is a filter row — it styles state and you drive the filtering. To switch content, add a .ds-tabpanels sibling: the Nth .ds-tab shows the Nth .ds-tabpanel, and the runtime (DesignSystemBehaviour.EnsureTabs) wires the clicks with no C# and no ids.

<ui:VisualElement class="ds-tabs">
    <ui:Button text="Graphics" class="ds-tab is-active"/>
    <ui:Button text="Audio"    class="ds-tab"/>
</ui:VisualElement>
<ui:VisualElement class="ds-tabpanels">
    <ui:VisualElement class="ds-tabpanel is-active">...graphics rows...</ui:VisualElement>
    <ui:VisualElement class="ds-tabpanel">...audio rows...</ui:VisualElement>
</ui:VisualElement>

is-active is the only state, so a C# controller can drive the same markup by flipping the class.

Toggles, checks, radios

ClassUse
.ds-toggleiOS-style switch; runtime injects the sliding knob.
.ds-toggle__knobThe white pill — usually injected, can be hand-authored too.
.ds-checkSquare checkbox; the tick icon is constrained via background-size: 12px 12px; so it doesn't overflow the 2 px border.
.ds-radioStandard radio button styled with surface-elev fill + primary inner dot.

Sliders & range

ClassUse
.ds-sliderSingle-value <Slider>; thumb cross-centred via margin-top: -9px.
.ds-slider--filledVariant that highlights the filled portion.
.ds-range<MinMaxSlider>; tracker, dragger, and both thumbs cross-centred via top: 50%; margin-top: -<half>px;.
.ds-progress<ProgressBar>; 8 px tall by default. Title hidden — app chrome, not a game bar.
.ds-progress-rowWrapper that adds a head row with title + percentage.

Meters (health, mana, XP, stamina)

A game bar is not .ds-progress. It is thick enough to read a number on, and the number is centred over the fill.

ClassUse
.ds-meterThe bar. 20 px — the HUD default.
.ds-meter__fillThe filled portion. Drive it with style="width: 62%;".
.ds-meter__labelThe number, centred over the fill.
.ds-meter--sm10 px. A status pip — too short for a label.
.ds-meter--lg24 px. A boss bar or a cast bar.
.ds-meter--secondary --tertiary --warning --dangerFill colour by role.
.ds-meter__fill.is-warning .is-dangerLive states — flip from game code as a value crosses a threshold.
<ui:VisualElement class="ds-meter ds-meter--danger">
    <ui:VisualElement name="hp-fill" class="ds-meter__fill" style="width: 62%;"/>
    <ui:Label text="184 / 240" class="ds-meter__label"/>
</ui:VisualElement>

The label is a sibling of the fill, placed after it. UI Toolkit has no z-index — later in the DOM is the only way to paint over the fill — and a Label inside the fill is clipped to the fill's width and slides around as the value changes.

Do not hand-roll this. The obvious hand-roll is broken and reads as correct:

.xp-track { height: 12px; overflow: hidden; }   /* clips at 12px      */
.xp-text  { position: absolute; top: -2px; }    /* text is ~14px tall */

The label is taller than the track that clips it, so you ship a bar with the top two pixels of some text peeking out of it.

A HUD bar is 20 px. It is not 40. At 40 px with a ds-h3 number on it, two of them own a quarter of the player's screen.

Cards

ClassUse
.ds-animal-cardExample product card; demonstrates layered children + .is-selected / .is-epic modifiers + check pin.
.ds-animal-card__imageTop image area.
.ds-animal-card__rarityTop-right rarity badge slot.
.ds-animal-card__title-rowTitle + accessory icon row.
.ds-animal-card__checkThe selected-state check pin (rendered last so it z-orders on top).
.ds-info-rowTwo-column attribute row (icon+label on left, value on right).
.ds-info-row__leftThe flex-row wrapper for the icon + label.
.ds-info-row__icon16 × 16 leading icon.
.ds-info-row__labelAttribute name (text-secondary).
.ds-info-row__valueValue text (text-primary).
.ds-swatch-rowToken swatch row used in the showcase.
ClassUse
.ds-side-navFull-width vertical nav with icon + label rows. This is a vertical tab strip — see below.
.ds-nav-itemA row inside .ds-side-nav; pair with .is-active.
.ds-nav-item__icon18 × 18 icon.
.ds-nav-item__labelLabel, flex-grows to fill remaining space.
.ds-side-railIcon-only vertical rail; 56 px wide.
.ds-rail-itemA 40 × 40 cell inside the rail.
.ds-bottom-navMobile bottom tab bar; 64 px tall.
.ds-bottom-nav__itemA column-flex tab with icon above label.
.ds-profileAvatar + name + chevron chip used at the bottom of side-nav.
.ds-profile__avatar32 × 32 circular avatar slot.
.ds-profile__nameDisplay name (bold).
.ds-profile__chevronDrop-down indicator.

A side nav is a tab strip

It switches panels through the same mechanism as .ds-tabs: give it a .ds-tabpanels sibling and the Nth .ds-nav-item shows the Nth .ds-tabpanel. Positional — nothing to name, nothing to wire.

<ui:VisualElement style="flex-direction: row;">
    <ui:VisualElement class="ds-side-nav">
        <ui:VisualElement class="ds-nav-item is-active">
            <ui:Label text="Graphics" class="ds-nav-item__label"/></ui:VisualElement>
        <ui:VisualElement class="ds-nav-item">
            <ui:Label text="Audio" class="ds-nav-item__label"/></ui:VisualElement>
    </ui:VisualElement>

    <ui:VisualElement class="ds-tabpanels">
        <ui:VisualElement class="ds-tabpanel is-active"> ...graphics... </ui:VisualElement>
        <ui:VisualElement class="ds-tabpanel">           ...audio...    </ui:VisualElement>
    </ui:VisualElement>
</ui:VisualElement>

Without the .ds-tabpanels sibling a side nav is inert: it highlights on hover and never switches anything, so a Settings screen ships with Audio and Controls permanently unreachable.

Every panel is a real panel. A .ds-tabpanel holding a single <Label text="Audio…"/> is not a tab — it is a tab that looks broken, because clicking it replaces a screenful of content with three words.

Badges & labels

ClassUse
.ds-badgeSmall uppercase pill (22 px tall); pair with rarity variants.
.ds-badge--commonGreen / primary-soft.
.ds-badge--rareBlue / secondary-soft.
.ds-badge--epicPurple / tertiary-soft.
.ds-badge--legendaryAmber / warning-soft.
.ds-tagHabitat-style filled pill (24 px tall).
.ds-tag--amphibious / --aquatic / --nocturnalVariant fills.
.ds-chipStatus pill with leading-icon slot + label (24 px tall).
.ds-chip__icon14 × 14 leading icon.
.ds-chip--equipped / --new / --owned / --limited / --event / --salePer-state colour.
.ds-avatarCircular avatar; pair with --sm (24), --md (40), --lg (56), --xl (72).
.ds-notif-wrapWrapper for an icon + corner notification dot.
.ds-notif-iconThe base icon (e.g. bell).
.ds-notif-dotThe red corner dot; 18 × 18 single-digit, --multi for 2+ digit pill.
.ds-notif-dot__countCount text inside the dot.

Overlays

ClassUse
.ds-modalGeneric modal panel.
.ds-modal__headerTop row with title + close button.
.ds-modal__titleModal title text.
.ds-modal__close24 × 24 icon-button in the header.
.ds-modal__bodyMain content area.
.ds-modal__actionsBottom row of action buttons (flex-row, equal-grow).
.ds-modal__list-rowRow inside a modal for list-style content (sort options, filter checkboxes).
.ds-modal__list-labelLabel inside a list row.
.ds-modal__illustrationCentered illustration block (e.g. for limit reached).
.ds-dialogCentered confirm dialog (slimmer than full modal).
.ds-dialog__title / __message / __actionsDialog slots.
.ds-toastNon-blocking notification toast.
.ds-toast__icon18 × 18 leading icon (auto-tinted by toast variant).
.ds-toast__messageBody text (white-space: normal).
.ds-toast__close18 × 18 trailing close.
.ds-toast--success / --info / --warning / --dangerPer-variant fill + icon tint.
.ds-sheetMobile bottom drawer.
.ds-sheet__handleTop drag-handle pill.
.ds-sheet__rowA single drawer row.
.ds-sheet__row-icon / __row-labelRow slots.
.ds-sheet__row--dangerRed text + icon for destructive rows (e.g. Release).
.ds-emptyEmpty-state block (icon-bg + title + message + CTA).
.ds-empty__icon-bg64 × 64 surface-elev circle behind the icon.
.ds-empty__title / __messageStack of centred labels.
.ds-tooltipFloating info surface; the consumer positions it (mouse-follow / edge-flip). Elevated tier + strong border.
.ds-tooltip__title / __subtitle / __bodyTitle (body-1 bold), subtitle (caption), body (body-2, wraps).
.ds-tooltip__dividerHairline separator between sections.
.ds-tooltip__rowFlex-row stat line; .ds-tooltip__row-label (left) + .ds-tooltip__row-value (right, bold).

Feedback

ClassUse
.ds-spinnerLoader; pair with .is-spinning to start the rotation. The runtime drives the rotation in C#; do not add transition-property: rotate — it would compound across per-frame writes and make the spinner jiggle.
.ds-spinner--lg48 × 48 variant.
.ds-skeletonPlaceholder shape.
.ds-skeleton--cardCard-shaped variant.
.ds-skeleton__shimmerSliding overlay element (auto-injected).
.ds-paginationContainer.
.ds-pagination__btnPage button (32 × 32 square); pair with .is-active.
.ds-pagination__ellipsis"…" between page numbers.
.ds-stepperQuantity selector container.
.ds-stepper__btn− / + button.
.ds-stepper__valueValue display.

Scrollbars

The system styles UI Toolkit's internal scroll classes, scoped to .ds-root so the rules don't bleed into editor windows or other UI Toolkit panels. You don't write a class — any <ui:ScrollView> inside a .ds-root-rooted hierarchy gets the slim themed scrollbar automatically.

ClassUse
.unity-scroller__low-button, .unity-scroller__high-buttonHidden via display: none — no arrow buttons.
.unity-scroll-view__vertical-scroller8 px wide.
.unity-scroll-view__horizontal-scroller8 px tall.
.unity-base-slider__trackerTransparent — thumb floats on the page surface.
.unity-base-slider__draggerThe visible thumb. 4 px radius pill, var(--color-border-strong) background, brightens to var(--color-text-secondary) on hover. Auto-themes through tokens.

To render a framed scroll-view demo (used in the showcase's SCROLLBARS section):

<ui:ScrollView mode="Vertical" class="ds-scrollbar-demo" style="height: 120px; flex-grow: 1;">
    <ui:Label text="..." class="ds-body-2" />
</ui:ScrollView>

The .ds-scrollbar-demo class adds a var(--color-bg) background, var(--color-border) outline, and 8 px padding — useful for surfacing the scrollbar visually in documentation contexts. Lives in Feedback.uss.

Drag & drop

ClassUse
.ds-draggableMarks an element draggable; the runtime auto-wires pointer drag + a ghost.
.ds-drop-zoneA container that accepts a dropped .ds-draggable (reparents the item on drop).
.ds-drop-zone.is-drag-overHighlight applied to a drop zone while a drag hovers it.
.ds-drag-ghostThe floating preview that follows the pointer during a drag. Reuse it from custom drag code (e.g. a game inventory) for a consistent look.

The runtime (DesignSystemBehaviour.EnsureDraggables) auto-wires .ds-draggable for the simple "move between zones" case. Inventories with split / merge / transfer logic drive their own pointer handling and reuse only the .ds-drag-ghost / .is-drag-over visuals.

Icons

.ds-icon is the base class. Pair with one of 120 .ds-icon--<name> classes from Icons.uss:

arrow-up arrow-down arrow-left arrow-right
chevron-up chevron-down chevron-left chevron-right
sort-asc sort-desc

check close info error warning help smile frown

plus edit trash share refresh sync search filter
more-horizontal more-vertical menu list
lock unlock eye settings bell clock calendar user home target

paw shirt hats store cart bag gift heart shield sword
bolt fire flame sparkle sun

leaf tree mountain droplet

mic sound speak nametag

Sizes: --xs (12), --sm (16), default (20), --lg (24), --xl (32), --xxl (48).

Tint variants: --primary (text-primary), --secondary (text-secondary), --disabled, --accent (primary green), --gold (warning amber), --danger, --warning, --info, --on-accent, --rarity-common, --rarity-rare, --rarity-epic, --rarity-legendary.

Typography

ClassStyle
.ds-h126 px / bold
.ds-h220 px / bold
.ds-h316 px / semibold
.ds-body-114 px / regular
.ds-body-212 px / regular
.ds-caption11 px / medium / text-secondary
.ds-text-successHelper colour utility (primary green).
.ds-text-primaryHelper colour utility (text-primary white).
.ds-nowrapOne line, refuses to shrink. Use on any label sitting in a row next to a fixed-size element.
.ds-truncateOne line, takes the space it's given, ellipsizes the rest. For user data of unknown length.
.ds-intlAdvanced text generator: Arabic joining forms, right-to-left reordering, Devanagari clusters. Compose on a root: class="ds-root ds-intl". Costs nothing for Latin-only UI.
.ds-weight-100.ds-weight-900One class per weight the imported family ships. Requires a font family — see below.

Weights need a font family

.ds-h3 is semibold and .ds-caption is medium in the table above, but out of the box both render as plain bold. That is not an oversight: USS has no font-weight property, UI Toolkit has exactly two style slots (normal and bold), and with no font family loaded there is no semibold face to reach for. Worse, -unity-font-style: bold with an unwired weight table does not even give you a real bold — TextCore synthesizes one by dilating the Regular outline.

Import a family (Design System > Google Fonts) and all of that resolves. The generated stylesheet declares --ds-font-100--ds-font-900, points .ds-root at var(--ds-font-body), adds the .ds-weight-* classes, wires each face's fontWeightTable so every existing -unity-font-style: bold rule lands on the family's real Bold, and re-points .ds-h3 at the real 600 and .ds-caption at the real 500.

<ui:Label text="SemiBold" class="ds-body-1 ds-weight-600" />

Full detail in FONTS.md, including why a fallback chain is mandatory for multilingual text and why its order matters.

Prose wraps. Labels do not.

Every class in the table above sets white-space: normal. That is right for a paragraph and wrong for the short fixed labels that make up most of a UI — a slot name, a stat value, a menu entry.

The reason it bites is flexbox, not typography. Inside a flex-direction: row, a Label is flex-shrink: 1 like everything else, so when the row is tight Yoga shrinks the label rather than the fixed-size icon next to it — and a white-space: normal Label that has been shrunk below its text width does not overflow the way a browser would. It wraps. If the box is narrower than the word, it wraps mid-word.

That is how Armor renders as Armo / r under a 56 px slot with 130 px of free space to its right. Nothing overflowed; the label just volunteered to be 30 px wide.

<!-- WRONG: wraps mid-word the moment the row is tight -->
<ui:VisualElement style="flex-direction: row; align-items: center;">
    <ui:VisualElement class="equip-slot"/>
    <ui:Label text="Main Hand" class="ds-caption"/>
</ui:VisualElement>

<!-- RIGHT -->
<ui:VisualElement style="flex-direction: row; align-items: center;">
    <ui:VisualElement class="equip-slot"/>
    <ui:Label text="Main Hand" class="ds-caption ds-nowrap"/>
</ui:VisualElement>

A quest description, a tooltip body, a death-screen blurb: leave those wrapping.

Showcase helpers

These classes exist for the showcase / live demo and aren't part of the drop-in design system per se — but they're useful patterns for anyone building a similar style guide on top of the system. All live in Feedback.uss so they ship in the same import chain.

ClassUse
.ds-sectionBordered section card (background, border, radius, padding) used to group related component demos in the showcase.
.ds-section__titleUppercase / bold / spaced label inside .ds-section.
.ds-rowFlex-row helper with align-items: center — used inside sections to lay out controls in a line.
.ds-row__gap > *Adds margin-right: var(--space-2) to direct children — useful for spaced rows.
.ds-col-gap > *Adds margin-bottom: var(--space-2) — same pattern, vertical.
.ds-swatch-rowFlex-row containing a swatch + name + hex label.
.ds-swatch14 × 14 colour chip used in the COLORS section. Border uses var(--color-border) so it themes correctly.
.ds-swatch__name, .ds-swatch__hexSlots inside a swatch row.
.ds-swatch--<token>One per colour token: --primary, --primary-hover, --secondary, --tertiary, --warning, --danger, --text-primary, --text-secondary, --text-disabled, --bg, --surface, --surface-elev, --border. Each binds the swatch's background to its var(--color-*), so a theme override repaints the COLORS section automatically.
.ds-btn--demo-hoverCombine with a button variant (e.g. class="ds-btn ds-btn--primary ds-btn--demo-hover") to lock the button to its *-hover token. Used in the showcase's BUTTONS section to display Default / Hover / Pressed / Disabled side-by-side without needing the cursor to actually be over the button.
.ds-scrollbar-demoFramed <ui:ScrollView> wrapper — adds var(--color-bg) background, var(--color-border) outline, and 8 px padding. Used in the showcase's SCROLLBARS section.
.showcase-chromeMarks an element (and all its descendants) as showcase page chrome — promo banner, future headers / footers. The selector-chain hover overlay (ShowcaseDocOverlay.cs) skips inspection inside .showcase-chrome. The .ds-h1 colour is locked to a fixed light value under .showcase-chrome regardless of theme so titles stay readable on the always-black banner.