- Cú pháp cơ bản và tư duy sử dụng
- Form UX không cần JS
- Style “phần tử đứng trước” nhờ sibling
- Card layout linh hoạt theo nội dung
- Menu thông minh theo trạng thái con
- Kết hợp với :not(), :is(), :where() để viết selector gọn
- Container Queries + :has(): component “tự hiểu bối cảnh”
- Hiệu năng và best practices
- Khả năng tương thích và fallback
- Tóm tắt nhanh
Cú pháp cơ bản và tư duy sử dụng
Dạng tổng quát: A:has(B) → chọn A nếu A có B khớp selector bên trong. B là selector tương đối (descendant, child, sibling…)
/* Style .card nếu bên trong có <img> */
.card:has(img) { border: 1px solid var(--accent); }
/* Chỉ khi .card có > h2 ở trực tiếp */
.card:has(> h2) { padding-top: 0.75rem; }
/* Nav item “đang active”: .nav li có <a aria-current> */
.nav li:has(> a[aria-current="page"]) { font-weight: 700; }
Form UX không cần JS
Chuyển trạng thái nhóm field theo input bên trong, làm nổi bật lỗi hợp lệ/không hợp lệ:
.field:has(input:focus) { outline: 2px solid var(--focus); }
.field:has(input:invalid) .error { display: block; }
.field:has(input:valid) .hint { opacity: .4; }
Style “phần tử đứng trước” nhờ sibling
:has() kiểm tra sibling phía sau để chọn phần tử hiện tại. Đây là mẹo để style phần tử đứng trước một phần tử “đặc biệt”.
/* Tô màu item đứng trước item đang active */
.item:has(+ .item--active) { border-right: 2px solid var(--accent); }
/* Breadcrumb: ẩn dấu chia khi phần tử sau là phần cuối */
.breadcrumb li:has(+ li[aria-current="page"])::after { content: "/"; }
.breadcrumb li[aria-current="page"]::after { content: ""; }
Card layout linh hoạt theo nội dung
Component tự điều chỉnh layout dựa vào nội dung thực tế, không lệ thuộc viewport:
.product-card { display: grid; grid-template-columns: 1fr; gap: 12px; }
/* Nếu có cả ảnh và badge thì chuyển sang layout 2 cột */
.product-card:has(img):has(.badge) {
grid-template-columns: 120px 1fr;
align-items: start;
}
/* Nếu có video thì ưu tiên aspect-ratio lớn hơn */
.product-card:has(video) video { aspect-ratio: 16 / 9; }
Menu thông minh theo trạng thái con
Đóng/mở submenu, đổi icon chỉ thuần CSS:
.menu__item:has(> .submenu[open]) > .toggle { transform: rotate(90deg); }
.menu__item:has(> .submenu[open]) { background: var(--soft); }
Kết hợp với :not(), :is(), :where() để viết selector gọn
/* Container có form nhưng KHÔNG có nút submit bị disabled */
.panel:has(form):has(:not(button[type="submit"]:disabled)) { box-shadow: 0 0 0 1px var(--ok); }
/* Bất kỳ section có tiêu đề h2/h3 */
section:has(:is(h2, h3)) { scroll-margin-top: 80px; }
Container Queries + :has(): component “tự hiểu bối cảnh”
Viết style theo kích thước container và cấu trúc bên trong một cách tinh gọn:
.card-wrap { container-type: inline-size; container-name: card; }
@container card (max-width: 420px) {
.card:has(.media) { grid-template-columns: 1fr; }
.card:has(.actions) .actions { justify-content: stretch; }
}
Hiệu năng và best practices
- Scope selector: Luôn giới hạn phạm vi (ví dụ
.card:has(img)thay vì*:has(img)). - Tránh lồng quá sâu: Nhiều
:has()lồng nhau làm selector phức tạp và khó debug. - Dùng cho logic UI rõ ràng: Trạng thái focus/valid, hiện/ẩn block, nhánh layout theo nội dung thực.
- Kiểm tra trong DevTools: Mở tab “Elements” và thử bật tắt class/DOM để xem rule khớp.
Khả năng tương thích và fallback
:has() đã được hỗ trợ rộng rãi trên trình duyệt hiện đại. Với môi trường cũ, cân nhắc:
- Progressive enhancement: Tính năng chính chạy được; hiệu ứng nâng cao dùng
:has()để tăng UX. - Class hook từ server/JS: Nếu cần tương thích tuyệt đối, thêm class vào parent như
.has-imgđể thay thế.
Tóm tắt nhanh
:has()cho phép viết “parent selector” thực dụng: chọn cha/tổ tiên dựa vào con hoặc sibling theo sau.- Giảm phụ thuộc JS cho các logic UI điển hình: form state, menu, card layout theo nội dung.
- Kết hợp tốt với Container Queries, Grid/Flex, và các pseudo-class hiện đại.
- Viết selector có phạm vi rõ ràng để đảm bảo hiệu năng và dễ bảo trì lâu dài.
Bình luận