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 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> và 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}:
| Unit | Meaning {Nghĩa} |
|---|---|
cqw | 1% of container width {1% chiều rộng container} |
cqh | 1% of container height {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 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}
| Feature | Chrome | Firefox | Safari | Status {Trạng thái} |
|---|---|---|---|---|
| Container Queries | 105+ | 110+ | 16+ | Safe to use {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 | ❌ | Progressive enhancement {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 |
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 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() |
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}.