- vừa được xem lúc

Accordion bằng HTML+CSS, JS ra chỗ khác chơi

0 0 7

Người đăng: Nguyễn Hải Bình

Theo Viblo Asia

Chào mọi người, mình là Bình, một software engineer tự xưng. Thông thường, mình sẽ chỉ đụng tay vào các dashboard phía frontend, đã phải 3 năm rồi mình chưa làm những home hay landing page. Đợt vừa rồi, không biết nhân duyên nào bắt mình phải làm nhiều dự án có home page, những trang này có đặc điểm là cần phải chạy tốt khi không có JS để tối ưu search engine. Nhiều thành phần thường thấy trong dashboard như dropdown menu, accordion, carousel phải chạy được ở home page mà không cần JS. Mình không thích phải dùng nhiều công nghệ trong cùng 1 product, vì vậy ngay bây giờ chúng ta sẽ làm 1 cái accordion trong NextJS + Tailwindcss, có thể chạy khi tắt JS (hoàn toàn có thể code lại bằng HTML/CSS, ở đây mình chỉ dùng thêm useId() của React).

Tại sao chúng ta thường phải dùng JS khi viết những component như accordion?

Đơn giản là "trạng thái" (state), sự đóng - mở của accordion chính là trạng thái, HTML và CSS không phải ngôn ngữ lập trình, nên chúng không thể lưu trạng thái, vì vậy thông thường sẽ rất khó khăn trong việc cài đặt những thứ có trạng thái chỉ với HTML và CSS.

Cái gì có thể thay thế JS trong việc lưu trữ trạng thái cho các component?

Trước khi JS được ứng dụng phổ biến, chúng ta vẫn phải nhập dữ liệu lên web, đó là một dạng trạng thái, và các thẻ input chính là nơi lưu trữ các trạng thái này:

  • <input type="checkbox">: Một thẻ input checkbox có thể lưu trữ trạng thái boolean (true hoặc false).
  • <input type="radio">: Một nhóm các thẻ input radio có thể lưu trữ trạng thái enum.
  • Và các thẻ input number, text lưu trữ trạng thái số và chữ.

Như vậy, để có thể mô phỏng trạng thái đóng mở của một accordion, ta chỉ cần 1 thẻ input checkbox.

Cấu tạo của một accordion

Một accordion có 2 thành phần: heading (summary) và body (detail). Heading sẽ chứa thông tin tóm tắt và body là phần diễn giải, chúng ta có thể click lên header để ẩn/hiện body.

image.png

Cài đặt accordion

Chúng ta không thể đặt 1 checkbox để người dùng tick cho accordion xổ ra được, vì vậy chúng ta sẽ sử dụng 1 thẻ label trỏ tới input này và ẩn input đi, trong label sẽ chứa heading, như vậy khi người dùng nhấn vào heading cũng tương đương việc nhấn vào input:

<label htmlFor={inputId} className="w-full block"> <span>{heading}</span> <input type="checkbox" id={inputId} name="switch" hidden />
</label>

Phần body của accordion sẽ được đặt trong một thẻ div liền kề với thẻ label với mặc định chiều cao bằng 0 và overflow hidden để ẩn đi:

<div className="w-full h-0 overflow-hidden"> {body}
</div>

Bọc tất cả vào trong 1 thẻ div để có thể cô lập heading và body thành 1 reusable component:

<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, ta dùng pseudo class :has:checked của CSS:

<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 body chứa 1 element có nameswitch và có giá trị checked là true thì apply class h-auto.

Tuy nhiên, nếu dùng height thì element 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) được, thay vì height ta sẽ dùng max-height:

<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ể animate được, max-height cần được xác định một giá trị cụ thể, vì vậy ta sẽ tiến hành thêm các bodyClsheadingCls để có thể tùy biến vào trong accordion linh hoạt hơn:

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> );
}

Và bây giờ gặp 1 vấn đề nữa, nếu như đặt điều kiện như hiện tại, khi component khác gọi tới Accordion sẽ phải đặt max-height kèm theo cả selector checkbox dài ngoằng kia rất bất tiện, vì vậy ta sẽ đảo điều kiện lại bằng pseudo class :not:

<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 gọi bởi component khác:

import { Accordion } from "@/components/Accordion"; export default function Home() { return ( <main> <Accordion rootCls="max-w-screen-sm" heading="Click to expand" body="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." bodyCls="transition-all duration-200 max-h-56" /> </main> );
}

Và đây là kết quả:

Và tất nhiên là SEO-safe và chạy hoàn toàn không cần JavaScript ở trình duyệt.

Lời kết

Chả biết viết gì để kết thúc cả, nếu bài viết này được quan tâm thì mình có thể làm thêm về dropdown menu và carousel nữa 😄.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Một số quy tắc và name class phổ biến cho Front-end Dev

Trong CSS, các thủ tục setting đóng vai trò to lớn, nhưng bên cạnh đó, cách đặt tên theo các từ đơn sao cho phù hợp cũng quan trọng không kém . Đặt tên rất khó, nhưng cũng rất quan trọng .

0 0 93

- vừa được xem lúc

2021, chúng ta cần tối ưu hóa việc tải hình ảnh trên web như nào?

Rất chào các bạn,. Như các bạn đã biết, trong kỉ nguyên công nghệ, song song với sự sinh ra dày đặc của các trang web mới cũng là sự biến mất của những trang web "lạc hậu" hay hoạt động kém hiệu quả.

0 0 57

- vừa được xem lúc

Tài nguyên nghiên cứu sâu Html

1. Articles and standards. . HTML 5.

0 0 198

- vừa được xem lúc

Tìm hiểu về Event.preventDefault(), Event.stopPropagation() và Event.stopImmediatePropagation()

Chúng ta thương thấy 3 method này và có thể dẫn dến bối rối và nhầm lẫn giữa chúng:. . Event.preventDefault().

0 0 52

- vừa được xem lúc

Vanilla JS Project: Tính tuổi

1. Yêu cầu.

0 0 74

- vừa được xem lúc

Thủ thuật nhỏ để căn chỉnh image với object-fit

Chào các bạn,. Có lẽ trong hành trình code của các bạn thì không ít lần gặp vấn đề méo ảnh do fix cứng cả width, height của ảnh nhỉ? Hoặc kể cả khi bạn set value cho 1 thuộc tính weigth hoặc height còn thuộc tính còn lại để auto thì nhiều lúc ảnh cũng không được hiển thị toàn vẹn cho lắm.

0 0 48