HC

Modern CSS

HTML & CSS · 12 entries

Native CSS Nesting

syntax
.parent {
  /* parent styles */
  .child {
    /* nested child styles */
  }
  &:hover {
    /* parent hover */
  }
}
example
.card {
  padding: 16px;
  border-radius: 8px;

  .card-title {
    font-size: 1.25rem;
    font-weight: 600;
  }

  .card-body {
    margin-top: 8px;
    color: #4b5563;
  }

  &:hover {
    box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
  }

  @media (min-width: 768px) {
    padding: 24px;
  }
}

Note Native CSS nesting works in all major browsers without a preprocessor. Use & for pseudo-classes and pseudo-elements. Media queries can be nested inside rules. You no longer need the & prefix for simple descendant selectors like .child.

Cascade Layers (@layer)

syntax
@layer layer-name { ... }
@layer base, components, utilities;
example
/* Declare layer order (lowest to highest priority) */
@layer reset, base, components, utilities;

@layer reset {
  * { margin: 0; padding: 0; box-sizing: border-box; }
}

@layer base {
  body { font-family: system-ui, sans-serif; line-height: 1.6; }
  a { color: #2563eb; }
}

@layer components {
  .button { padding: 8px 16px; border-radius: 6px; }
}

@layer utilities {
  .hidden { display: none; }
  .mt-4 { margin-top: 16px; }
}

Note Layers control the cascade order regardless of specificity or source order within each layer. Later layers in the declaration beat earlier ones. Unlayered styles beat all layered styles. This solves specificity wars with third-party CSS libraries.

@scope

syntax
@scope (.scope-root) to (.scope-limit) {
  /* styles apply between root and limit */
}
example
@scope (.card) to (.card-footer) {
  p {
    color: #374151;
    line-height: 1.6;
  }

  a {
    color: #2563eb;
    text-decoration: underline;
  }
}

/* The .card-footer and its contents are excluded */

Note @scope limits where styles apply by defining both a root and an optional lower boundary. Styles only match elements between the root and the limit, not inside the limit. This provides true component scoping without build tools.

@property (Typed Custom Properties)

syntax
@property --name {
  syntax: "<type>";
  inherits: true | false;
  initial-value: value;
}
example
@property --gradient-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.gradient-box {
  --gradient-angle: 0deg;
  background: linear-gradient(var(--gradient-angle), #3b82f6, #8b5cf6);
  transition: --gradient-angle 0.5s ease;
}

.gradient-box:hover {
  --gradient-angle: 180deg;
}

Note @property registers a custom property with a specific type, enabling the browser to animate it. Without registration, custom properties are strings that cannot be transitioned. Supported types include <color>, <length>, <angle>, <number>, <percentage>.

Advanced :has() Patterns

syntax
/* Form validation */
.field:has(input:invalid) { }
/* State-driven layouts */
body:has(.modal[open]) { }
example
/* Dim page when modal is open */
body:has(dialog[open]) {
  overflow: hidden;
}

/* Change grid layout based on number of children */
.grid:has(> :nth-child(4)) {
  grid-template-columns: repeat(2, 1fr);
}

.grid:has(> :nth-child(7)) {
  grid-template-columns: repeat(3, 1fr);
}

/* Style label when sibling input is focused */
.field:has(input:focus) label {
  color: #2563eb;
}

Note :has() can check for any condition: child state, sibling state, attribute presence, or even count of children. It enables previously impossible CSS-only patterns like conditional layouts and form state styling without JavaScript.

Scroll-Driven Animations

syntax
animation-timeline: scroll() | view();
animation-range: start end;
example
/* Progress bar tied to page scroll */
.scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  background: #3b82f6;
  transform-origin: left;
  animation: scaleProgress linear;
  animation-timeline: scroll();
}

@keyframes scaleProgress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

/* Fade in when element enters viewport */
.reveal {
  animation: fadeIn linear;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(30px); }
  to { opacity: 1; transform: translateY(0); }
}

Note scroll() ties animation progress to scroll position of a scroll container. view() ties it to the element's visibility in the viewport. animation-range controls which portion of the scroll/view timeline the animation occupies. No JavaScript needed for scroll-linked effects.

CSS Anchor Positioning

syntax
.anchor {
  anchor-name: --name;
}
.tooltip {
  position: absolute;
  position-anchor: --name;
  top: anchor(bottom);
  left: anchor(center);
}
example
.trigger {
  anchor-name: --trigger;
}

.popover {
  position: absolute;
  position-anchor: --trigger;
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% 0;
  margin-bottom: 8px;

  /* Fallback if not enough space above */
  position-try-fallbacks: flip-block;
}

Note Anchor positioning lets you position elements relative to other elements anywhere in the DOM without a shared parent. position-try-fallbacks automatically repositions (flips) when the element would overflow the viewport. This replaces JavaScript-based tooltip/popover positioning libraries.

Light-Dark Function

syntax
color-scheme: light dark;
color: light-dark(light-value, dark-value);
example
:root {
  color-scheme: light dark;
}

body {
  background: light-dark(#ffffff, #0f172a);
  color: light-dark(#1e293b, #e2e8f0);
}

.card {
  background: light-dark(#f8fafc, #1e293b);
  border: 1px solid light-dark(#e2e8f0, #334155);
}

Note light-dark() provides dark mode support inline without media queries. It uses the color-scheme property to determine which value to use. Declare color-scheme: light dark on :root to enable it. This is much simpler than managing separate media query blocks for colors.

Popover API (HTML + CSS)

syntax
<button popovertarget="id">Toggle</button>
<div id="id" popover>Content</div>
example
<button popovertarget="menu">Open Menu</button>
<div id="menu" popover>
  <nav>
    <a href="/settings">Settings</a>
    <a href="/profile">Profile</a>
    <a href="/logout">Log Out</a>
  </nav>
</div>

<style>
[popover] {
  padding: 16px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
}

[popover]::backdrop {
  background: rgb(0 0 0 / 0.1);
}
</style>

Note The popover attribute creates a top-layer element that dismisses on outside click or Escape key, with no JavaScript required. It handles focus trapping and accessibility automatically. Style the ::backdrop pseudo-element for the overlay behind it.

@starting-style (Entry Animations)

syntax
@starting-style {
  .element {
    /* initial state before transition */
  }
}
example
dialog[open] {
  opacity: 1;
  transform: scale(1);
  transition: opacity 0.3s ease, transform 0.3s ease,
             display 0.3s ease allow-discrete;
}

@starting-style {
  dialog[open] {
    opacity: 0;
    transform: scale(0.95);
  }
}

/* Exit animation */
dialog:not([open]) {
  opacity: 0;
  transform: scale(0.95);
}

Note @starting-style defines the initial state for elements transitioning from display: none (or not yet rendered). Without it, browsers cannot animate elements that appear for the first time. Combined with allow-discrete on the display property, it enables pure CSS enter/exit animations.

Modern Text Wrapping

syntax
text-wrap: wrap | nowrap | balance | pretty | stable;
example
h1, h2, h3, h4 {
  text-wrap: balance;
}

article p {
  text-wrap: pretty;
}

.editable-field {
  text-wrap: stable;
}

Note balance evenly distributes text across lines (best for headings). pretty avoids orphaned words on the last line (best for paragraphs). stable prevents text from reflowing while the user is editing. These require no JavaScript and are progressive enhancements.

Relative Color Syntax

syntax
color: oklch(from var(--base) calc(l * factor) c h);
example
:root {
  --brand: oklch(0.6 0.15 250);
}

.button {
  background: var(--brand);
}

.button:hover {
  /* 20% darker, same chroma and hue */
  background: oklch(from var(--brand) calc(l - 0.1) c h);
}

.button-light {
  /* Much lighter variant */
  background: oklch(from var(--brand) 0.95 calc(c * 0.3) h);
}

Note Relative color syntax creates color variations from a base color by decomposing it into components and modifying them with calc(). This works in any color space (oklch, hsl, srgb). It is the most powerful approach for generating color palettes from a single token.