模态对话框

对话框在逐渐变暗的背景上浮起放大,离开时原路返回,全部基于原生 dialog 元素。

  • CSS
  • 布局
  • 切换
  • 收录于

要删除这个项目吗?

项目中的动效和设置都会被移除,此操作无法撤销。

AI 提示词

用原生 <dialog> 元素制作带 CSS 进出场过渡的模态框。用 showModal() 打开,自动获得焦点锁定、Escape 关闭和顶层显示。关闭状态设为 opacity 0、translateY(12px)、scale(0.96),[open] 状态为 opacity 1、无 transform。opacity、transform、overlay 和 display 过渡 260ms,缓动 cubic-bezier(0.16, 1, 0.3, 1),并设置 transition-behavior: allow-discrete,让退场也能动画。用 @starting-style 给 [open] 状态设置起始值,让进场有动画。::backdrop 同样处理,淡入深色遮罩并加一点背景模糊。点击“取消”、“删除”或点击遮罩时关闭。在 prefers-reduced-motion 下保留淡入淡出,去掉位移。对话框标题为“要删除这个项目吗?”。

源代码

Demo.tsx
'use client'

import { useId, useRef } from 'react'
import s from './demo.module.css'

export default function ModalDialog({ labels }: { labels: Record<string, string> }) {
  const ref = useRef<HTMLDialogElement>(null)
  const titleId = useId()
  const close = () => ref.current?.close()

  return (
    <>
      <button type="button" className={s.open} onClick={() => ref.current?.showModal()}>
        {labels.open}
      </button>
      <dialog
        ref={ref}
        className={s.dialog}
        aria-labelledby={titleId}
        onClick={(e) => e.target === e.currentTarget && close()}
      >
        <div className={s.body}>
          <h2 id={titleId} className={s.title}>
            {labels.title}
          </h2>
          <p className={s.text}>{labels.body}</p>
          <div className={s.actions}>
            <button type="button" className={s.ghost} onClick={close} autoFocus>
              {labels.cancel}
            </button>
            <button type="button" className={s.danger} onClick={close}>
              {labels.confirm}
            </button>
          </div>
        </div>
      </dialog>
    </>
  )
}
demo.module.css
.open {
  height: 44px;
  padding: 0 20px;
  border: 1px solid var(--stage-rule);
  border-radius: var(--radius-sm);
  background: var(--stage-surface);
  color: var(--stage-ink);
  font: 500 var(--text-sm) / 1 var(--font-body);
  cursor: pointer;
}

.open:hover { border-color: var(--stage-accent); }
.open:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

.dialog {
  width: min(360px, calc(100vw - 32px));
  padding: 0;
  border: 1px solid var(--stage-rule);
  border-radius: var(--radius-md);
  background: var(--stage-surface);
  color: var(--stage-ink);
  box-shadow: 0 24px 60px oklch(0% 0 0 / 0.45);
  opacity: 0;
  transform: translateY(12px) scale(0.96);
  transition:
    opacity 260ms var(--ease-out),
    transform 260ms var(--ease-out),
    overlay 260ms allow-discrete,
    display 260ms allow-discrete;
}

.dialog[open] {
  opacity: 1;
  transform: none;
}

@starting-style {
  .dialog[open] {
    opacity: 0;
    transform: translateY(12px) scale(0.96);
  }
}

.dialog::backdrop {
  background: oklch(0% 0 0 / 0);
  backdrop-filter: blur(0);
  transition:
    background-color 260ms var(--ease-out),
    backdrop-filter 260ms var(--ease-out),
    overlay 260ms allow-discrete,
    display 260ms allow-discrete;
}

.dialog[open]::backdrop {
  background: oklch(0% 0 0 / 0.55);
  backdrop-filter: blur(3px);
}

@starting-style {
  .dialog[open]::backdrop {
    background: oklch(0% 0 0 / 0);
    backdrop-filter: blur(0);
  }
}

.body {
  display: grid;
  gap: 10px;
  padding: 22px;
}

.title {
  margin: 0;
  font: 600 var(--text-lg) / 1.3 var(--font-display);
  color: var(--stage-ink);
}

.text {
  margin: 0;
  font: 400 var(--text-sm) / 1.5 var(--font-body);
  color: var(--stage-muted);
}

.actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 10px;
}

.ghost, .danger {
  height: 38px;
  padding: 0 16px;
  border-radius: var(--radius-sm);
  font: 500 var(--text-sm) / 1 var(--font-body);
  cursor: pointer;
}

.ghost { border: 1px solid var(--stage-rule); background: transparent; color: var(--stage-ink); }
.danger { border: 0; background: var(--stage-accent); color: var(--stage-accent-ink); }
.ghost:focus-visible, .danger:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .dialog, .dialog[open] { transform: none; }
  @starting-style {
    .dialog[open] { transform: none; }
  }
}

投稿创意

看到想收录的动效或主题?简单描述一下,会打开一个预先填好的 GitHub Issue,确认后即可提交。

你想投稿的是?

需要 GitHub 账号,投稿内容会公开。