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.
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> và 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:
| Unit | Nghĩa |
|---|---|
cqw | 1% 1% chiều rộng container |
cqh | 1% 1% chiều cao container |
cqi | 1% of container inline size |
cqb | 1% of container block size |
cqmin | Smaller of cqi/cqb |
cqmax | Larger 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
| Feature | Chrome | Firefox | Safari | Trạng thái |
|---|---|---|---|---|
| Container Queries | 105+ | 110+ | 16+ | An toàn |
:has() | 105+ | 121+ | 15.4+ | Safe to use |
| Native Nesting | 120+ | 117+ | 17.2+ | Safe to use |
@layer | 99+ | 97+ | 15.4+ | Safe to use |
<dialog> | 37+ | 98+ | 15.4+ | Safe to use |
| Popover API | 114+ | 125+ | 17+ | Safe to use |
| Scroll-driven Animations | 115+ | Nightly | ❌ | Nâng cao dần |
| Anchor Positioning | 125+ | ❌ | ❌ | Progressive enhancement |
@scope | 118+ | Nightly | 17.4+ | Progressive enhancement |
@starting-style | 117+ | ❌ | 17.5+ | Progressive enhancement |
@property | 85+ | 128+ | 16.4+ | Safe to use |
| Subgrid | 117+ | 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ước | Bây giờ |
|---|---|
| Media queries for component responsiveness | Container Queries |
| JS for parent-based styling | :has() |
| Sass/LESS for nesting | Native nesting |
!important wars | @layer |
| JS modal libraries | <dialog> + Popover API |
| JS tooltip positioning (Popper/Floating UI) | Anchor Positioning |
| IntersectionObserver for scroll animations | Scroll-driven animations |
| CSS Modules for scoping | @scope |
| JS for entry animations | @starting-style |
| Sass color functions | color-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.