TL;DR

Bạn có thể xây dựng modal với hiệu ứng animation đẹp - fade, slide, scale, flip, blur và nhiều hơn nữa - mà không cần viết một dòng JavaScript nào. Bộ đôi @starting-style + transition-behavior: allow-discrete (CSS 2024) đã giải quyết vấn đề lịch sử là không thể animate display: none. Kết hợp với Popover API mới, đây là cách tiếp cận nhẹ nhất, nhanh nhất và ít bug nhất để làm modal trong 2025.

8 kiểu animation modal thuần CSS: Fade In, Slide Up, Scale, Swing, Flip, Blur, Unfold, Rotate

Bài toán cũ - animate modal bằng CSS là "không thể"

Nhiều năm qua, lý do chính khiến developer phải dùng JavaScript cho modal là một vấn đề kỹ thuật đơn giản nhưng cứng đầu: CSS không thể animate display: none. Thuộc tính displaydiscrete property - nó thay đổi đột ngột từ none sang block, không có trạng thái trung gian.

Giải pháp cũ thường là: dùng visibility: hidden + opacity: 0, rồi lắng nghe sự kiện transitionend bằng JavaScript để set display: none sau khi animation kết thúc. Code phình to, dễ bug, và phụ thuộc vào JS.

Vũ khí CSS mới năm 2024

Hai thuộc tính CSS mới thay đổi hoàn toàn cuộc chơi:

  • transition-behavior: allow-discrete - Cho phép discrete properties như displayoverlay tham gia vào CSS transition. Browser chạy hết các transition mượt (opacity, transform) rồi mới flip giá trị discrete. Đạt ~85% global usage, available từ 2024.
  • @starting-style - Baseline August 2024. Định nghĩa trạng thái khởi đầu cho element trước khi nó được render lần đầu (từ display: none). Không có @starting-style, animation entry sẽ không chạy vì browser không có "trạng thái trước" để transition từ đó.

Kết hợp hai thứ này, code từ 15 dòng JavaScript rút xuống còn 6 dòng CSS thuần túy.

8 kiểu animation modal bạn có thể làm ngay

Đây là 8 kiểu animation mà chỉ cần HTML + CSS:

  1. Fade In - Đơn giản nhất. Dùng opacity: 0 → 1 trong @starting-style.
  2. Slide Up - Modal trượt từ dưới lên. Kết hợp translate(0, 40px) → translate(0, 0) với opacity.
  3. Scale - Modal zoom in từ nhỏ ra lớn. Dùng scale(0.8) → scale(1) + Bezier curve cho cảm giác vật lý.
  4. Swing - Hiệu ứng lắc lư khi vào. Dùng rotate kết hợp transform-origin.
  5. Flip - Modal lật 3D. Dùng perspective + rotateX() hoặc rotateY().
  6. Blur - Modal xuất hiện từ blur về nét. Dùng backdrop-filter: blur() trên backdrop (available September 2024).
  7. Unfold - Hiệu ứng mở ra như tờ giấy. Kết hợp scaleY + clip-path.
  8. Rotate - Modal xoay vào. Dùng rotate(-15deg) → rotate(0deg) với opacity transition.

Tất cả đều dùng cùng pattern: định nghĩa open state, đặt @starting-style để set trạng thái ban đầu, và thêm transition-behavior: allow-discrete vào danh sách transition.

Popover API - toggle modal không cần JavaScript

Nếu bạn cần trigger modal bằng button, Popover API (Baseline January 2025) là lựa chọn hoàn toàn không JS:

<button popovertarget="my-modal">Mở modal</button>
<dialog id="my-modal" popover>
  Nội dung modal
</dialog>

Browser tự promote element lên "top layer" - lớp trên cùng của document, không bị ảnh hưởng bởi z-index của các element khác. Một CSS one-liner ngăn scroll background khi modal mở: body:has(dialog:popover-open) { overflow: hidden; }.

CSS-only vs JavaScript - khi nào dùng gì

Tiêu chíCSS-onlyJavaScript
Tốc độ loadNhanh hơn (không parse JS)Chậm hơn (block render)
ResilienceHoạt động khi JS lỗiPhụ thuộc JS
Focus trappingCần workaroundTự động qua .showModal()
Background inertCần thêm 1 dòng JSTự động qua .showModal()
Code size6 dòng15+ dòng

Kết luận thực tế: dùng CSS-only cho modal đơn giản (login prompt, alert, image gallery). Khi cần accessibility hoàn chỉnh hoặc multi-step form, vẫn nên thêm một ít JS để bật inert attribute trên background.

Hạn chế cần biết

  • Focus trapping: Popover API không trap focus như .showModal() - người dùng keyboard có thể tab ra ngoài modal.
  • :target method: Tạo browser history entry mỗi khi mở/đóng, người dùng phải Back nhiều lần.
  • @starting-style chưa global: Chưa available trên tất cả browser cũ - cần fallback bằng CSS keyframes nếu target IE/older.
  • popover="manual" phá vỡ Esc: Nếu dùng manual mode để ngăn đóng khi click backdrop, phím Esc cũng bị tắt.

Tìm hiểu thêm

Source code full: Comment "code" dưới tweet gốc của @davidm_ml. Tài liệu kỹ thuật chi tiết: web.dev - dialog & popover Baseline patterns, LogRocket - animating with @starting-style, modern-css.com - transition-behavior guide.