Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

45 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PCSS

Pragmatic CSS is a naming and file-structure convention for scalable stylesheets. No dependencies, no build plugins. Works with plain CSS, SCSS, Less, or any preprocessor.

The core rule: a class name tells you exactly which file it lives in.

.form--nav--search   →   component/form/nav/_search.scss
.btn--primary        →   component/btn/_primary.scss
.sidebar__header     →   component/sidebar/_index.scss

PCSS draws from SMACSS (Base/State/Theme layers), BEM (element __ and modifier -- separators), and OOCSS (single-responsibility, open/closed principles).


Quick reference

Class Type File
.btn component component/btn/_index.scss
.btn__icon element component/btn/_index.scss
.btn--primary subclass component/btn/_primary.scss
.is-hidden state base/_global-state.scss
.has-error state base/_global-state.scss
.u-uppercase utility base/_utilities.scss
.theme-dark theme base/_definitions.scss

Contents


Component

A component is a self-contained, reusable UI block: btn, nav-bar, dialog, progressbar. Each component gets its own directory with a base file _index.scss.

Class File
.panel component/panel/_index.scss
.nav-bar component/nav-bar/_index.scss

Element

An element is a structural part of a component. It cannot be reused outside its parent.

Class File
.panel__header component/panel/_index.scss

Subclass

A subclass extends a component via inheritance. Base styles go in _index.scss, variant styles go in a separate file named after the subclass.

Class File
.panel--primary component/panel/_primary.scss
<div class="dialog dialog--alert">...</div>
<div class="dialog dialog--prompt">...</div>

Elements are styled within the scope of each subclass:

.shopping-cart {
  // base styles shared by all variants
}
.shopping-cart--default {
  .shopping-cart__heading { color: white; }
}
.shopping-cart--inverse {
  .shopping-cart__heading { color: black; }
}

Component example

<div class="progressbar progressbar--big">
  <output class="progressbar__status">Install is 70% complete</output>
  <progress class="progressbar__progress" value="70" max="100"></progress>
  <div class="progressbar__actions">
    <div class="icon icon--pause">pause</div>
    <div class="icon icon--play is-hidden">resume</div>
  </div>
</div>

component/progressbar/_index.scss

.progressbar {
  position: relative;
}
.progressbar__progress {
  border: 0;
  position: absolute;
  width: 100%;
  bottom: 0;
  left: 0;
  appearance: none;
  &::-webkit-progress-bar { /* ... */ }
  &::-webkit-progress-value { /* ... */ }
  &::-moz-progress-bar { /* ... */ }
}
.progressbar__status {
  display: flex;
  position: relative;
  font-size: 1rem;
}
.progressbar__actions {
  position: absolute;
  bottom: 0;
  right: 0;
  > .icon { /* ... */ }
}

component/progressbar/_big.scss

.progressbar--big > .progressbar__status {
  font-size: 1.6rem;
  text-transform: uppercase;
  padding: 16px;
}

component/progressbar/_small.scss

.progressbar--small > .progressbar__status {
  font-size: 1.1rem;
  padding: 11px;
}

Top


State

States are toggled by JavaScript to reflect runtime behavior: .is-expanded, .is-hidden, .has-error. They are not static helpers.

<div class="main has-error">
  <aside class="sidebar is-hidden">...</aside>
</div>

component/_main.scss

.main {
  /* default style */
  &.has-error {
    /* error state */
  }
}

base/_global-state.scss

/* JS-toggled only */
.is-hidden {
  display: none !important;
}

Top


Utility

Utilities are static, single-purpose helpers set once in markup and never toggled by JS. They use a u- prefix to distinguish them from states.

Class File
.u-hidden base/_utilities.scss
.u-uppercase base/_utilities.scss
.u-sticky base/_utilities.scss

base/_utilities.scss

.u-hidden    { display: none !important; }
.u-uppercase { text-transform: uppercase; }
.u-sticky    { position: sticky; top: 0; }

The distinction: if a class is set once in markup and stays, it is a utility (.u-). If code adds or removes it at runtime to reflect a behavior change, it is a state (.is- / .has-).

Top


Theme

Themes swap visual style based on context. The recommended approach uses CSS custom properties so no recompile is needed.

CSS custom properties (recommended)

<html class="theme-dark">
  <div class="l-main">
    <aside class="sidebar">...</aside>
  </div>
</html>

base/_definitions.scss

:root       { --sidebar-bg: white;   --sidebar-color: black; }
.theme-dark { --sidebar-bg: #1a1a2e; --sidebar-color: white; }
.theme-warm { --sidebar-bg: #f5f5f0; --sidebar-color: #333; }

component/sidebar/_index.scss

.sidebar {
  background-color: var(--sidebar-bg);
  color: var(--sidebar-color);
}

SCSS mixin (legacy, requires recompile)

If you cannot use custom properties, drive themes through a SCSS mixin and theme map. Each theme generates a descendant selector per component.

@mixin theme-dialog($theme) {
  $bg: get-theme-style($theme, "bg");
  .theme-#{$theme} .dialog {
    background-color: #{$bg};
  }
}
@each $theme in $themes {
  @include theme-dialog($theme);
}

base/_definitions.scss

$themes: baz qux;

@function get-theme-style($theme, $key) {
  $baz-map: ("bg": $baz-bg);
  $qux-map: ("bg": $qux-bg);
  @if $theme == "baz" { @return map-get($baz-map, $key); }
  @if $theme == "qux" { @return map-get($qux-map, $key); }
  @return map-get($baz-map, $key);
}

Top


File Structure

styles/
├── component/
│   ├── btn/
│   │   ├── _index.scss       ← .btn base styles
│   │   └── _primary.scss     ← .btn--primary
│   └── form/
│       ├── _index.scss       ← .form base styles
│       ├── auth/
│       │   ├── _index.scss
│       │   └── _login.scss
│       └── nav/
│           ├── _index.scss
│           └── _search.scss
└── base/
    ├── _normalize.scss
    ├── _base.scss
    ├── _definitions.scss     ← variables, theme tokens
    ├── _global-state.scss    ← .is-*, .has-*
    ├── _utilities.scss       ← .u-*
    ├── _animations.scss
    └── mixin/
        └── _media.scss

Top


Naming Conventions

The class name encodes the file location. .form--nav--search lives in component/form/nav/_search.scss.

Class Entity
.btn, .main-nav component (hyphen-delimited words only)
.main-nav__title element (part of a component)
.btn--primary, .main-nav--landing subclass (variant)
.is-hidden, .has-success state (JS-toggled)
.u-uppercase, .u-sticky utility (static helper)
.theme-default, .theme-dark theme

Further reading:

Top


Selector Conventions

Keep selectors short

Browsers read selectors right-to-left. Long selectors slow rendering and bloat output. Keep nesting to 3-4 levels max.

Prefer @include over @extend

@extend generates a comma-separated selector list whose length depends on how many other rules share the same placeholder. This can be surprising in compiled output and hard to debug.

Reserve @extend %placeholder for cases where you genuinely want selectors to share a rule-set. Never @extend a concrete class like .container--compact, because that couples the subclass to the base class's source position.

// avoid
.container--loose {
  @extend .container--compact;
}

// prefer
@mixin container-compact-styles {
  .col { padding: $col-pad; }
}
.container--compact { @include container-compact-styles; }
.container--loose   { @include container-compact-styles; /* plus overrides */ }

Use classes for styling; use dedicated attributes for JS binding

IDs belong in HTML fragments, not CSS. When the same ID serves as both a CSS hook and a JS target, changes to one break the other.

<!-- avoid -->
<button id="submit-btn" class="btn btn--primary">Submit</button>

<!-- prefer -->
<button class="btn btn--primary" data-action="submit">Submit</button>
<!-- or -->
<button class="btn btn--primary js-submit">Submit</button>

input[disabled] and similar semantic attributes are fine for styling. Avoid styling via arbitrary data-* attributes; use classes instead.

Avoid tag-qualified selectors

button.btn ties the style to a specific element type. Drop the tag so the class can move freely across different elements.

Avoid long descendant selectors

.feed nav ul li h2 means the style only works at that exact DOM path. Single-class selectors let you move components anywhere in the markup without rewriting CSS.

Top


CSS Cascade Layers

CSS @layer (baseline since 2022) formalizes what PCSS manages through naming discipline. PCSS layers map directly:

@layer base, component, utility, state, theme;

Declaring layers upfront fixes their priority order regardless of source order. A later layer always wins at equal specificity. You no longer need !important on state rules or extra nesting for theme overrides.

@layer base      { /* _normalize.scss, _base.scss */ }
@layer component { /* component/**/_index.scss, subclasses */ }
@layer utility   { /* _utilities.scss */ }
@layer state     { /* _global-state.scss */ }
@layer theme     { /* token overrides */ }

Top


Tooling

The file-path-to-class-name convention is only as strong as your enforcement. Add stylelint with stylelint-selector-bem-pattern to validate selectors automatically:

npm install --save-dev stylelint stylelint-selector-bem-pattern

.stylelintrc

{
  "plugins": ["stylelint-selector-bem-pattern"],
  "rules": {
    "plugin/selector-bem-pattern": {
      "componentName": "[a-z][a-z0-9-]*",
      "componentSelectors": {
        "initial": "^\\.{componentName}(?:__[a-z][a-z0-9-]*)?(?:--[a-z][a-z0-9-]*)?$"
      },
      "utilitySelectors": "^\\.u-[a-z][a-z0-9-]*$"
    }
  }
}

Top


Further Reading

About

Guidelines for writing scalable and maintainable style-sheets

Topics

Resources

Stars

18 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors