Back to Hub
Frontend Fundamentals

CSS Advanced

#stacking-context#contain#content-visibility#CSS-Houdini#scroll-driven-animations#paint-worklet#layer#logical-properties

CSS Advanced

Beyond the basics lies a layer of CSS that most engineers never explore — yet it's where the real control lives. These concepts underpin every framework, design system, and performance-optimised site.


Stacking Contexts

A stacking context is an independent 3D rendering layer. Elements inside a stacking context are painted as a unit, and z-index only works within the same stacking context.

What creates a stacking context?

PropertyValue that triggers
positionAny value except static, with z-index set
opacity< 1
transformAny value except none
filterAny value except none
will-changeAny animatable property
isolationisolate
mix-blend-modeAny value except normal
containlayout, paint
clip-path, maskAny value except none
css
/* Classic z-index confusion */ .parent { position: relative; z-index: 1; } /* creates stacking context */ .child { position: absolute; z-index: 9999; } /* stuck inside parent's context */ .sibling { position: relative; z-index: 2; } /* appears above BOTH */ /* Fix: isolate a subtree's stacking context */ .scoped-component { isolation: isolate; /* creates context without affecting layout */ }

CSS Containment

contain tells the browser that an element is independent from the rest of the page, allowing rendering optimisations:

css
.card { contain: layout paint; /* layout changes inside don't affect outside */ } .widget { contain: strict; /* layout + paint + size — strongest isolation */ }
ValueWhat it isolates
sizeElement size is independent of its children
layoutInternal layout doesn't affect external elements
paintChildren clipped to element's box; offscreen not painted
styleCSS counters/quotes don't escape the element
contentlayout + paint + style
strictAll of the above

Use contain: layout paint on component roots to dramatically speed up layout recalculations.


content-visibility

content-visibility: auto skips rendering of off-screen content entirely — one of the highest-impact CSS performance properties:

css
.article { content-visibility: auto; /* Hint the browser about the element's expected size to prevent layout shift */ contain-intrinsic-size: auto 500px; }

The browser will skip style calculation, layout, and paint for elements with content-visibility: auto that are off-screen. When they scroll into view, they render. Google reports up to 7× rendering time improvements on long content pages.


CSS Logical Properties

Logical properties adapt to writing mode (LTR, RTL, vertical text) instead of hardcoding physical directions:

PhysicalLogical
margin-leftmargin-inline-start
margin-rightmargin-inline-end
margin-topmargin-block-start
padding-left / rightpadding-inline
padding-top / bottompadding-block
widthinline-size
heightblock-size
top, leftinset-block-start, inset-inline-start
border-topborder-block-start
css
.container { /* Physical — breaks in RTL layouts */ margin-left: auto; margin-right: auto; /* Logical — works in any writing mode */ margin-inline: auto; padding-block: 2rem; max-inline-size: 1280px; }

Cascade Layers (@layer)

Cascade layers give you explicit control over the cascade order — preventing specificity wars:

css
/* Declare layers in order (lower = lower priority) */ @layer reset, base, components, utilities; @layer reset { * { margin: 0; padding: 0; } } @layer base { h1 { font-size: 2rem; color: navy; } } @layer components { .card { padding: 1rem; background: white; } } @layer utilities { .mt-auto { margin-top: auto; } } /* Unlayered styles beat ALL layers regardless of specificity */ h1 { color: red !important; } /* ← overrides everything */

Rules in later layers beat earlier layers, regardless of specificity. This is especially useful when importing third-party CSS:

css
@import url('bootstrap.css') layer(bootstrap); @layer bootstrap, custom; /* custom always wins over bootstrap */

CSS Custom Properties — Advanced

css
/* Typed custom properties with @property */ @property --rotation { syntax: '<angle>'; inherits: false; initial-value: 0deg; } /* Now you can animate it! (non-@property vars can't be animated) */ @keyframes spin { to { --rotation: 360deg; } } .spinner { animation: spin 2s linear infinite; transform: rotate(var(--rotation)); }
css
/* Custom properties in calc() */ :root { --base-spacing: 4px; } .element { padding: calc(var(--base-spacing) * 4); /* 16px */ margin-top: calc(var(--base-spacing) * 8); /* 32px */ }

Environment Variables

css
/* Safe areas for notched devices */ padding-bottom: env(safe-area-inset-bottom); padding-top: env(safe-area-inset-top);

Scroll-Driven Animations

Animate elements based on scroll position — no JavaScript:

css
/* Link animation to scroll position */ @keyframes reveal { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .card { animation: reveal linear; animation-timeline: view(); /* tied to element's intersection with viewport */ animation-range: entry 0% entry 30%; /* play during entry phase */ } /* Progress bar tied to page scroll */ .progress-bar { animation: grow-bar linear; animation-timeline: scroll(root block); /* scroll of root on block axis */ animation-fill-mode: both; } @keyframes grow-bar { from { transform: scaleX(0); } to { transform: scaleX(1); } }
css
/* Named timelines for complex scenarios */ .container { scroll-timeline-name: --my-scroll; scroll-timeline-axis: block; } .child { animation-timeline: --my-scroll; }

View Transitions API

Animate between page states (or full page navigations in SPAs) with CSS:

javascript
// Trigger a view transition document.startViewTransition(() => { // DOM mutation happens here updateContent(); });
css
/* Default: cross-fade. Customise with pseudo-elements */ ::view-transition-old(root) { animation: slide-out 0.3s ease forwards; } ::view-transition-new(root) { animation: slide-in 0.3s ease forwards; } /* Named view transitions for individual elements */ .hero-image { view-transition-name: hero; } /* The hero will animate independently (morphing between pages) */

CSS Houdini

CSS Houdini exposes browser rendering internals to JavaScript — letting you extend CSS itself.

Paint API (CSS Paint Worklet)

javascript
// my-painter.js (loaded as a worklet) class CheckerboardPainter { static get inputProperties() { return ['--checkerboard-size']; } paint(ctx, size, props) { const tileSize = parseInt(props.get('--checkerboard-size')) || 20; for (let y = 0; y < size.height; y += tileSize) { for (let x = 0; x < size.width; x += tileSize) { ctx.fillStyle = ((x + y) / tileSize) % 2 === 0 ? '#fff' : '#eee'; ctx.fillRect(x, y, tileSize, tileSize); } } } } registerPaint('checkerboard', CheckerboardPainter);
javascript
// main.js CSS.paintWorklet.addModule('./my-painter.js');
css
.element { --checkerboard-size: 30; background: paint(checkerboard); }

Layout API (experimental)

Define custom layout algorithms (like display: masonry before it's native):

javascript
registerLayout('masonry', class { async layout(children, edges, constraints, styleMap) { // Custom layout algorithm } });

Container Queries (Advanced Usage)

css
/* Style queries — query computed style values */ @container style(--variant: primary) { .button { background: blue; } } /* Size queries with different container types */ .sidebar { container-type: inline-size; } .widget { container-type: size; } /* both dimensions queryable */ /* Named containers */ .card-grid { container: card-grid / inline-size; } @container card-grid (width > 600px) { .card { grid-template-columns: 1fr 1fr; } }

The :has() Selector — "Parent Selector"

Select elements based on their descendants — a long-requested feature finally in all modern browsers:

css
/* Form with an error */ .form:has(.error) { border: 2px solid red; } /* Card with an image */ .card:has(> img) { padding-top: 0; } /* Input that's focused */ label:has(+ input:focus) { color: blue; font-weight: bold; } /* nth-child based on sibling count — count siblings */ li:has(~ li:nth-last-child(1)):first-child { /* first child when there are exactly 2 items */ } /* Dark mode class on :root */ :root:has(.dark-mode-toggle:checked) { --bg: #0f172a; --fg: #f8fafc; }

@scope — CSS Scoping

Restrict styles to a specific subtree without classes or increased specificity:

css
@scope (.card) { /* Only applies inside .card elements */ h2 { font-size: 1.25rem; } p { color: #666; } } /* Scope with lower boundary — exclude nested cards */ @scope (.card) to (.card .card) { h2 { color: navy; } }

CSS Nesting (Native)

css
/* Now supported natively (no preprocessor needed) */ .card { padding: 1rem; background: white; &:hover { background: #f8fafc; } & .title { font-size: 1.25rem; font-weight: 700; } @media (min-width: 768px) { padding: 2rem; } @container (width > 400px) { flex-direction: row; } }

Performance — CSS Properties by Rendering Cost

PropertyTriggersCost
transform, opacityComposite only🟢 Cheapest
filter (most)Paint + Composite🟡 Medium
color, backgroundPaint🟡 Medium
width, height, marginLayout + Paint + Composite🔴 Expensive
font-size on parentLayout subtree🔴 Expensive
css
/* ✅ Animate on compositor — no layout or paint */ .drawer { transition: transform 0.3s ease; } .drawer.open { transform: translateX(0); } .drawer.closed { transform: translateX(-100%); } /* ❌ Animate layout properties — triggers full reflow */ .drawer { transition: left 0.3s ease; } .drawer.open { left: 0; }

Always use will-change: transform before complex animations — promotes element to its own GPU layer:

css
.animated-element { will-change: transform; /* hint to browser before animation starts */ } /* Remove will-change after animation — it's not free */