Workflow comparisons
12 old vs modern workflow CSS techniques, side by side.
Browser compatibility:
CSS feature detection without JavaScript
Widely available Old
// JS: detect support, add classif (CSS.supports('container-type', 'inline-size')) { document.documentElement.classList.add('has-container-queries');} Modern
@supports (display: grid) { .layout { display: grid; }} Range style queries without multiple blocks
Limited availability Old
/* Multiple discrete checks */@container style(--progress: 0%) { .bar { background: red; }}@container style(--progress: 25%) { .bar { background: orange; }}@container style(--progress: 50%) { .bar { background: yellow; }}/* ... repeat for every threshold *//* Or use JavaScript to set classes *//* based on the numeric value */ Modern
@container style( /* [!code ++] */--progress > 75% /* */) { .bar { background: var(--green); }} Typed attribute values without JavaScript
Limited availability Old
/* JS: read data attr and set style */document.querySelectorAll('.bar') .forEach(el => { const pct = el.dataset.pct; el.style.width = pct + '%'; el.style.setProperty( '--pct', pct );}); Modern
.bar { width: attr( data-pct type(<percentage>) );} Inline conditional styles without JavaScript
Limited availability Old
/* Multiple class variants */.btn { background: gray;}.btn.primary { background: blue;}.btn.danger { background: red;}/* Plus JS to manage state */el.classList.toggle( 'primary', isPrimary );/* Duplicated rules per variant */ Modern
.btn { background: if( style(--variant: primary): blue; else: gray );} Reusable CSS logic without Sass mixins
Limited availability Old
// Sass / SCSS@function fluid($min, $max) { @return clamp( /* [!code --] */ $min, calc($min + ($max - $min) * ((100vw - 320px) / 960)), $max ); /* [!code --] */} Modern
@function --fluid( /* */--min, --max /* */) { @return clamp( /* [!code ++] */ var(--min), /* [!code ++] */ 50vi, /* [!code ++] */ var(--max) /* [!code ++] */ );} Lazy rendering without IntersectionObserver
Newly available Old
// JavaScriptconst observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { renderContent(entry.target); } }); }); Modern
.section { content-visibility: auto; contain-intrinsic-size: auto 500px;} Scoped styles without BEM naming
Newly available Old
.card__title { font-size: 1.25rem; margin-bottom: 0.5rem;}.card__body { color: #444;}/* HTML: class="card__title" */ Modern
@scope (.card) { .title { font-size: 1.25rem; margin-bottom: 0.5rem; } .body { color: #444; }}/* HTML: class="card", class="title" */ Typed custom properties without JavaScript
Newly available Old
:root { --hue: 0;}.wheel { background: hsl(var(--hue), 80%, 50%); transition: --hue .3s; /* ignored, not interpolable */} Modern
@property --hue { syntax: "<angle>"; inherits: false; initial-value: 0deg;}.wheel { background: hsl(var(--hue), 80%, 50%); transition: --hue .3s;} Dark mode defaults without extra CSS
Widely available Old
@media (prefers-color-scheme: dark) { input, select, textarea, button { background: #333; color: #eee; } ::-webkit-scrollbar { ... }} Modern
:root { color-scheme: light dark;} Controlling specificity without !important
Widely available Old
.card .title { font-size: 1rem;}.page .card .title { font-size: 1.25rem;}.page .card .title.special { color: red !important;}// More selectors or !important to win Modern
@layer base, components, utilities;@layer utilities { .mt-4 { margin-top: 1rem; }} Theme variables without a preprocessor
Widely available Old
// Sass/LESS: compile-time only$primary: #7c3aed;$spacing: 16px;.btn { background: $primary; padding: $spacing;} Modern
:root { --primary: #7c3aed; --spacing: 16px;}.btn { background: var(--primary);} Nesting selectors without Sass or Less
Newly available Old
// nav.scss, requires Sass compiler.nav { display: flex; gap: 8px; & a { color: blue; }} Modern
/* nav.css, plain CSS, no compiler */.nav { display: flex; gap: 8px; & a { color: #888; text-decoration: none; &:hover { color: white; } }}