Style guide and SCSS build pipeline
Covers the full SCSS stack behind PreciseAlloy: design tokens, Atomic Design naming with BEM, the Dart Sass and PostCSS build pipeline, component style authoring without manual imports, and runtime CSS loading with light and dark theme support.
This document describes how styles are developed, compiled, and consumed in the PreciseAlloy frontend codebase.
Architecture Overview
Styles are built using a custom SCSS compilation pipeline (styles.ts) rather than relying on Vite's built-in CSS handling. The pipeline uses:
- Dart Sass (
sass) for SCSS compilation - PostCSS with Autoprefixer (including CSS Grid support) and cssnano for post-processing and minification
- source-map-js for accurate source map generation
- chokidar for file watching in development
The design follows Atomic Design methodology (atoms → molecules → organisms → templates) combined with BEM naming and a custom zzz- namespace prefix.
Directory Structure
src/
├── assets/
│ └── styles/
│ ├── style-base.scss # Base bundle entry point
│ ├── style.all.scss # Full bundle entry (base + all components)
│ ├── 00-abstracts/ # Variables, colors, breakpoints, typography tokens
│ │ ├── _abstracts.scss # Index: forwards all abstract partials
│ │ ├── _breakpoints.scss # Breakpoint variables ($tablet, $desktop)
│ │ ├── _colors.scss # Color variables (CSS custom property aliases)
│ │ ├── _typography.scss # Font weight/family variables
│ │ └── _variables.scss # Additional variables (placeholder)
│ ├── 01-mixins/ # Reusable mixins
│ │ ├── _mixins.scss # Index: forwards all mixin partials
│ │ ├── _px2rem.scss # px2rem() conversion function
│ │ ├── _breakpoints.scss # Responsive mixins (@mixin tablet, desktop, print)
│ │ ├── _browser-target.scss # Browser-specific mixins (@mixin firefox-only)
│ │ ├── _typography.scss # Typography mixins (font-h1 through font-h6, etc.)
│ │ ├── _buttons.scss # Button mixin (@mixin zzz-button)
│ │ ├── _input.scss # Input mixin (@mixin zzz-input)
│ │ └── _theme.scss # Theme mixins (@mixin light, @mixin dark)
│ └── 02-base/ # Base/reset styles
│ ├── _base.scss # Index: forwards all base partials
│ ├── _reset.scss # CSS reset (Meyer + border-box)
│ ├── _fonts.scss # @font-face declarations (Crimson Text, Work Sans)
│ ├── _typography.scss # Heading styles (h1-h6 and .h1-.h6 classes)
│ ├── _style-base.scss # Root/body/main, .zzz-container, section-margin
│ ├── _epi-fixes.scss # Optimizely CMS style overrides
│ └── _theme.scss # CSS custom properties for light/dark themes
├── atoms/ # Smallest UI elements
│ ├── buttons/index.scss
│ ├── forms/text-input.scss, text-area.scss, error-message.scss
│ ├── icons/index.scss
│ ├── links/index.scss
│ └── pictures/index.scss
├── molecules/ # Composed atom groups
│ ├── list/index.scss
│ └── prices/index.scss
├── organisms/ # Complex UI sections
│ ├── alert/index.scss
│ ├── contact/index.scss
│ ├── footer/index.scss
│ ├── header/index.scss
│ ├── hero/index.scss
│ └── ...
└── templates/ # Page layouts (typically no SCSS)
├── home/index.tsx
├── about/index.tsx
└── ...
xpack/styles/ # Pattern library shell styles
├── root.scss
├── pl-states.scss
├── _border.scss, _breakpoint.scss, _form.scss, ...
└── ...
public/assets/css/ # Compiled CSS outputSCSS Design System Layers
00-abstracts
Contains design tokens as SCSS variables. These define the visual language of the application:
| File | Contents |
|---|---|
_colors.scss | Color variables: $white, $black, $primary, $red, etc. (mapped to CSS custom properties) |
_breakpoints.scss | Breakpoint variables: $tablet: 768px, $desktop: 1024px |
_typography.scss | Font weight variables ($regular, $semi-bold, $bold), font family stacks |
_variables.scss | Additional shared variables |
All abstracts are forwarded through _abstracts.scss and made available to every component file automatically (see Prelude Auto-Injection).
01-mixins
Reusable SCSS mixins that encapsulate common patterns:
| Mixin | Purpose |
|---|---|
px2rem($pxValue) | Converts pixel values to rem (divides by 16) |
tablet, desktop, print | Responsive breakpoint wrappers using rem-based media queries |
firefox-only | Firefox-specific styles via @supports |
font-h1 through font-h6 | Typography presets for headings |
font-body, font-label, font-sub-heading | Typography presets for body text |
zzz-button | Full button styling with CSS custom properties and hover effects |
zzz-input | Form input styling |
light, dark | Theme-scoped selectors using [data-theme] attribute |
02-base
Global styles applied to the HTML document:
- Reset — Meyer CSS reset with
box-sizing: border-box - Fonts —
@font-facedeclarations for Crimson Text and Work Sans (woff2) - Typography —
h1–h6element and.h1–.h6class styles - Style base — Root, body, main element styles;
.zzz-container(max-width 1200px); section margin utilities - Theme — CSS custom properties for light/dark mode
- Epi fixes — Optimizely CMS quickNavigator overrides
Component Styles
Naming Convention
All component CSS classes follow the BEM methodology with a zzz- namespace prefix and an Atomic Design layer indicator:
.zzz-{layer}-{block}__{element}--{modifier}Examples:
.zzz-a-button— Atom: button.zzz-a-link-with-icon— Atom: link with icon.zzz-m-price— Molecule: price card.zzz-o-header— Organism: header.zzz-o-header__nav-list__item— Organism: nested element.zzz-o-hero__content— Organism: hero content area
Atomic Design Prefixes
| Layer | Class Prefix | Output Prefix | Example Output File |
|---|---|---|---|
| Atoms | zzz-a- | (bundled into style-base) | style-base.css |
| Molecules | zzz-m- | (bundled into style-base) | style-base.css |
| Organisms | zzz-o- | b- | b-header.css, b-alert.css |
| Templates | (varies) | p- | p-home.css |
Atoms and molecules are bundled together into style-base.css. Organisms and templates are compiled into individual CSS files with b- and p- prefixes respectively.
Writing Component SCSS
Component SCSS files do not need @use or @import statements. The build pipeline automatically injects abstracts and mixins before compiling. Simply use variables and mixins directly:
// src/organisms/alert/index.scss
// No imports needed — abstracts and mixins are auto-injected
.zzz-o-alert {
position: sticky;
top: 0;
background-color: $red;
color: $white;
padding: px2rem(16px) px2rem(24px);
@include desktop {
padding: px2rem(20px) px2rem(32px);
}
&__heading {
font-weight: $bold;
}
&__item {
margin-bottom: px2rem(8px);
}
}// src/atoms/buttons/index.scss
.zzz-a-button {
@include zzz-button;
}Rules:
- Files starting with
_(e.g.,_shared.scss) are partials — they won't be compiled as standalone files but can be@used by sibling files - Use
index.scssfor the main stylesheet of a component directory - Non-index files are named after the component they style (e.g.,
text-input.scss)
Build Pipeline
Compilation Overview
The build is orchestrated by styles.ts, which is run via Bun:
bun styles.ts # One-time build
bun styles.ts --watch # Watch mode for developmentThe pipeline for each SCSS file is:
SCSS source
→ Prelude injection (abstracts + mixins)
→ Dart Sass compilation (compileStringAsync)
→ PostCSS (autoprefixer + cssnano)
→ Output to public/assets/css/Prelude Auto-Injection
Before compilation, the build system prepends two @use statements to every component SCSS file:
@use '../../assets/styles/00-abstracts/abstracts' as *;
@use '../../assets/styles/01-mixins/mixins' as *;The paths are automatically resolved relative to each source file. This is handled by prepareCssFileContent() in styles.ts.
Exceptions:
- Files in
00-abstracts/,01-mixins/,02-base/, andxpack/are loaded without prelude injection - Mixin files themselves only receive the abstracts prelude (not mixin self-reference)
Bundling Strategy
| Source | Output | Strategy |
|---|---|---|
style-base.scss | style-base.css | Compiles base styles + all atoms + all molecules into one bundle |
src/organisms/{name}/index.scss | b-{name}.css | Each organism compiled individually |
src/templates/{name}/index.scss | p-{name}.css | Each template compiled individually |
xpack/styles/root.scss | root.css | xpack shell styles (no prelude injection) |
xpack/styles/pl-states.scss | pl-states.css | Pattern library state toggle styles |
When style-base.scss is compiled, the build system also compiles every atom and molecule SCSS file synchronously and appends their CSS output to the base bundle.
PostCSS Processing
After Sass compilation, each CSS file goes through PostCSS with:
- Autoprefixer — Adds vendor prefixes, including CSS Grid support (
grid: true) - cssnano — Minifies the output CSS
Source Maps
Source maps are generated for all output files (*.css.map). The build includes a prelude-stripping step that corrects source map line numbers to account for the auto-injected @use statements, ensuring browser DevTools point to the correct line in the original SCSS source.
Watch Mode
In watch mode (--watch), chokidar monitors src/ and xpack/styles/ for .scss file changes. The rebuild strategy is smart about dependencies:
| Changed File Location | Triggers Rebuild Of |
|---|---|
00-abstracts/ or 01-mixins/ | style-base + all organisms + all templates + pl-states |
src/atoms/, src/molecules/, 02-base/ | style-base only |
src/organisms/{name}/ | That specific organism only (or all siblings if a partial _ file changed) |
src/templates/{name}/ | That specific template only (or all siblings if a partial _ file changed) |
xpack/styles/pl-states* | pl-states only |
xpack/styles/* (other) | root only |
Debouncing (200ms) is applied to bulk operations (e.g., rebuilding all organisms) to prevent excessive recompilation.
Output Structure
All compiled CSS goes to public/assets/css/:
public/assets/css/
├── style-base.css # Base + atoms + molecules
├── style-base.css.map
├── b-alert.css # Organism: alert
├── b-alert.css.map
├── b-header.css # Organism: header
├── b-header.css.map
├── b-hero.css # Organism: hero
├── b-hero.css.map
├── ... # Other organisms
├── root.css # xpack shell
├── root.css.map
├── pl-states.css # Pattern library states
└── pl-states.css.mapOn a clean build (non-watch mode), the entire public/assets/css/ directory is deleted and recreated.
Runtime CSS Loading
Components load their pre-compiled CSS at runtime using the <RequireCss> helper component:
import RequireCss from '@helpers/RequireCss';
// In a component's render:
<RequireCss path="style-base" /> // → /assets/css/style-base.css
<RequireCss path="b-header" /> // → /assets/css/b-header.css
<RequireCss path="vendors/something" /> // → /assets/vendors/something.cssRequireCss renders a <link rel="stylesheet"> tag with a data-pl-require attribute. Paths are resolved relative to /assets/css/ (or /assets/ for vendor paths).
Theming
The codebase supports light and dark themes via the data-theme attribute on the <html> element:
<html data-theme="light"> <!-- or "dark" -->Theme CSS Custom Properties
Defined in src/assets/styles/02-base/_theme.scss:
:root,
[data-theme='light'] {
--bg-color: #{$white};
--color: #{$black};
--link-color: #{$primary};
// ...
}
[data-theme='dark'] {
--bg-color: #{$dark-bg};
--color: #{$white};
--link-color: #{$primary-light};
// ...
}Using Theme Mixins in Components
.my-component {
background: var(--bg-color);
color: var(--color);
@include dark {
// Dark-mode-only overrides
border-color: rgba(255, 255, 255, 0.1);
}
}The light and dark mixins wrap styles in [data-theme='light'] and [data-theme='dark'] selectors respectively.
xpack / Pattern Library Styles
The xpack/styles/ directory contains styles for the pattern library development shell — the UI that wraps around component previews. These include:
- Layout for the sidebar navigation, frame controls, and content area
- Its own theme system (independent CSS custom properties)
- Extended breakpoint mixins (xxs through xxl)
- State toggle panel styling
xpack styles are compiled without the abstracts/mixins prelude injection, as they have their own independent design system.
Utility: css-vip
src/_helpers/css-vip.ts provides a utility that takes compiled CSS and adds !important to every declaration (except CSS custom properties and those already marked !important). This is used for generating high-specificity CSS variants when needed for CMS integration or override scenarios.
SCSS Module Migration
The migrate-scss.ts script uses sass-migrator to migrate from the legacy @import syntax to the modern @use/@forward module system. It processes:
- Entry/index files (with
--forward=all):style-base.scss,style.all.scss,_abstracts.scss,_mixins.scss,_base.scss - Component files: All SCSS in
atoms/,molecules/,organisms/, andxpack/styles/
npm Scripts
| Script | Command | Description |
|---|---|---|
styles | bun styles.ts | One-time SCSS compilation |
dev | concurrently ... "bun styles.ts --watch" ... | Development mode with style watching |
generate | bun states.ts && bun run styles && ... | Full generation including styles |
inte | bun run styles && ... | Integration build including styles |
Path Aliases
These aliases are configured in both tsconfig.json and Vite's resolve.alias (via xpack/alias.ts):
| Alias | Path |
|---|---|
@helpers/* | src/_helpers/* |
@assets/* | src/assets/* |
@atoms/* | src/atoms/* |
@molecules/* | src/molecules/* |
@organisms/* | src/organisms/* |
@templates/* | src/templates/* |
@xpack/* | xpack/* |
Adding New Styles
Adding a New Atom or Molecule
-
Create an SCSS file in the appropriate directory:
src/atoms/my-component/index.scss -
Write your styles using the
zzz-a-(atom) orzzz-m-(molecule) prefix. No@useimports needed:.zzz-a-my-component { padding: px2rem(16px); color: $primary; @include tablet { padding: px2rem(24px); } } -
The styles will be automatically bundled into
style-base.csson the next build.
Adding a New Organism
-
Create an SCSS file:
src/organisms/my-section/index.scss -
Write your styles with the
zzz-o-prefix:.zzz-o-my-section { // ... } -
The build will output
public/assets/css/b-my-section.css. -
Load it in your component:
<RequireCss path="b-my-section" />
Adding a Partial (Shared Styles)
-
Create a file prefixed with
_:src/organisms/my-section/_shared.scss -
Use it from sibling files:
@use 'shared'; -
Partial files are not compiled as standalone CSS — they must be imported by non-partial files.