Projects About me Blog FAQ Contact me

CSS Architecture for PHP Projects (BEM + Custom Properties)

How I organize stylesheets in vanilla PHP projects to avoid specificity wars and maintain sanity.

May 8, 2026 · BLOG
CSS Architecture for PHP Projects (BEM + Custom Properties)

CSS Architecture for PHP Projects

Working with PHP templates (not JSX or Vue SFCs) requires a different CSS strategy. Here's my battle-tested approach.

The Problem with PHP + CSS

In PHP, your HTML is scattered across includes:

  • header.php
  • footer.php
  • post-card.php
  • sidebar.php

Without discipline, you end up with:

  • #header .nav ul li a.active (specificity nightmare)
  • Duplicate styles across files
  • Fear of deleting anything

My Solution: BEM + ITCSS + Custom Properties

1. File Structure (ITCSS)

/* css/
   ├── 01-settings/ (variables, fonts)
   ├── 02-tools/ (mixins, functions)
   ├── 03-generic/ (reset, box-sizing)
   ├── 04-elements/ (h1, p, a, etc.)
   ├── 05-objects/ (layout, grid, container)
   ├── 06-components/ (buttons, cards, nav)
   └── 07-utilities/ (text-center, hidden)
*/

2. Design Tokens (Custom Properties)

/* 01-settings/tokens.css */
:root {
    /* Colors */
    --color-primary: #3b82f6;
    --color-surface: #ffffff;
    --color-text: #1f2937;
    
    /* Dark mode ready */
    --color-surface-dark: #111827;
    --color-text-dark: #f3f4f6;
    
    /* Spacing scale */
    --space-xs: 0.5rem;
    --space-sm: 1rem;
    --space-md: 1.5rem;
    --space-lg: 2rem;
    --space-xl: 4rem;
    
    /* Typography */
    --font-sans: 'Inter', system-ui, sans-serif;
    --font-mono: 'JetBrains Mono', monospace;
    
    /* Transitions */
    --transition-fast: 150ms ease;
    --transition-base: 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Auto dark mode */
@media (prefers-color-scheme: dark) {
    :root {
        --color-surface: var(--color-surface-dark);
        --color-text: var(--color-text-dark);
    }
}

3. BEM for Components

Since PHP includes render specific components, BEM keeps them isolated:

/* 06-components/blog-card.css */
.blog-card {
    background: var(--color-surface);
    border-radius: 12px;
    padding: var(--space-md);
    transition: transform var(--transition-base);
}

.blog-card:hover {
    transform: translateY(-4px);
}

.blog-card__image {
    aspect-ratio: 16/9;
    object-fit: cover;
    width: 100%;
}

.blog-card__title {
    font-size: 1.25rem;
    margin-bottom: var(--space-xs);
}

.blog-card__meta {
    color: var(--color-text-muted);
    font-size: 0.875rem;
}

/* Modifier for featured posts */
.blog-card--featured {
    grid-column: span 2;
    border-left: 4px solid var(--color-primary);
}

4. PHP Integration

<?php // components/blog-card.php ?>
<article class="blog-card <?= $featured ? 'blog-card--featured' : '' ?>">
    <img class="blog-card__image" 
         src="<?= $post['image'] ?>" 
         alt="<?= htmlspecialchars($post['title']) ?>">
    
    <div class="blog-card__content">
        <h2 class="blog-card__title"><?= htmlspecialchars($post['title']) ?></h2>
        <div class="blog-card__meta">
            <time datetime="<?= $post['date'] ?>"><?= date('M j', strtotime($post['date'])) ?></time>
            <span>·</span>
            <span><?= $post['views'] ?> views</span>
        </div>
        <p class="blog-card__excerpt"><?= htmlspecialchars($post['excerpt']) ?></p>
    </div>
</article>

5. Critical CSS Inline

Since PHP controls the <head>, I inline critical CSS:

<?php // header.php ?>
<style>
    /* Critical above-fold styles */
    .header { /* ... */ }
    .hero { /* ... */ }
</style>

<link rel="preload" href="/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>

The Results

  • Zero specificity conflicts in 2 years of development
  • 4KB critical CSS (inlined)
  • 32KB total CSS (gzipped)
  • Instant dark mode (no JS flash)
Good CSS architecture is invisible. You only notice it when you don't have it.

More from the blog