Preview Updated 2026-05-10

Modal

Dialog with built-in portal, focus trap, scroll lock, tone variants, async confirm, draggable, resizable, imperative service, stacked z-index.

Basic usage

v-model:open (Vue) or open + onOpenChange (React) for two-way binding. The component handles portal-to-body, focus trap, body scroll lock, ESC close, overlay click, and fade + scale transitions internally.

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const open = ref(false);
</script>

<template>
  <CfButton @click="open = true">打开 Modal</CfButton>
  <CfModal v-model:open="open" title="确认操作">
    <p style="margin: 0;">这是一个最简弹窗。点击遮罩 / Esc / × 都能关闭。</p>
    <template #footer>
      <CfButton variant="ghost" @click="open = false">取消</CfButton>
      <CfButton @click="open = false">确定</CfButton>
    </template>
  </CfModal>
</template>

Sizes

size controls the max width. full fills nearly the entire viewport. width / min-height accept custom pixel values.

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const sm = ref(false);
const md = ref(false);
const lg = ref(false);
const xl = ref(false);
const full = ref(false);
</script>

<template>
  <div class="demo-row">
    <CfButton variant="tertiary" @click="sm = true">sm · 360px</CfButton>
    <CfButton variant="tertiary" @click="md = true">md · 480px</CfButton>
    <CfButton variant="tertiary" @click="lg = true">lg · 640px</CfButton>
    <CfButton variant="tertiary" @click="xl = true">xl · 800px</CfButton>
    <CfButton variant="tertiary" @click="full = true">full · 几乎铺满</CfButton>
  </div>

  <CfModal v-model:open="sm" size="sm" title="size = sm">
    <p style="margin: 0;">最大宽度 360px,适合精简的确认弹窗。</p>
  </CfModal>
  <CfModal v-model:open="md" size="md" title="size = md(默认)">
    <p style="margin: 0;">最大宽度 480px,是大多数表单的合理尺寸。</p>
  </CfModal>
  <CfModal v-model:open="lg" size="lg" title="size = lg">
    <p style="margin: 0;">最大宽度 640px,适合内容较多的弹窗。</p>
  </CfModal>
  <CfModal v-model:open="xl" size="xl" title="size = xl">
    <p style="margin: 0;">最大宽度 800px,适合复杂表单或预览面板。</p>
  </CfModal>
  <CfModal v-model:open="full" size="full" title="size = full">
    <p style="margin: 0;">几乎铺满视口(保留 1.5rem 外边距),适合设置面板或全屏预览。</p>
  </CfModal>
</template>

Close behavior

By default any close path works: overlay click, ESC, top-right ×. Disable individually with closeOnOverlay / closeOnEsc / showClose. While async (onBeforeOk in flight) every close path is blocked automatically.

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const a = ref(false);
const b = ref(false);
</script>

<template>
  <div class="demo-row">
    <CfButton variant="tertiary" @click="a = true">禁用遮罩关闭</CfButton>
    <CfButton variant="tertiary" @click="b = true">隐藏 × 按钮</CfButton>
  </div>

  <CfModal v-model:open="a" title="只能从底部关闭" :close-on-overlay="false">
    <p style="margin: 0;">点遮罩不会关闭,只能用 Esc 或下面的按钮。</p>
    <template #footer>
      <CfButton @click="a = false">我知道了</CfButton>
    </template>
  </CfModal>

  <CfModal v-model:open="b" title="无右上角 ×" :show-close="false">
    <p style="margin: 0;">这个 Modal 隐藏了右上角的关闭按钮,必须靠 footer 操作或 Esc 关闭。</p>
    <template #footer>
      <CfButton @click="b = false">关闭</CfButton>
    </template>
  </CfModal>
</template>

Tone variants

tone="info | success | warning | error" adds a circular tone icon plus accent color in the header. tone="error" automatically switches the default OK button to danger red.

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const open = ref<'info' | 'success' | 'warning' | 'error' | null>(null);
function set(t: typeof open.value) { open.value = t; }
</script>

<template>
  <div style="display: flex; gap: 8px; flex-wrap: wrap;">
    <CfButton variant="tertiary" @click="set('info')">Info</CfButton>
    <CfButton variant="tertiary" @click="set('success')">Success</CfButton>
    <CfButton variant="tertiary" @click="set('warning')">Warning</CfButton>
    <CfButton variant="danger" @click="set('error')">Error</CfButton>
  </div>

  <CfModal
    :open="open === 'info'"
    @update:open="(v) => set(v ? 'info' : null)"
    tone="info"
    title="新功能上线"
    description="表格组件现已支持双向虚拟化与 ResizeObserver 自动测高。"
    ok-text="知道了"
  />
  <CfModal
    :open="open === 'success'"
    @update:open="(v) => set(v ? 'success' : null)"
    tone="success"
    title="部署成功"
    description="v0.1.5 已发布到生产环境。"
    ok-text="完成"
  />
  <CfModal
    :open="open === 'warning'"
    @update:open="(v) => set(v ? 'warning' : null)"
    tone="warning"
    title="离开当前页面?"
    description="未保存的更改会丢失。"
    ok-text="离开"
    cancel-text="留下"
  />
  <CfModal
    :open="open === 'error'"
    @update:open="(v) => set(v ? 'error' : null)"
    tone="error"
    title="删除工作区"
    description="工作区内的所有数据将被立即清除,且无法恢复。"
    ok-text="确认删除"
    cancel-text="取消"
  />
</template>

Built-in OK / Cancel + async confirm

Pass okText / cancelText and the component renders the default footer buttons. onBeforeOk accepts an async function — return false or throw to keep the dialog open and restore the loading state; resolve normally to close and emit @ok.

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal, CfInput } from '@chufix-design/vue';

const open = ref(false);
const phrase = ref('');

async function onBeforeOk() {
  // 模拟服务端校验:必须输入 'delete'
  await new Promise((r) => setTimeout(r, 800));
  if (phrase.value !== 'delete') {
    // 阻止关闭
    return false;
  }
  // 真删
  await new Promise((r) => setTimeout(r, 600));
}
</script>

<template>
  <CfButton variant="danger" @click="open = true">删除项目(异步)</CfButton>

  <CfModal
    v-model:open="open"
    tone="error"
    title="确认删除"
    description="这是不可撤销的操作。请输入 'delete' 确认。"
    :on-before-ok="onBeforeOk"
    ok-text="确认删除"
    cancel-text="取消"
  >
    <CfInput v-model="phrase" placeholder="输入 delete 启用确认按钮" />
  </CfModal>
</template>

Draggable + resizable

draggable turns the title bar into a drag handle. resizable adds a corner handle for diagonal resizing. Both are PointerEvent-based with zero third-party deps.

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const open = ref(false);
</script>

<template>
  <CfButton @click="open = true">打开可拖拽 + 可缩放</CfButton>
  <CfModal
    v-model:open="open"
    title="可移动 / 可缩放"
    description="拖标题栏移动;右下角拖把柄缩放最小 280×160。"
    draggable
    resizable
    :centered="false"
    ok-text="完成"
    cancel-text="取消"
  >
    <p style="line-height: 1.6;">
      在桌面应用风格的工具型 modal 里很常用:用户希望并排看背景内容时移动 modal,或拉大到屏幕大小看长内容。
    </p>
  </CfModal>
</template>

Imperative service

modal.confirm / danger / alert / info / success / warning / error opens a dialog and returns Promise<boolean> — no state binding, no template nesting. Best for “confirm-then-act” auxiliary flows.

import { modal } from '@chufix-design/vue'; // React: import from '@chufix-design/react'

const ok = await modal.confirm({
  title: 'Submit order?',
  description: 'This action cannot be undone.',
  onOk: async () => {
    const r = await api.submit(); // throw or return false to block close
    if (!r.ok) return false;
  },
});
if (ok) modal.success({ title: 'Submitted' });
背景
<script setup lang="ts">
import { CfButton, modal } from '@chufix-design/vue';

async function onConfirm() {
  const ok = await modal.confirm({
    title: '提交订单?',
    description: '提交后无法撤销。',
    onOk: async () => {
      await new Promise((r) => setTimeout(r, 800));
      // 返回 false 阻止关闭
    },
  });
  if (ok) modal.success({ title: '订单已提交', description: '我们已经发邮件给你确认。' });
}

async function onDanger() {
  const ok = await modal.danger({
    title: '清空回收站?',
    description: '所有 18 个文件将永久删除。',
  });
  if (ok) modal.info({ title: '已清空' });
}

function onAlert() {
  modal.warning({
    title: '余额不足',
    description: '当前 ¥12.00,本次消费 ¥38.00。请先充值。',
  });
}
</script>

<template>
  <div style="display: flex; gap: 8px; flex-wrap: wrap;">
    <CfButton variant="primary" @click="onConfirm">异步 confirm</CfButton>
    <CfButton variant="danger" @click="onDanger">danger</CfButton>
    <CfButton variant="tertiary" @click="onAlert">warning alert</CfButton>
  </div>
</template>

Service method signatures:

MethodtoneDefault buttons
modal.open(opts)from optsfrom opts
modal.confirm(opts)warningOK + Cancel
modal.danger(opts)errorred OK + Cancel
modal.alert(opts)infoOK
modal.info / success / warning / error(opts)matching toneOK

Stacked modals

Each open Modal is pushed onto an internal stack with z-index auto-incremented by 10. ESC only closes the topmost one, and closing pops it from the stack — so you can open a Modal from inside another Modal without thinking about layering.

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfModal } from '@chufix-design/vue';

const layer1 = ref(false);
const layer2 = ref(false);
</script>

<template>
  <CfButton @click="layer1 = true">打开第一层</CfButton>

  <CfModal
    v-model:open="layer1"
    title="第一层"
    description="可以在这一层再打开嵌套的对话框。"
    ok-text="确定"
    cancel-text="取消"
  >
    <p style="line-height: 1.6; margin-bottom: 12px;">两层 modal 同时存在时,组件维护内部 z-index 栈,每层自动 +10。Esc 只关闭最顶层。</p>
    <CfButton variant="tertiary" @click="layer2 = true">打开第二层</CfButton>

    <CfModal
      v-model:open="layer2"
      tone="warning"
      title="第二层"
      description="这一层是从第一层里弹出来的;遮罩仍然位于第一层之上。"
      ok-text="知道了"
    />
  </CfModal>
</template>

API

PropTypeDefaultDescription
openbooleanfalseControlled state
titlestringHeader title text
descriptionstringSubtitle below the title
size'sm' | 'md' | 'lg' | 'xl' | 'full''md'Max-width preset
tone'default' | 'info' | 'success' | 'warning' | 'error''default'Visual tone + auto icon
centeredbooleantrueVertically center vs. 80px from top
widthnumber | stringCustom width (overrides size)
minHeightnumber | stringCustom min height
closeOnOverlaybooleantrueClose on overlay click
closeOnEscbooleantrueClose on ESC
showClosebooleantrueTop-right × button
footerAlign'start' | 'center' | 'end' | 'space-between''end'Footer alignment
draggablebooleanfalseDrag header to move
resizablebooleanfalseBottom-right resize handle
okText / cancelTextstringDefault button labels; omit to skip default footer
okVariant'primary' | 'danger' | 'secondary''primary'OK button variant
onBeforeOk() => boolean | void | Promise<...>Async hook; false / throw blocks close
tostring | Element'body'Teleport / Portal target
zIndexnumberauto-stackCustom z-index base

Slots / events

  • Vue: named slots header / default / footer. The footer slot receives { ok, cancel, loading } for custom button rendering.
  • React: header, footer (accepts ReactNode or ({ ok, cancel, loading }) => ReactNode), plus children.
  • Events: update:open / close / ok / cancel.

反馈与讨论

Modal · Discussion

0
0 / 600
一键发送
正在加载评论...