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.phpfooter.phppost-card.phpsidebar.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.