Skip to content

WIP: CSS/SCSS Patterns for Statamic Projects

Overview

This specification defines the structured approach to CSS/SCSS in Statamic projects using selective Bootstrap integration and isolated component architecture.

Bootstrap Integration

Used Bootstrap Modules

We use only a reduced selection of Bootstrap modules:

  • Root - CSS Custom Properties and base variables
  • Reboot - Normalization and basic element styles
  • Containers - Container layouts (.container, .container-fluid)
  • Grid - Grid system (.row, .col-*)
  • Forms - Basic form styles
  • Helpers - Utility helpers (Clearfix, Position, etc.)
  • Utilities - Spacing, Colors, Display, Flexbox, etc.

Prefer Bootstrap Utilities

Use Bootstrap utilities for common styling tasks:

scss
// ✅ Preferred - Bootstrap utilities directly in CSS
.my-card {
  // Custom styles here
  
  &__header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: var(--bs-light);
    color: var(--bs-dark);
  }
}
html
<!-- ✅ Or directly in HTML with Bootstrap classes -->
<div class="my-card__header d-flex justify-content-between align-items-center bg-light text-dark">
  <!-- Content -->
</div>

Component Architecture

Intrinsic Web Design Pattern

Core Principle: We use the "Intrinsic Web Design" pattern, where elements define their intrinsic properties (size, proportions, appearance) while containers determine the extrinsic constraints (layout, positioning, spacing).

Intrinsic vs. Extrinsic Properties

Intrinsic (Element Responsibility):

  • Minimum/Maximum dimensions
  • Aspect ratios (aspect-ratio)
  • Internal spacing (padding)
  • Appearance (colors, typography)
  • Internal layout (flex/grid for children)

Extrinsic (Container Responsibility):

  • Positioning in layout
  • External spacing (margin, gap)
  • Grid/Flex placement
  • Responsive layout behavior

Context-Agnostic Components

Core Rule: Every element defines only its intrinsic properties and knows nothing about its context.

scss
// ✅ Good - Element defines intrinsic properties
.testimonial-card {
  // Intrinsic constraints
  min-width: 280px;
  max-width: 500px;
  aspect-ratio: 4/3;
  
  // Appearance (intrinsic)
  background: white;
  border-radius: 8px;
  padding: 1.5rem;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  
  // Internal layout (intrinsic)
  display: flex;
  flex-direction: column;
  gap: 1rem;
  
  &__avatar {
    width: 60px;
    height: 60px;
    border-radius: 50%;
    object-fit: cover;
  }
  
  &__content {
    flex: 1;
    font-size: 1rem;
    line-height: 1.5;
  }
  
  &__author {
    font-weight: 600;
    margin-top: auto;
  }
}

// ❌ Bad - Element knows its context
.testimonial-card {
  margin-bottom: 2rem; // Extrinsic - belongs to container!
  grid-column: span 2; // Extrinsic - layout responsibility!
  
  .sidebar & {
    width: 100%; // Context-dependent!
  }
  
  &:nth-child(3n) {
    margin-right: 0; // Layout-specific!
  }
}

Container-Element Relationship (Inversion of Control)

Containers define layout constraints and can place elements arbitrarily:

scss
// Universal Card Component - Defines only intrinsic properties
.product-card {
  // Intrinsic constraints
  min-width: 250px;
  max-width: 350px;
  
  // Appearance (intrinsic)
  background: white;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  overflow: hidden;
  
  // Internal layout (intrinsic)
  display: flex;
  flex-direction: column;
  
  &__image {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }
  
  &__content {
    padding: 1.5rem;
    flex: 1;
    display: flex;
    flex-direction: column;
  }
  
  &__title {
    font-size: 1.25rem;
    font-weight: 600;
    margin-bottom: 0.5rem;
  }
  
  &__description {
    color: var(--bs-gray-600);
    margin-bottom: 1rem;
    flex: 1;
  }
  
  &__price {
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--bs-primary);
    margin-top: auto;
  }
}

// Container 1: Flex container for featured products
.featured-products {
  display: flex;
  gap: 2rem;
  align-items: stretch;
  overflow-x: auto;
  padding: 2rem 0;
  
  // Card adapts automatically - no changes needed
  .product-card {
    flex: 0 0 auto; // Extrinsic property defined by container
    scroll-snap-align: start;
  }
  
  // Responsive behavior
  @media (max-width: 768px) {
    gap: 1rem;
    
    .product-card {
      min-width: 280px; // Container can override intrinsic values
    }
  }
}

// Container 2: Grid layout for product catalog
.product-catalog {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 2rem;
  
  // Same card, different layout - no changes to card needed
  .product-card {
    // No additional styles required
    // Card already defines all intrinsic properties
  }
}

Core Principle: The product-card component remains unchanged and works in all different layout contexts. Containers define only the extrinsic layout properties while the card maintains its intrinsic properties.

Container Queries Integration

Use Container Queries for truly responsive components:

scss
.testimonial-card {
  // Base intrinsic properties
  min-width: 200px;
  container-type: inline-size;
  
  // Responds to own container width, not viewport
  @container (min-width: 300px) {
    .testimonial-card__content {
      font-size: 1.125rem;
    }
    
    .testimonial-card__avatar {
      width: 80px;
      height: 80px;
    }
  }
  
  @container (min-width: 400px) {
    flex-direction: row;
    text-align: left;
    
    .testimonial-card__avatar {
      margin-right: 1rem;
      margin-bottom: 0;
    }
  }
}

BEM Naming Convention

Basic Structure

All CSS classes follow BEM methodology with adapted syntax:

scss
.my-element {           // Block
  // Block styles
  
  &__property {         // Element
    // Element styles
    
    &--modifier {       // Modifier
      // Modifier styles
    }
  }
  
  &--variant {          // Block modifier
    // Variant styles
  }
}

Practical Examples

scss
.card {
  background: white;
  border-radius: 0.5rem;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  
  &__header {
    padding: 1.5rem;
    border-bottom: 1px solid #e9ecef;
    
    &--highlighted {
      background-color: #f8f9fa;
    }
  }
  
  &__body {
    padding: 1.5rem;
  }
  
  &__footer {
    padding: 1rem 1.5rem;
    background-color: #f8f9fa;
    border-top: 1px solid #e9ecef;
  }
  
  &--shadow-lg {
    box-shadow: 0 10px 25px rgba(0,0,0,0.15);
  }
}

SCSS Nesting

scss
.navigation {
  // Direct block properties
  background-color: white;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  
  // Pseudo-selectors on block level
  &:hover {
    box-shadow: 0 4px 8px rgba(0,0,0,0.15);
  }
  
  // Block modifiers
  &--fixed {
    position: fixed;
    top: 0;
    width: 100%;
    z-index: 1000;
  }
  
  // Elements (max 3 levels deep)
  &__list {
    display: flex;
    list-style: none;
    margin: 0;
    padding: 0;
    
    &--vertical {
      flex-direction: column;
    }
  }
  
  &__item {
    position: relative;
    
    &--active {
      .navigation__link {
        color: #0d6efd;
        font-weight: 600;
      }
    }
  }
  
  &__link {
    display: block;
    padding: 1rem;
    text-decoration: none;
    color: #333;
    transition: color 0.2s ease;
    
    &:hover {
      color: #0d6efd;
    }
    
    &--button {
      @extend .btn;
      @extend .btn-primary;
    }
  }
}

Avoid Deep Nesting

scss
// ❌ Too deeply nested (> 4 levels)
.header {
  .navigation {
    .menu {
      .item {
        .link {
          // Too deep!
        }
      }
    }
  }
}

// ✅ Better - flatter structure
.navigation-link {
  // Styles here
}

Button Implementation

Follow Bootstrap Standards

Buttons must follow Bootstrap naming conventions but are self-implemented:

scss
// Base button class
.btn {
  display: inline-block;
  padding: 0.375rem 0.75rem;
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.5;
  text-align: center;
  text-decoration: none;
  vertical-align: middle;
  cursor: pointer;
  border: 1px solid transparent;
  border-radius: 0.375rem;
  transition: all 0.15s ease-in-out;
  
  &:focus {
    outline: 0;
    box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
  }
  
  &:disabled {
    opacity: 0.65;
    pointer-events: none;
  }
}

.btn-primary {
  display: inline-block;
  padding: 0.375rem 0.75rem;
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.5;
  text-align: center;
  text-decoration: none;
  vertical-align: middle;
  cursor: pointer;
  border: 1px solid transparent;
  border-radius: 0.375rem;
  transition: all 0.15s ease-in-out;
  
  // Specific primary styles
  color: white;
  background-color: #0d6efd;
  border-color: #0d6efd;
  
  &:hover:not(:disabled) {
    background-color: #0b5ed7;
    border-color: #0a58ca;
  }
  
  &:focus {
    outline: 0;
    box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
  }
  
  &:disabled {
    opacity: 0.65;
    pointer-events: none;
  }
}

Best Practices

Do's ✅

  • Intrinsic Web Design - Elements define intrinsic properties
  • Container Queries for responsive components
  • Bootstrap Utilities for common styles
  • Context-Agnostic Components - Develop components in isolation
  • BEM Naming Convention
  • Inversion of Control - Containers control layout, not elements
  • Limit nesting to max 3-4 levels
  • Follow button standards
  • Use semantic HTML

Don'ts ❌

  • Extrinsic Properties in elements (margin, positioning)
  • Context-Dependent Styles - Making components context-dependent
  • Including unused Bootstrap modules
  • Deep SCSS nesting (>4 levels)
  • Inline styles in templates
  • ID selectors for styling
  • Excessive use of !important

Intrinsic vs. Extrinsic Checklist

Intrinsic (✅ belongs in element):

  • width, min-width, max-width
  • height, min-height, max-height
  • aspect-ratio
  • padding
  • background, border, border-radius
  • font-*, color, text-*
  • display: flex/grid (for internal structure)

Extrinsic (❌ belongs in container):

  • margin, gap
  • position, top, left, right, bottom
  • grid-column, grid-row, flex-grow, flex-shrink
  • order
  • Layout-specific transformations

File Structure

resources/
├── scss/
│   ├── bootstrap/
│   │   └── custom-bootstrap.scss    // Only needed modules
│   ├── components/
│   │   ├── _buttons.scss
│   │   ├── _cards.scss
│   │   ├── _navigation.scss
│   │   └── _forms.scss
│   ├── blocks/
│   │   ├── _hero.scss
│   │   ├── _text-image.scss
│   │   └── _gallery.scss
│   ├── utilities/
│   │   ├── _variables.scss
│   │   ├── _mixins.scss
│   │   └── _helpers.scss
│   └── app.scss                     // Main SCSS file