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:
// ✅ 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);
}
}<!-- ✅ 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.
// ✅ 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:
// 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:
.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:
.my-element { // Block
// Block styles
&__property { // Element
// Element styles
&--modifier { // Modifier
// Modifier styles
}
}
&--variant { // Block modifier
// Variant styles
}
}Practical Examples
.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
Recommended Nesting
.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
// ❌ 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:
// 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-widthheight,min-height,max-heightaspect-ratiopaddingbackground,border,border-radiusfont-*,color,text-*display: flex/grid(for internal structure)
Extrinsic (❌ belongs in container):
margin,gapposition,top,left,right,bottomgrid-column,grid-row,flex-grow,flex-shrinkorder- 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