Thanks for considering a contribution. The design system is small (~1700 lines USS + 180 lines C#) and intentionally so — every rule earns its keep against the showcase, against the live game, or against a documented Unity quirk. Read the existing files before adding new ones; they double as documentation.
- One style for one job. A new component should not duplicate an existing one with a different prefix. If you need a "card with a price tag", extend
.ds-animal-cardwith a__price-tagslot, don't fork it. - Tokens, never hex. Every colour, radius, spacing, motion timing must reference a
var(--…)fromDesignTokens.uss. If your design needs a value that doesn't exist as a token, add the token first (one PR), then the rule (next PR). - Comments answer "why", not "what". A reader can see a
width: 18px;declaration. The comment should explain why 18px and not 16 or 20 — for example "= half of the 36 px button so the icon centres withoutalign-items: centeron every consumer." - The showcase is the test suite. Every new component must appear in
DesignSystemShowcase.uxmlin at least one state. If your rule has hover / pressed / active / disabled /--variantmodifiers, render each one. PRs that don't update the showcase get bounced. - No
var(...)in inline UXML styles. Unity 6's clone-timeStyleVariableResolverNREs when it encountersvar(...)inside astyle="..."attribute on a UXML element — it crashes the wholeVisualTreeAssetclone with "The UXML file set for the UIDocument could not be cloned." Author colour and dimension overrides as USS classes (e.g..ds-scrollbar-demo,.ds-swatch--primary) and reserve inline styles for one-off literal values. Inside the USS files themselves,var(...)works perfectly.
The system uses BEM-ish naming with a ds- prefix:
.ds-<block> e.g. .ds-btn, .ds-card, .ds-toast
.ds-<block>__<element> e.g. .ds-toast__icon, .ds-card__footer
.ds-<block>--<modifier> e.g. .ds-btn--primary, .ds-toast--success
.ds-<block>.is-<state> e.g. .ds-tab.is-active, .ds-pagination__btn.is-active
State classes (.is-active, .is-loading, .is-disabled) are prefixed is- to distinguish them from variant modifiers.
For icons: .ds-icon--<glyph-name> resolves the SVG via resource("Textures/Icons/<glyph-name>"). Hyphenate multi-word names (.ds-icon--more-horizontal for more_horizontal.svg — the USS class uses hyphens, the file uses underscores; the mapping is explicit in Icons.uss).
DesignSystem.uss @imports the subsystems in a specific order. Specificity ties resolve by source order, so a rule loaded later wins over a rule loaded earlier with the same selector specificity:
1. DesignTokens.uss → :root variables (no element rules)
2. Typography.uss → text classes
3. Icons.uss → .ds-icon base + parent-state cascade
4. Buttons.uss
5. Inputs.uss → .ds-search__icon overrides .ds-icon size
6. TabsAndFilters.uss
7. Cards.uss → .ds-info-row__icon overrides .ds-icon size
8. Navigation.uss → .ds-nav-item__icon overrides .ds-icon size
9. Badges.uss → .ds-chip__icon overrides .ds-icon size
10. Controls.uss
11. Overlays.uss
12. Feedback.uss
13. Mobile.uss → all .mobile overrides — loaded LAST so they always win
When you add a component file, append it to DesignSystem.uss's import block in the right slot (typically before Mobile.uss). Don't reorder existing imports without reading the comments — Inputs.uss's .ds-search__icon rule is intentionally loaded after Icons.uss so its 18×18 size wins over the default .ds-icon 20×20.
When in doubt, here's the routing:
| Component / class | File |
|---|---|
| Tokens (color, spacing, radius, motion) | DesignTokens.uss |
.ds-h1 / .ds-body-1 / .ds-caption |
Typography.uss |
.ds-icon and per-glyph mappings |
Icons.uss |
.ds-btn and variants |
Buttons.uss |
| Anything that hosts text input | Inputs.uss |
| Tab strips, view toggles, filter rows | TabsAndFilters.uss |
| Bordered surfaces with content (cards, info rows) | Cards.uss |
| Navigation containers (side / rail / bottom / profile) | Navigation.uss |
| Pills, chips, tags, avatars, notification dots | Badges.uss |
| Toggles, checkboxes, radios, sliders, range, progress, scrollbars | Controls.uss |
| Modal / dialog / drawer / toast / empty-state | Overlays.uss |
| Pagination, stepper, spinner, skeleton, swatch + section helpers | Feedback.uss |
Anything .mobile-prefixed |
Mobile.uss |
Showcase-only theme overrides (.theme-light, universal transitions) |
Assets/Showcase/Resources/ShowcaseTheme.uss |
If your new rule doesn't fit any file cleanly, you've probably invented a new component family. Make a new file (<Family>.uss), add the import to DesignSystem.uss, document it in docs/COMPONENTS.md.
Two ways, depending on how invasive your change is:
Editor only. Open the project in Unity Hub, open Assets/Showcase/Showcase.unity, hit Play. The bootstrap loads the showcase and your USS edits are reflected on first frame. Hover any element to see its selector chain.
WebGL build (matches what visitors see). From the repo root:
.\Tools\Build\Build-Showcase.ps1 -ServeBuilds the showcase to build/WebGL/ and serves it at http://localhost:3000. First build takes ~5 min (Unity asset reimport); subsequent builds with the Library cache warm take ~2 min. Use Chrome DevTools' device toolbar to verify the .mobile flip and the styled scrollbar at narrow widths. See Tools/Build/README.md for the full pipeline.
- Drop the SVG into
Assets/DesignSystem/Resources/Textures/Icons/. - Make sure the SVG fills are
white—fill="white"andstroke="white". Black-fill SVGs render black regardless of tint because-unity-background-image-tint-coloris multiplicative (black × any_colour = black). The bulk-conversion script in our 2026-05-01 commit converted 63 source icons; new contributions must arrive white-filled. - After Unity reimports, set the importer's
SVG Typeto Texture (not Sprite, not VectorImage). The asset pipeline flag issvgType: 3in the.svg.metafile. - Add one line to
Icons.uss:.ds-icon--newglyph { background-image: resource("Textures/Icons/newglyph"); }
- Render it in
DesignSystemShowcase.uxmlunder the ICONS section. - Open a PR with a screenshot of the showcase row.
- Sketch the DOM. Decide which existing element type owns the block.
.ds-btnis a<ui:Button>;.ds-chipis a<ui:VisualElement>with two children. Decide before you write USS. - Write the rule. Use existing tokens. Reference existing component patterns (status chips for "icon + label" layout, modals for "header + body + actions" scaffolding).
- Add hover / active / disabled / state variants if the component is interactive. Don't ship a button without a hover state.
- Add a mobile override in
Mobile.ussif the component changes shape on touch — typically growing the tap target to 48 px. - Render it in the showcase in the appropriate section.
- Document it in
docs/COMPONENTS.mdwith a one-line description and the expected DOM.
- Rule uses tokens, no inline hex / px / ms (except where commented as load-bearing).
- Showcase UXML updated with all states / variants of the new rule.
- No
var(...)in inline UXMLstyle="..."attributes (use a class). -
Mobile.ussupdated if the component has a touch-tier override. -
docs/COMPONENTS.mdline added (or relevant doc updated). -
CHANGELOG.mdentry under the unreleased section. - No
using LeapOfLegends.*or product-specific imports in C# changes. - No
Resources.Load<Texture2D>in new C# — icons resolve via USSresource(...). - Tested in the editor with the showcase scene; tested at desktop and
.mobilewidths. - Tested via
Tools\Build\Build-Showcase.ps1 -Serveand confirmed the rendered WebGL build matches the editor (catchesvar()-in-inline-UXML crashes and mobile-breakpoint regressions).
If a component looks wrong:
- Reproduce in the showcase if possible. If the bug only appears in your project, attach a minimal UXML.
- Include Unity version. Unity 6 USS additions (
background-size,background-position,@import) behave differently across6000.0.xpatch releases. - Note your render pipeline. URP / HDRP / built-in all share the UI Toolkit panel renderer, but font rendering differs.
- Screenshot the broken state next to the showcase's expected state. A diff is worth a thousand words.
By contributing you agree your contributions are released under the project's MIT licence. See LICENSE.