jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Modern CSS Features You Should Be Using in 2026

A bilingual guide to the CSS features that have changed how we build UIs — container queries, :has(), native nesting, cascade layers, popover, anchor positioning, scroll-driven animations, and more.

11 MIN READ

CSS Has Grown Up {CSS đã trưởng thành}

If you learned CSS before 2023 {Nếu bạn học CSS trước 2023}, you’re missing half the language {bạn đang thiếu nửa ngôn ngữ}. The features that shipped between 2023-2026 are not incremental improvements {Các tính năng ra mắt từ 2023-2026 không phải cải tiến nhỏ} — they fundamentally change what’s possible without JavaScript {chúng thay đổi căn bản những gì có thể làm mà không cần JavaScript}.

This post covers the features that are production-ready now {Bài viết này bao gồm các tính năng sẵn sàng production bây giờ} with universal browser support {với hỗ trợ trình duyệt phổ quát}.


Live Demo {Demo trực tiếp}

Many of the features below are best understood by touching them {Nhiều tính năng dưới đây dễ hiểu nhất khi bạn tự nghịch}. This demo has live, native-CSS examples {Demo này có ví dụ CSS native trực tiếp}: a container-query card you resize {một card container-query bạn tự kéo giãn}, a :has() form that reacts to its inputs {một form :has() phản hồi theo input}, a native <dialog> and Popover {một <dialog>Popover native}, scroll-driven animations {animation theo cuộn}, and a color-mix() generator {và một trình tạo màu color-mix()}.

Open the full demo {Mở demo đầy đủ}: /tools/modern-css-demo/.


Container Queries {Container Queries}

The Problem {Vấn đề}

Media queries respond to the viewport {Media query phản hồi viewport}. But components live in containers of varying sizes {Nhưng component sống trong container có kích thước khác nhau} — a card in a sidebar is different from a card in main content {một card trong sidebar khác với card trong main content}.

The Solution {Giải pháp}

Container queries let elements respond to their parent’s size {Container query cho phép element phản hồi kích thước parent}:

/* Define the container */
.card-grid {
  container-type: inline-size;
  container-name: card-grid;
}

/* Respond to container width, not viewport */
@container card-grid (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
    gap: 1rem;
  }
}

@container card-grid (min-width: 700px) {
  .card {
    grid-template-columns: 250px 1fr 100px;
  }
}

Container Query Units {Đơn vị Container Query}

Size relative to the container, not viewport {Kích thước tương đối với container, không phải viewport}:

UnitMeaning {Nghĩa}
cqw1% of container width {1% chiều rộng container}
cqh1% of container height {1% chiều cao container}
cqi1% of container inline size
cqb1% of container block size
cqminSmaller of cqi/cqb
cqmaxLarger of cqi/cqb
.card-title {
  font-size: clamp(1rem, 3cqi, 2rem);
}

Style Queries {Style Query}

Query the computed value of a custom property {Truy vấn giá trị computed của custom property}:

.card {
  --variant: default;
}

@container style(--variant: featured) {
  .card-title {
    font-size: 1.5rem;
    color: var(--color-accent);
  }
}

The :has() Selector {Selector :has()}

The “parent selector” CSS never had {“Parent selector” mà CSS chưa bao giờ có} — until now {cho đến bây giờ}:

/* Style a card differently if it contains an image */
.card:has(img) {
  grid-template-columns: 200px 1fr;
}

/* Style a card differently if it does NOT contain an image */
.card:not(:has(img)) {
  padding: 2rem;
}

/* Style a form group if its input is invalid */
.form-group:has(:invalid) {
  border-left: 3px solid var(--color-error);
}

/* Style a nav if it contains a dropdown */
nav:has(.dropdown.open) {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

/* Select the previous sibling (!) */
/* Style a label that is BEFORE an invalid input */
label:has(+ input:invalid) {
  color: var(--color-error);
}

Use cases {Trường hợp sử dụng}:

  • Conditional layouts based on content {Layout có điều kiện dựa trên nội dung}
  • Form validation styling without JS {Styling validation form không cần JS}
  • Sibling-dependent styling {Styling phụ thuộc anh em}
  • Empty state detection {Phát hiện trạng thái trống}: .list:not(:has(.item))

Native CSS Nesting {Nesting CSS native}

No more Sass/LESS just for nesting {Không cần Sass/LESS chỉ cho nesting nữa}:

/* Before: flat selectors everywhere */
.card { border: 1px solid var(--color-border); }
.card:hover { border-color: var(--color-accent); }
.card .title { font-weight: 600; }
.card .title:hover { text-decoration: underline; }

/* After: native nesting */
.card {
  border: 1px solid var(--color-border);

  &:hover {
    border-color: var(--color-accent);
  }

  .title {
    font-weight: 600;

    &:hover {
      text-decoration: underline;
    }
  }

  @media (width >= 768px) {
    padding: 2rem;
  }
}

Key difference from Sass {Khác biệt chính so với Sass}: the & is required for pseudo-classes and pseudo-elements {& bắt buộc cho pseudo-class và pseudo-element}. Without &, the nested selector must start with a symbol (., #, @, :, etc.) {Không có &, selector lồng phải bắt đầu bằng ký hiệu}.


Cascade Layers (@layer) {Cascade Layers}

Control specificity wars without !important {Kiểm soát cuộc chiến specificity mà không cần !important}:

/* Declare layer order — later layers win */
@layer reset, base, components, utilities;

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

@layer base {
  body { font-family: var(--font-mono); color: var(--color-fg); }
  a { color: var(--color-accent); }
}

@layer components {
  .btn {
    padding: 0.5rem 1rem;
    background: var(--color-accent);
    color: var(--color-accent-fg);
  }
}

@layer utilities {
  .hidden { display: none; }
  .sr-only { position: absolute; width: 1px; height: 1px; }
}

Why this matters {Tại sao điều này quan trọng}: a utility in the utilities layer will ALWAYS beat a component style in components layer {utility trong layer utilities sẽ LUÔN thắng style component trong layer components} — regardless of selector specificity {bất kể specificity của selector}. No more !important hacks {Không cần hack !important nữa}.


The <dialog> Element {Phần tử <dialog>}

Native modal without a single line of positioning CSS {Modal native không cần một dòng CSS định vị}:

<dialog id="confirm-dialog">
  <h2>Are you sure?</h2>
  <p>This action cannot be undone.</p>
  <form method="dialog">
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

<button onclick="document.getElementById('confirm-dialog').showModal()">
  Delete
</button>
dialog {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-fg);
  max-width: min(90vw, 500px);
  padding: 2rem;
}

/* The backdrop (free with showModal()) */
dialog::backdrop {
  background: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(4px);
}

/* Entry/exit animations */
dialog[open] {
  animation: fade-in 0.2s ease;
}

@starting-style {
  dialog[open] {
    opacity: 0;
    transform: translateY(-10px);
  }
}

@keyframes fade-in {
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: translateY(0); }
}

What you get for free {Bạn được miễn phí}:

  • Focus trap (keyboard users stay inside) {Bẫy focus (người dùng bàn phím ở trong)}
  • ESC to close {ESC để đóng}
  • Backdrop click handling via method="dialog" on form
  • Top-layer rendering (always above other content) {Render top-layer (luôn trên nội dung khác)}
  • Inert background (screen readers ignore content behind) {Background inert (screen reader bỏ qua nội dung phía sau)}

Popover API {Popover API}

Like dialog but for non-modal overlays {Giống dialog nhưng cho overlay không modal} — tooltips, dropdowns, menus {tooltip, dropdown, menu}:

<button popovertarget="menu">Open Menu</button>

<div id="menu" popover>
  <nav>
    <a href="/profile">Profile</a>
    <a href="/settings">Settings</a>
    <a href="/logout">Logout</a>
  </nav>
</div>
[popover] {
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  border-radius: var(--radius-sm);
  padding: 0.5rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}

No JavaScript needed for open/close {Không cần JavaScript để mở/đóng}. The browser handles {Trình duyệt xử lý}: light dismiss (click outside), ESC, top-layer, focus management {click ngoài đóng, ESC, top-layer, quản lý focus}.


Anchor Positioning {Định vị neo}

Position an element relative to another element anywhere in the DOM {Định vị element so với element khác ở bất kỳ đâu trong DOM}:

.trigger {
  anchor-name: --menu-trigger;
}

.menu {
  position: fixed;
  position-anchor: --menu-trigger;

  /* Position below the trigger, aligned to its left edge */
  top: anchor(bottom);
  left: anchor(left);

  /* Fallback if it overflows viewport */
  position-try-fallbacks: flip-block;
}

Use cases {Trường hợp sử dụng}: tooltips that follow their trigger {tooltip theo sát trigger}, dropdown menus, popovers that flip when near viewport edges {popover lật khi gần rìa viewport}.


Scroll-Driven Animations {Animation theo cuộn}

Animate elements based on scroll position — entirely on the compositor thread {Animate element dựa trên vị trí cuộn — hoàn toàn trên thread compositor}:

/* Animate as the page scrolls */
.progress-bar {
  animation: grow linear;
  animation-timeline: scroll();
}

@keyframes grow {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}
/* Animate when element enters viewport */
.card {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

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

Why this is revolutionary {Tại sao đây là cách mạng}: scroll-linked animations previously required JavaScript with IntersectionObserver or scroll event listeners {animation liên kết scroll trước đây cần JavaScript với IntersectionObserver hoặc scroll event listener}. Now they run on the compositor thread {Giờ chúng chạy trên thread compositor} — zero main thread cost, butter-smooth 120fps {không tốn main thread, mượt 120fps}.


@scope {@scope}

Limit style reach to a specific DOM subtree {Giới hạn phạm vi style đến subtree DOM cụ thể}:

@scope (.card) to (.card-footer) {
  /* These styles ONLY apply inside .card but NOT inside .card-footer */
  p { color: var(--color-fg-muted); }
  a { color: var(--color-accent); }
}

Use case {Trường hợp sử dụng}: prevent styles from “leaking” into nested components {ngăn style “rò rỉ” vào component lồng nhau}. Think of it as CSS Modules behavior but native {Nghĩ như hành vi CSS Modules nhưng native}.


@starting-style {@starting-style}

Define the initial state for entry animations without JavaScript {Định nghĩa trạng thái ban đầu cho animation vào mà không cần JavaScript}:

.toast {
  opacity: 1;
  transform: translateX(0);
  transition: opacity 0.3s, transform 0.3s;
}

/* When the element FIRST appears, start from this state */
@starting-style {
  .toast {
    opacity: 0;
    transform: translateX(100%);
  }
}

This replaces the common pattern of {Thay thế pattern phổ biến}: add class → wait a frame → add another class {thêm class → đợi một frame → thêm class khác}. Pure CSS entry animations {Animation vào thuần CSS}.


color-mix() and Modern Color Functions {color-mix() và hàm màu hiện đại}

Generate color variations without preprocessors {Tạo biến thể màu không cần preprocessor}:

:root {
  --brand: #c8ff00;

  /* Lighter/darker variants */
  --brand-light: color-mix(in oklch, var(--brand), white 30%);
  --brand-dark: color-mix(in oklch, var(--brand), black 30%);

  /* Semi-transparent */
  --brand-ghost: color-mix(in oklch, var(--brand), transparent 80%);
}

/* Automatic light/dark theming */
.surface {
  background: light-dark(#ffffff, #0a0a0a);
  color: light-dark(#1a1a1a, #e5e5e5);
}

@property — Typed Custom Properties {Custom Property có kiểu}

Animate custom properties by declaring their type {Animate custom property bằng cách khai báo kiểu}:

@property --gradient-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

.conic-loader {
  --gradient-angle: 0deg;
  background: conic-gradient(
    from var(--gradient-angle),
    var(--color-accent),
    transparent
  );
  animation: spin 2s linear infinite;
}

@keyframes spin {
  to { --gradient-angle: 360deg; }
}

Without @property, the browser doesn’t know --gradient-angle is an angle {Không có @property, browser không biết --gradient-angle là góc} and can’t interpolate it smoothly {và không thể nội suy mượt}.


Subgrid {Subgrid}

Align nested grid items to the parent grid {Căn chỉnh grid item lồng theo parent grid}:

.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
}

.card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3; /* Card spans 3 implicit rows */
}

Result {Kết quả}: all card titles align, all card bodies align, all card footers align {tất cả tiêu đề card căn hàng, tất cả body card căn hàng, tất cả footer card căn hàng} — even if content length varies {ngay cả khi độ dài nội dung khác nhau}.


Browser Support Summary {Tổng kết hỗ trợ trình duyệt}

FeatureChromeFirefoxSafariStatus {Trạng thái}
Container Queries105+110+16+Safe to use {An toàn}
:has()105+121+15.4+Safe to use
Native Nesting120+117+17.2+Safe to use
@layer99+97+15.4+Safe to use
<dialog>37+98+15.4+Safe to use
Popover API114+125+17+Safe to use
Scroll-driven Animations115+NightlyProgressive enhancement {Nâng cao dần}
Anchor Positioning125+Progressive enhancement
@scope118+Nightly17.4+Progressive enhancement
@starting-style117+17.5+Progressive enhancement
@property85+128+16.4+Safe to use
Subgrid117+71+16+Safe to use
color-mix()111+113+16.2+Safe to use

The Shift {Sự chuyển dịch}

The modern CSS stack replaces what used to require JavaScript or preprocessors {CSS stack hiện đại thay thế những gì từng cần JavaScript hoặc preprocessor}:

Before {Trước}Now {Bây giờ}
Media queries for component responsivenessContainer Queries
JS for parent-based styling:has()
Sass/LESS for nestingNative nesting
!important wars@layer
JS modal libraries<dialog> + Popover API
JS tooltip positioning (Popper/Floating UI)Anchor Positioning
IntersectionObserver for scroll animationsScroll-driven animations
CSS Modules for scoping@scope
JS for entry animations@starting-style
Sass color functionscolor-mix() / oklch()

Learn these now {Học những thứ này bây giờ}. They’re not “future CSS” — they’re today’s CSS {Chúng không phải “CSS tương lai” — mà là CSS hôm nay}.