Chào các bạn, mình là Bình, một kỹ sư phần mềm với kinh nghiệm làm việc trong lĩnh vực Frontend. Trong thời gian qua, mình thường chỉ làm việc với các dashboard mà ít có cơ hội phát triển các trang web chính như Home hay Landing Page. Gần đây, mình lại có cơ hội làm việc với nhiều dự án yêu cầu thiết kế trang chính cần phải hoạt động hiệu quả mà không cần JavaScript, điều này tạo ra thách thức lớn khi các thành phần như dropdown menu, accordion và carousel đều yêu cầu JavaScript để hoạt động. Tuy nhiên, mình muốn tối ưu hóa việc sử dụng công nghệ, vì vậy trong bài viết này, mình sẽ hướng dẫn các bạn cách tạo một Accordion chỉ với HTML và CSS, tận dụng thêm một chút tính năng của React trong Next.js.
Tại sao Accordion thường cần JavaScript?
Accordion về cơ bản là một thành phần có trạng thái (state), nghĩa là nó yêu cầu khả năng lưu trạng thái mở và đóng. Mặc dù HTML và CSS là rất hữu ích, nhưng chúng không phải là ngôn ngữ lập trình, do đó chúng không thể lưu trạng thái. Thông thường, đây là lý do mà chúng ta phải dùng JavaScript để xử lý các thành phần này.
Giải pháp thay thế cho trạng thái mà không cần JavaScript
Trước khi JavaScript trở nên phổ biến, chúng ta vẫn có thể nhập dữ liệu lên trang web thông qua các thẻ input, mà thực tế chính là cách lưu trữ trạng thái. Ví dụ:
<input type="checkbox">
: Có thể lưu trữ trạng thái boolean (true hoặc false).<input type="radio">
: Cho phép lựa chọn trong một nhóm (enum).- Các thẻ input khác như number, text cũng giúp lưu trữ trạng thái số và chữ.
Như vậy, chúng ta hoàn toàn có thể tạo ra một accordion chỉ bằng một thẻ input checkbox.
Cấu tạo của Accordion
Accordion bao gồm hai phần: phần tiêu đề (heading) và phần nội dung (body). Tiêu đề sẽ chứa thông tin tóm tắt và nội dung là phần chi tiết mà người dùng có thể mở hoặc đóng.
Cách cài đặt Accordion
Để tạo Accordion, chúng ta sẽ sử dụng một thẻ label, trong đó trỏ tới input checkbox, ẩn input đi để người dùng không thấy. Dưới đây là cách làm:
html
<label htmlFor={inputId} className="w-full block">
<span>{heading}</span>
<input type="checkbox" id={inputId} name="switch" hidden />
</label>
Phần nội dung của accordion sẽ được đặt trong một thẻ div liền kề với thẻ label, với chiều cao mặc định là 0 và overflow ẩn để ẩn đi:
html
<div className="w-full h-0 overflow-hidden">
{body}
</div>
Toàn bộ mã HTML sẽ được bọc trong một thẻ div để dễ dàng tạo thành một component tái sử dụng:
html
<div>
<label htmlFor={inputId} className="w-full block">
<span>{heading}</span>
<input type="checkbox" id={inputId} name="switch" hidden />
</label>
<div className="w-full h-0 overflow-hidden">
{body}
</div>
</div>
Để bắt trạng thái của input, chúng ta sẽ sử dụng pseudo class :has
và :checked
của CSS:
html
<div>
<label htmlFor={inputId} className="w-full block">
<span>{heading}</span>
<input type="checkbox" id={inputId} name="switch" hidden />
</label>
<div className="w-full h-0 overflow-hidden [*:has([name=switch]:checked)>&]:h-auto">
{body}
</div>
</div>
Diễn giải: Nếu phần tử cha của nội dung chứa một element có name
là switch
và có trạng thái checked
, chúng ta sẽ áp dụng class h-auto
để hiển thị.
Tuy nhiên, khi sử dụng height
, mà không có chiều cao sẽ không thể animate từ trạng thái không có chiều cao (height: 0
) sang chiều cao tự động (height: auto
). Do đó, thay vì sử dụng height
, chúng ta sẽ dùng max-height
:
html
<div>
<label htmlFor={inputId} className="w-full block">
<span>{heading}</span>
<input type="checkbox" id={inputId} name="switch" hidden />
</label>
<div className="w-full max-h-0 overflow-hidden [*:has([name=switch]:checked)>&]:max-h-max">
{body}
</div>
</div>
Tuy nhiên, để có thể thực hiện animation, max-height
cần được chỉ định một giá trị cụ thể. Chúng ta có thể tùy chỉnh các class của body và heading để linh hoạt hơn:
javascript
import clsx from "clsx";
import { ReactNode, useId } from "react";
interface IProps {
rootCls?: string;
heading: string | ReactNode;
headingCls?: string;
body: string | ReactNode | ReactNode[];
bodyCls?: string;
}
export function Accordion({
rootCls,
heading,
headingCls,
body,
bodyCls,
}: IProps) {
const id = useId();
const inputId = `accordions.${id}`;
return (
<div className={rootCls}>
<label htmlFor={inputId} className={clsx("w-full block", headingCls)}>
<span>{heading}</span>
<input type="checkbox" id={inputId} name="switch" hidden />
</label>
<div
className={clsx(
"w-full max-h-0 overflow-hidden [*:has([name=switch]:checked)>&]:max-h-max",
bodyCls,
)}
>
{body}
</div>
</div>
);
}
Tuy nhiên, nếu điều kiện như hiện tại, khi component khác gọi đến Accordion sẽ cần phải xác định cả max-height
kèm theo selector dài. Vì vậy, ta sẽ đảo điều kiện bằng pseudo class :not
:
html
<div
className={clsx(
"w-full max-h-max overflow-hidden [*:not(:has([name=switch]:checked))>&]:max-h-0",
bodyCls,
)}
>
{body}
</div>
Cuối cùng, Accordion sẽ được sử dụng trong một component khác như sau:
javascript
import { Accordion } from "@/components/Accordion";
export default function Home() {
return (
<main>
<Accordion
rootCls="max-w-screen-sm"
heading="Nhấn để mở rộng"
body="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
bodyCls="transition-all duration-200 max-h-56"
/>
</main>
);
}
Cuối cùng, với việc thực hiện như trên, Accordion của chúng ta đã hoàn tất và có thể hoạt động mà không cần JavaScript, đồng thời rất tối ưu cho SEO.
Kết luận
Hy vọng bài viết này đã giúp các bạn hiểu rõ hơn về cách tạo Accordion chỉ với HTML và CSS. Nếu bài viết nhận được sự quan tâm, mình sẽ tiếp tục chia sẻ thêm những dự án như dropdown menu và carousel.
source: viblo