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.

8 MIN READ

CSS đã trưởng thành

Nếu bạn học CSS trước 2023, bạn đang thiếu nửa ngôn ngữ. Các tính năng ra mắt từ 2023-2026 không phải cải tiến nhỏ — chúng thay đổi căn bản những gì có thể làm mà không cần JavaScript.

Bài viết này bao gồm các tính năng sẵn sàng production bây giờ với hỗ trợ trình duyệt phổ quát.


Demo trực tiếp

Nhiều tính năng dưới đây dễ hiểu nhất khi bạn tự nghịch. Demo này có ví dụ CSS native trực tiếp: một card container-query bạn tự kéo giãn, một form :has() phản hồi theo input, một <dialog>Popover native, animation theo cuộn, và một trình tạo màu color-mix().

Mở demo đầy đủ:


Container Queries

Vấn đề

Media query phản hồi viewport. Nhưng component sống trong container có kích thước khác nhau — một card trong sidebar khác với card trong main content.

Giải pháp

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;
  }
}

Đơn vị Container Query

Kích thước tương đối với container, không phải viewport:

UnitNghĩa
cqw1% 1% chiều rộng container
cqh1% 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 Query

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);
  }
}

Selector :has()

“Parent selector” mà CSS chưa bao giờ có — 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);
}

Trường hợp sử dụng:

  • Layout có điều kiện dựa trên nội dung
  • Styling validation form không cần JS
  • Styling phụ thuộc anh em
  • Phát hiện trạng thái trống: .list:not(:has(.item))

Nesting CSS native

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;
  }
}

Khác biệt chính so với Sass: & bắt buộc cho pseudo-class và pseudo-element. Không có &, selector lồng phải bắt đầu bằng ký hiệu.


Cascade Layers

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; }
}

Tại sao điều này quan trọng: utility trong layer utilities sẽ LUÔN thắng style component trong layer components — bất kể specificity của selector. Không cần hack !important nữa.


Phần tử <dialog>

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); }
}

Bạn được miễn phí:

  • Bẫy focus (người dùng bàn phím ở trong)
  • ESC để đóng
  • Backdrop click handling via method="dialog" on form
  • Render top-layer (luôn trên nội dung khác)
  • Background inert (screen reader bỏ qua nội dung phía sau)

Popover API

Giống dialog nhưng cho overlay không modal — 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);
}

Không cần JavaScript để mở/đóng. Trình duyệt xử lý: click ngoài đóng, ESC, top-layer, quản lý focus.


Định vị neo

Đị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;
}

Trường hợp sử dụng: tooltip theo sát trigger, popover lật khi gần rìa viewport.


Animation theo cuộn

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);
  }
}

Tại sao đây là cách mạng: animation liên kết scroll trước đây cần JavaScript với IntersectionObserver hoặc scroll event listener. Giờ chúng chạy trên thread compositor — không tốn main thread, mượt 120fps.


@scope

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); }
}

Trường hợp sử dụng: ngăn style “rò rỉ” vào component lồng nhau. Nghĩ như hành vi CSS Modules nhưng native.


@starting-style

Đị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%);
  }
}

Thay thế pattern phổ biến: thêm class → đợi một frame → thêm class khác. Animation vào thuần CSS.


color-mix() và hàm màu hiện đại

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);
}

Custom Property có kiểu

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; }
}

Không có @property, browser không biết --gradient-angle là góc và không thể nội suy mượt.


Subgrid

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 */
}

Kết quả: 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 — ngay cả khi độ dài nội dung khác nhau.


Tổng kết hỗ trợ trình duyệt

FeatureChromeFirefoxSafariTrạng thái
Container Queries105+110+16+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+NightlyNâ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

Sự chuyển dịch

CSS stack hiện đại thay thế những gì từng cần JavaScript hoặc preprocessor:

TrướcBâ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()

Học những thứ này bây giờ. Chúng không phải “CSS tương lai” — mà là CSS hôm nay.