Tangible UI class naming and CSS structure
Tangible UI aims to be predictable to work with, hard to accidentally override, and polite to the host theme. This document describes how class names are structured, how styles are scoped, and how to extend or override them safely.
Prefixes and high‑level conventions
Tangible UI uses three layers of naming to keep styles organized and collision‑free:
- Interface wrapper – scopes all Tangible UI styles
- Block / component classes – identify the UI pattern
- Element and modifier classes – identify sub‑parts and variations
Interface wrapper: .tui-interface
All Tangible UI components are expected to live inside a wrapper with the class .tui-interface. This wrapper is the primary guardrail against leaking styles into the rest of a site, and it also gives us a consistent way to increase specificity when we need to.
Example markup:
<div class="tui-interface">
<div class="tui-card tui-card__body">
...
</div>
</div>
Example SCSS:
.tui-interface {
// global resets and layout helpers for Tangible UI only
.tui-card {
// card styles
}
}
When writing new component styles, assume they will always be rendered inside .tui-interface and keep selectors nested under that wrapper in SCSS.
Component blocks: tui-<component>
Components are named with a tui- prefix followed by a descriptive block name:
tui-buttontui-cardtui-chiptui-tabletui-modaltui-progress
This avoids collisions with theme classes like button, card, or table, and makes it obvious which CSS belongs to Tangible UI.
Elements and modifiers
Tangible UI loosely follows BEM-like patterns:
- Element:
tui-card__head,tui-card__body,tui-card__foot - Modifier (state or variant):
.is-primary,.is-secondary,.is-outline,.is-active,.is-disabled
Patterns you will see:
tui-card__head– elementtui-card__body– elementtui-card__foot– elementtui-button is-primary– primary varianttui-button is-outline– outline layouttui-chip is-selectable is-selected– selectable chip in selected state
Modifiers never stand alone. They always appear on top of a base block class.
<button class="tui-button is-primary is-outline">Save changes</button>
Do not create new global modifier names for one‑off cases. Prefer component‑specific modifiers when necessary, or data attributes for very custom states.
Design tokens: --tui-{category}-{axis}-{step}
Token names use one grammar, where the category is a design-language noun — not a CSS property name:
- Categories:
color,theme,spacing,radius,motion,typography,shadow,layer,focus-ring,opacity - Axis: the dimension within the category (
weight,size,line-height,bg,fg,border) - Step: the point on the scale (
sm/md/lg,normal/tight,medium/semibold,base/subtle/strong)
Examples: --tui-typography-weight-semibold, --tui-color-bg-muted,
--tui-theme-primary-subtlest, --tui-spacing-md.
The grammar is what makes token names guessable: if you know
--tui-typography-size-sm, you can derive --tui-typography-weight-medium
without looking it up. The corollaries:
- One name per concept. Never introduce a synonym for an existing token
(the pre-0.0.20 system briefly had both
--tui-font-weight-*and--tui-typography-weight-*; that class of duplication is a defect). - Don't mirror CSS property names.
--tui-typography-weight-*, not--tui-font-weight-*— half the system (theme,spacing) has no CSS property to mirror, so property-mirroring would always mean two grammars. - Component API tokens are namespaced by component instead:
--tui-{component}-{property}(e.g.--tui-button-bg,--tui-pager-item-border). TUI never defines these, only reads them — see the Read-Only Component API pattern in CONTRIBUTING.
Wrapper and specificity strategy
Why the wrapper exists
The .tui-interface wrapper ensures:
- Tangible UI resets and utilities do not leak into the host theme
- Components can rely on predictable base styles
- We can rely on a small, controlled increase in specificity when needed
All Tangible UI SCSS should be written in the form:
:where(.tui-interface) {
.tui-button { ... }
.tui-card { ... }
.tui-table { ... }
}
Avoid targeting elements without the wrapper.
Recommended specificity
Aim to keep selectors shallow and class‑only:
- Typical rule:
:where(.tui-interface) .tui-button - Element rule:
:where(.tui-interface) .tui-card__head - Variant rule:
:where(.tui-interface) .tui-button.is-primary
:where() targets without adding specificity.
Avoid:
- IDs
- Type selectors (
button,h2) as part of non-reset component rules - Deep descendant chains (more than 2–3 classes)
If overriding Tangible UI from a plugin or theme, prefer to match class‑only specificity as well. For example, to override a primary button:
.tui-interface .tui-button.is-primary {
background-color: ...;
}
Structure of Tangible UI styles
Tangible UI SCSS is organized into several layers. The exact file layout may evolve, but the intent stays the same.
System tokens
Located under src/styles/system/:
- Design tokens for color, spacing, typography, radius, shadows, breakpoints
- Palette helpers and mixins
These are the single source of truth for visual decisions. Components and utilities should depend on tokens, not hard‑coded values.
Utilities
Located under src/styles/utilities/.
Utilities are small, single‑purpose classes for layout and spacing – things like:
- Flex and alignment helpers
- Gap and margin helpers
- Typography helpers
- Border and radius helpers
Components
Components are co-located in src/components/*/:
src/components/Button/
├── Button.tsx # React component
├── Button.stories.ts # Storybook stories
├── styles.scss # Component styles
└── index.ts # Barrel export
Component styles mirror the React component structure:
- Block:
.tui-button - Elements:
.tui-button__icon,.tui-button__label - Modifiers:
.is-primary,.is-outline,.is-destructive
See Architecture for full project structure.