Component Anatomy
How Chassis CSS components are built — CSS variables, placeholder extends, and a small set of mixins that turn design tokens into ready-to-use classes.
Every Chassis CSS component follows the same recipe: a base class declares CSS variables (sourced from design tokens), extends placeholder selectors to inherit shared behavior, and calls a handful of mixins to apply those variables to actual properties. The pattern keeps source short, output consistent, and the result fully theme-aware.
Two real components serve as worked examples — .notification (a self-contained context-aware container) and .list-action (an interaction modifier that layers onto an existing component).
The recipe
A typical component is built from four parts.
1. CSS variables, sourced from tokens. The class declares its own CSS variables and points each at a design-token value with the Sass token as the compiled fallback. This is what makes the component customizable: any consumer can override the variable on an ancestor or on the element itself without touching the source. SCSS source omits the cx- prefix — postcss-prefix-custom-properties adds it at build time, so --notification-bg-color in SCSS becomes --cx-notification-bg-color in compiled CSS. See Design Tokens for the token side of this.
2. Placeholder extends. Most components extend %component to inherit typography, color, border, and border-radius assignments. Components that map context-palette tokens onto their local variables extend %body-context. Context-aware components also extend %context so they react to the Context Class on themselves or an ancestor. Interactive items extend %interactive to pick up hover, focus, active, and disabled states.
3. Mixins. A small set of mixins in scss/mixins/_component.scss wire CSS variables onto actual properties. They handle the parts that don't fit cleanly into a placeholder — for instance, padding that varies per component, or the stroke-exclusion calculation.
4. The component's own rules. Layout, positioning, gaps, and any structural CSS unique to the component round out the class.
Building blocks
All the pieces the recipe relies on live in two folders.
Placeholders
scss/placeholders/_component.scss provides two foundational placeholders:
%component— typography + colors + border + border-radius. The default extend for any component that needs the full Chassis treatment.%interactive— hover, focus, active, and disabled states driven by CSS variables. Extend this on any element that needs interactive state behavior.
%context and the four style placeholders (%body-context, %solid-context, %smooth-context, %outline-context) live in scss/placeholders/_context.scss and power the context color system — see Context Class for details.
Component mixins
scss/mixins/_component.scss exposes four mixins. colors(), padding(), and border() each accept an optional $comp namespace string — when supplied, they read from --cx-{comp}-{property} instead of --cx-{property} in compiled CSS, useful when a component keeps variables namespaced rather than aliasing them onto the canonical names:
colors($comp)— applies color properties.padding($comp)— applies padding.border($comp)— applies border properties.exclude-strokes($comp)— subtracts border width from padding so visual spacing matches the design token; see Box Model → Stroke exclusion for details.$compis a lookup key, not a namespace prefix: when$enable-exclude-strokesistruethe subtraction always applies; when it is a list, subtraction applies only if$compappears in that list; whenfalse, standardpaddingis emitted regardless.
Notification anatomy
.notification is a banner with optional title, icon, and dismiss button. It needs full theming, contextual color variants, and pixel-perfect padding regardless of border width — so it's a fair representative of the pattern.
.notification {
@extend %context, %body-context, %component;
--fg-color: var(--notification-fg-color, #{$notification-fg-color});
--bg-color: var(--notification-bg-color, #{$notification-bg-color});
--padding-y: var(--notification-padding-y, #{$notification-padding-y});
--padding-x: var(--notification-padding-x, #{$notification-padding-x});
--gap: var(--notification-gap, #{$notification-gap});
--border-width: var(--notification-border-width, #{$notification-border-width});
--border-radius: var(--notification-border-radius, #{$notification-border-radius});
@include exclude-strokes(notification);
position: relative;
display: flex;
flex-direction: column;
gap: var(--text-gap) var(--gap);
&.solid { @extend %solid-context; }
}Three things to notice:
- Variables are namespaced first, then aliased to the canonical names.
--cx-padding-yreads from--cx-notification-padding-y, which itself falls back to the$notification-padding-ySass token. A consumer can therefore override--cx-notification-padding-yto retheme every notification on the page, or set--cx-padding-ydirectly on one element to retheme that instance, without touching the source. The same pattern applies to color, gap, and border properties. - Three placeholders carry the heavy lifting.
%componentapplies typography, color, border, and border-radius via the canonical CSS variables.%body-contextmaps the active context-palette tokens (--cx-fg-main,--cx-bg-main,--cx-border-subtle) onto those same canonical names (--cx-fg-color,--cx-bg-color,--cx-border-color).%contextexposes the full set of context-scoped variables so the notification reacts to a.context.primary(or any other) ancestor. Adding.solidextends%solid-contextto flip the palette to the filled variant. exclude-strokes(notification)is the only direct mixin call. It keeps visual padding pixel-aligned with the design token value regardless of border width by subtracting--cx-border-widthfrom--cx-padding-yand--cx-padding-x. See Box Model → Stroke exclusion for details.
List-action anatomy
.list-action is a different shape of customization: it doesn't define a new component — it adds an interaction layer to an existing one (.list-item).
.list.list-action > .list-item,
.list > .list-item.list-action {
@extend %interactive;
&.active,
&.active:hover {
--fg-color: var(--item-fg-active);
--bg-color: var(--item-bg-active);
z-index: 2; // Place active items above their siblings for proper border styling
}
}The pattern is composition rather than authorship:
%interactiveadds the state behavior. Hover, focus, active, and disabled — all driven by CSS variables, so the colors adapt automatically to whatever palette is active on the parent.list.- The active state re-aims the canonical variables. Rather than extending a context placeholder,
.activesets--cx-fg-colorand--cx-bg-colorto the--cx-item-fg-activeand--cx-item-bg-activevalues that.listexposes. This keeps the active indicator in step with the parent component's palette without requiring a full context re-extension on each item. - No new variables are declared.
.list-actionrides on the variables that.list-itemalready exposes, so customizations to the parent component flow through automatically.
This composition approach suits any behavior modifier that should work alongside — rather than replace — an existing component.
Custom components
Putting these pieces together, a custom component that wants the same theming and contextual behavior as the built-ins follows this skeleton:
.my-component {
// 1. Inherit shared behavior
@extend %component; // typography + colors + border + border-radius
@extend %context; // optional: react to .context.* on ancestors
@extend %body-context; // optional: map context tokens onto fg/bg/border
// 2. Declare component-scoped CSS variables, with token fallbacks
--padding-y: var(--my-component-padding-y, #{$my-component-padding-y});
--padding-x: var(--my-component-padding-x, #{$my-component-padding-x});
--border-radius: var(--my-component-border-radius, #{$my-component-border-radius});
// 3. Wire the variables onto properties via mixins
@include exclude-strokes(my-component); // or @include padding();
// 4. Component-specific layout
display: inline-flex;
align-items: center;
// 5. Optional: opt in to context style variants
&.solid { @extend %solid-context; }
&.smooth { @extend %smooth-context; }
&.outline { @extend %outline-context; }
}A few practical notes:
- Keep component-specific values namespaced (
--my-component-*in SCSS source, compiled to--cx-my-component-*) and alias them onto the canonical names inside the component. That gives consumers two override surfaces in compiled CSS —--cx-my-component-*to retheme every instance, or--cx-padding-yand--cx-bg-colorto retheme a single element — without additional work in the component itself. - Reach for a placeholder before a mixin. Placeholders compile to grouped selectors and reduce output size; mixins inline the same rules into every call site.
- For responsive variants, pair
@each $breakpoint in map.keys($breakpoints)withmedia-breakpoint-up— the same pattern Chassis uses for dropdown alignment classes and horizontal list groups:
$breakpoints: (
xsmall: 0,
small: $breakpoint-small,
medium: $breakpoint-medium,
large: $breakpoint-large,
xlarge: $breakpoint-xlarge,
2xlarge: $breakpoint-2xlarge
);For the Sass-map and loop pattern — including how to add, modify, or remove entries from these maps — see Sass maps and loops.
Sass mixins
scss/mixins/_component.scss defines four mixins that wire component CSS variables onto CSS properties.
colors(), padding(), and border():
/// Apply standard color properties using CSS custom properties.
/// Optionally namespaces variables under a component prefix.
///
/// @param {String|null} $comp [null] - Component namespace (e.g., "button"); prepends `{comp}-` to variable names
@mixin colors($comp: null) {
$-prefix: "";
@if $comp { $-prefix: "#{$comp}-"; }
color: var(--#{$-prefix}fg-color);
background-color: var(--#{$-prefix}bg-color);
}/// Apply standard padding using CSS custom properties.
/// Optionally namespaces variables under a component prefix.
///
/// @param {String|null} $comp [null] - Component namespace; prepends `{comp}-` to variable names
@mixin padding($comp: null) {
$-prefix: "";
@if $comp { $-prefix: "#{$comp}-"; }
padding: var(--#{$-prefix}padding-y) var(--#{$-prefix}padding-x);
}/// Apply a full border shorthand using CSS custom properties.
/// Optionally namespaces variables under a component prefix.
///
/// @param {String|null} $comp [null] - Component namespace; prepends `{comp}-` to variable names
@mixin border($comp: null) {
$-prefix: "";
@if $comp {
$-prefix: "#{$comp}-";
}
border: var(--#{$-prefix}border-width) var(--#{$-prefix}border-style) var(--#{$-prefix}border-color);
}exclude-strokes():
/// Apply padding while optionally subtracting the border width, so the visual
/// padding matches design specs regardless of border width.
///
/// Controlled by `$enable-exclude-strokes`:
/// - `true` — Always subtract border from padding
/// - `false` — Never subtract; emit standard padding
/// - `List` — Only subtract when `$comp` is in the list
///
/// @param {String} $comp - Component name to look up in `$enable-exclude-strokes`
@mixin exclude-strokes($comp) {
@if $enable-exclude-strokes == true or list.index($enable-exclude-strokes, $comp) {
$border-width: var(--border-width);
$padding-y: calc(var(--padding-y) - #{$border-width});
$padding-x: calc(var(--padding-x) - #{$border-width});
padding: $padding-y $padding-x;
} @else {
padding: var(--padding-y) var(--padding-x);
}
}