Tiêu đề cố định là một mẫu giao diện người dùng phổ biến, nhưng thường cần thay đổi giao diện khi "bị cố định" để duy trì khả năng đọc hoặc tiết kiệm không gian. Các giải pháp truyền thống yêu cầu trình nghe cuộn JavaScript, có thể gây ra vấn đề về hiệu suất. CSS hiện đại cho phép bạn xử lý vấn đề này một cách tự nhiên bằng Truy vấn trạng thái cuộn.
Các bước triển khai
1. Tạo vùng chứa cố định
Bạn cần có một vùng chứa đóng vai trò là phần tử cố định và vùng chứa cho truy vấn trạng thái cuộn.
<!-- Wrap in a section to define the containment area for the sticky element -->
<div class="section">
<div class="sticky-container">
<div class="sticky-header">
Section Header
</div>
</div>
<div class="content">
<!-- Content goes here -->
</div>
</div>
2. Áp dụng CSS cho hành vi cố định và loại vùng chứa
Đặt container-type: scroll-state trên phần tử position: sticky, chứ không phải trên phần tử cấp trên có thể cuộn. Luôn chỉ định container-name, vì các truy vấn này có thể được lồng vào nhau và các vùng chứa không có tên sẽ xung đột. Bạn cũng có thể kết hợp thuộc tính này với các truy vấn kích thước, ví dụ: container-type: scroll-state inline-size.
.sticky-container {
position: sticky;
top: 0;
/* MANDATORY: Always specify a container name to avoid collisions if CQs are nested */
container-type: scroll-state;
container-name: section-header;
z-index: 10;
}
.sticky-header {
/* Base styles for the header */
background: #f0f0f0;
padding: 20px;
transition: background-color 0.3s, box-shadow 0.3s;
}
Quan trọng: Trạng thái cuộn được truy vấn bởi phần tử con của vùng chứa trạng thái cuộn. Các kiểu trong truy vấn @container sẽ không áp dụng cho chính .sticky-container mà áp dụng cho các phần tử con của nó (như .sticky-header). Bạn không thể tạo kiểu cho chính phần tử vùng chứa bằng truy vấn trạng thái cuộn của chính phần tử đó.
3. Nhắm đến trạng thái bị kẹt
Sử dụng truy vấn @container scroll-state(...) để áp dụng kiểu khi tiêu đề bị kẹt.
/* Apply styles when the named container is stuck at the top */
@container section-header scroll-state(stuck: top) {
.sticky-header {
background: #0056b3;
color: white;
box-shadow: 0 4px 6px rgb(0 0 0 / 0.15);
}
}
Mẹo: Bạn cũng có thể sử dụng các thuộc tính logic như stuck: inset-block-start hoặc stuck: inset-inline-start để hỗ trợ tốt hơn cho việc quốc tế hoá (i18n) và bố cục từ phải sang trái (RTL) bằng cách truy vấn các cạnh tương đối của luồng thay vì các cạnh vật lý.
Lưu ý về việc thay đổi phương diện
Nếu không tắt tính năng neo cuộn, việc thay đổi các thuộc tính ảnh hưởng đến bố cục (chiều cao, khoảng đệm, cỡ chữ) khi bị kẹt có thể gây ra hiện tượng nhấp nháy hình ảnh. Tính năng neo cuộn trên nội dung trong luồng bên dưới phần tử cố định sẽ điều chỉnh độ lệch cuộn để bù đắp cho thay đổi về bố cục, từ đó đẩy phần tử trở lại vị trí bị kẹt và kích hoạt dao động.
Tắt tính năng neo cuộn trên phần tử mẹ của phần tử cố định để tránh điều này:
.section {
overflow-anchor: none;
}
Áp dụng overflow-anchor: none cho phần tử mẹ trực tiếp của phần tử position: sticky, chứ không phải cho :root hoặc body, vì việc này sẽ tắt tính năng neo cuộn cho toàn bộ trang. Khi đã áp dụng, bạn có thể thoải mái thay đổi bất kỳ thuộc tính nào ở trạng thái cố định, bao gồm cả thuộc tính mô hình hộp và phép biến đổi.
KHÔNG NÊN: Dựa vào overflow-anchor: none cho các phần tử dính vào phần dưới cùng. Tính năng neo cuộn chỉ bù đắp cho các thay đổi về bố cục phía trên vị trí cuộn hiện tại, vì vậy, tính năng này không ảnh hưởng đến các phần tử bị kẹt ở dưới cùng. Tránh thay đổi các thuộc tính mô hình hộp ở trạng thái cố định cho các tiêu đề cố định ở dưới cùng.
Chiến lược dự phòng
Truy vấn trạng thái cuộn là một tính năng cải tiến tăng dần. Trong các trình duyệt không hỗ trợ container-type: scroll-state, tiêu đề vẫn sẽ dính vào đầu trang (do position: sticky), nhưng sẽ không biến đổi về mặt hình ảnh. Đối với hầu hết các trường hợp sử dụng, đây là phương pháp nên dùng.
Nếu độ dính không cần thiết, nhưng position: sticky không có kiểu khác sẽ là trải nghiệm tệ hơn so với việc không có độ dính, bạn cũng có thể gói position: sticky và nội dung khai báo liên quan trong một truy vấn @supports:
.sticky-container {
@supports (container-type: scroll-state) {
position: sticky;
top: 0;
container-type: scroll-state;
container-name: section-header;
z-index: 10;
}
}
Mẹo: Nếu kiểu "bị kẹt" của bạn yêu cầu một màu nền khác để dễ đọc, hãy cân nhắc thiết lập bố cục sao cho kiểu mặc định hoạt động ở mọi nơi (Cải tiến tăng dần). Nếu phải sử dụng phương án dự phòng, bạn có thể kiểm soát hành vi của position: sticky trong một truy vấn @supports (container-type: scroll-state).
Nếu việc chuyển đổi hình ảnh là cực kỳ quan trọng đối với thiết kế (ví dụ: trạng thái cố định tạo ra nền hoặc độ nén mà nếu không có thì nội dung sẽ không đọc được), bạn có thể triển khai một phương án dự phòng JavaScript mạnh mẽ bằng cách sử dụng IntersectionObserver. Bạn phải sao chép kiểu CSS trong một lớp .is-stuck. Polyfill chung sau đây kiểm tra getComputedStyle để duyệt lên và tìm vùng chứa cuộn chính xác (mặc định là khung nhìn), khớp với hành vi của @container scroll-state(stuck: top):
function getScrollParent(node) {
if (node == null || node === document.body || node === document.documentElement) {
return null; // default to viewport
}
if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth) {
const overflow = getComputedStyle(node).overflow;
if (overflow !== 'visible' && overflow !== 'clip') {
return node;
}
}
return getScrollParent(node.parentNode);
}
document.querySelectorAll('.sticky-container').forEach(container => {
const root = getScrollParent(container);
const topOffset = parseFloat(getComputedStyle(container).top) || 0;
const observer = new IntersectionObserver(
([e]) => {
// Toggle the fallback class on the sticky header container
e.target.classList.toggle('is-stuck', e.intersectionRatio < 1);
},
{
root: root,
threshold: [1],
rootMargin: `-${topOffset + 1}px 0px 0px 0px`
}
);
observer.observe(container);
});
Lưu ý: Bạn cũng có thể sử dụng mẫu IntersectionObserver chung này làm polyfill cho truy vấn scroll-state(scrollable).