Skip to Main content

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 output

SCSS Design System Layers

00-abstracts

Contains design tokens as SCSS variables. These define the visual language of the application:

FileContents
_colors.scssColor variables: $white, $black, $primary, $red, etc. (mapped to CSS custom properties)
_breakpoints.scssBreakpoint variables: $tablet: 768px, $desktop: 1024px
_typography.scssFont weight variables ($regular, $semi-bold, $bold), font family stacks
_variables.scssAdditional 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:

MixinPurpose
px2rem($pxValue)Converts pixel values to rem (divides by 16)
tablet, desktop, printResponsive breakpoint wrappers using rem-based media queries
firefox-onlyFirefox-specific styles via @supports
font-h1 through font-h6Typography presets for headings
font-body, font-label, font-sub-headingTypography presets for body text
zzz-buttonFull button styling with CSS custom properties and hover effects
zzz-inputForm input styling
light, darkTheme-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-face declarations for Crimson Text and Work Sans (woff2)
  • Typographyh1h6 element and .h1.h6 class 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

LayerClass PrefixOutput PrefixExample Output File
Atomszzz-a-(bundled into style-base)style-base.css
Moleculeszzz-m-(bundled into style-base)style-base.css
Organismszzz-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.scss for 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 development

The 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/, and xpack/ are loaded without prelude injection
  • Mixin files themselves only receive the abstracts prelude (not mixin self-reference)

Bundling Strategy

SourceOutputStrategy
style-base.scssstyle-base.cssCompiles base styles + all atoms + all molecules into one bundle
src/organisms/{name}/index.scssb-{name}.cssEach organism compiled individually
src/templates/{name}/index.scssp-{name}.cssEach template compiled individually
xpack/styles/root.scssroot.cssxpack shell styles (no prelude injection)
xpack/styles/pl-states.scsspl-states.cssPattern 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:

  1. Autoprefixer — Adds vendor prefixes, including CSS Grid support (grid: true)
  2. 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 LocationTriggers 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.map

On 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.css

RequireCss 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:

  1. Entry/index files (with --forward=all): style-base.scss, style.all.scss, _abstracts.scss, _mixins.scss, _base.scss
  2. Component files: All SCSS in atoms/, molecules/, organisms/, and xpack/styles/

npm Scripts

ScriptCommandDescription
stylesbun styles.tsOne-time SCSS compilation
devconcurrently ... "bun styles.ts --watch" ...Development mode with style watching
generatebun states.ts && bun run styles && ...Full generation including styles
intebun 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):

AliasPath
@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

  1. Create an SCSS file in the appropriate directory:

    src/atoms/my-component/index.scss
  2. Write your styles using the zzz-a- (atom) or zzz-m- (molecule) prefix. No @use imports needed:

    .zzz-a-my-component {
      padding: px2rem(16px);
      color: $primary;
    
      @include tablet {
        padding: px2rem(24px);
      }
    }
  3. The styles will be automatically bundled into style-base.css on the next build.

Adding a New Organism

  1. Create an SCSS file:

    src/organisms/my-section/index.scss
  2. Write your styles with the zzz-o- prefix:

    .zzz-o-my-section {
      // ...
    }
  3. The build will output public/assets/css/b-my-section.css.

  4. Load it in your component:

    <RequireCss path="b-my-section" />

Adding a Partial (Shared Styles)

  1. Create a file prefixed with _:

    src/organisms/my-section/_shared.scss
  2. Use it from sibling files:

    @use 'shared';
  3. Partial files are not compiled as standalone CSS — they must be imported by non-partial files.