Preview Updated 2026-05-10

Drawer

Edge-anchored sliding panel — 4 placements, tone variants, resizable, async confirm, no-mask mode, imperative service, stacked z-index.

Basic usage

v-model:open (Vue) / open + onOpenChange (React) two-way binding. Drawer shares Modal’s portal / focus trap / body scroll lock / ESC-close infrastructure.

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

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

<template>
  <CfButton @click="open = true">打开抽屉</CfButton>
  <CfDrawer 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>
  </CfDrawer>
</template>

Four placements

placement controls the slide-in edge — right (default, the most common detail panel) / left (mobile nav) / top / bottom (playground-style ad-hoc workspace).

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

const right = ref(false);
const left = ref(false);
const top = ref(false);
const bottom = ref(false);
</script>

<template>
  <div style="display: flex; flex-wrap: wrap; gap: 8px;">
    <CfButton @click="right = true">从右侧</CfButton>
    <CfButton variant="secondary" @click="left = true">从左侧</CfButton>
    <CfButton variant="secondary" @click="top = true">从顶部</CfButton>
    <CfButton variant="secondary" @click="bottom = true">从底部</CfButton>
  </div>

  <CfDrawer v-model:open="right" title="右侧抽屉" placement="right" />
  <CfDrawer v-model:open="left" title="左侧抽屉" placement="left" />
  <CfDrawer v-model:open="top" title="顶部抽屉" placement="top" />
  <CfDrawer v-model:open="bottom" title="底部抽屉" placement="bottom" />
</template>

Five sizes

size controls width (left/right placement) or height (top/bottom). Available presets: sm / md (default) / lg / xl / full. width / height accept custom pixel values.

背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfButton, CfDrawer } 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 style="display: flex; flex-wrap: wrap; gap: 8px;">
    <CfButton @click="sm = true">sm</CfButton>
    <CfButton @click="md = true">md</CfButton>
    <CfButton @click="lg = true">lg</CfButton>
    <CfButton @click="xl = true">xl</CfButton>
    <CfButton @click="full = true">full</CfButton>
  </div>

  <CfDrawer v-model:open="sm" title="size = sm" size="sm" />
  <CfDrawer v-model:open="md" title="size = md" size="md" />
  <CfDrawer v-model:open="lg" title="size = lg" size="lg" />
  <CfDrawer v-model:open="xl" title="size = xl" size="xl" />
  <CfDrawer v-model:open="full" title="size = full" size="full" />
</template>

Tone variants

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

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

const info = ref(false);
const success = ref(false);
const warning = ref(false);
const error = ref(false);
</script>

<template>
  <div style="display: flex; flex-wrap: wrap; gap: 8px;">
    <CfButton variant="tertiary" @click="info = true">info</CfButton>
    <CfButton variant="tertiary" @click="success = true">success</CfButton>
    <CfButton variant="tertiary" @click="warning = true">warning</CfButton>
    <CfButton variant="tertiary" @click="error = true">error</CfButton>
  </div>

  <CfDrawer
    v-model:open="info"
    tone="info"
    title="新版本上线"
    description="0.2.0 — 拖拽面板、tone 变体、命令式 service。"
    ok-text="知道了"
  >
    <p style="margin: 0;">使用 tone 在 header 上自动加圆形语义图标。</p>
  </CfDrawer>

  <CfDrawer
    v-model:open="success"
    tone="success"
    title="部署成功"
    description="cd-12 已发布到生产环境"
    ok-text="完成"
  >
    <p style="margin: 0;">tone="success" 适合状态提示型抽屉。</p>
  </CfDrawer>

  <CfDrawer
    v-model:open="warning"
    tone="warning"
    title="存在未保存的修改"
    description="离开前请先保存草稿"
    ok-text="保存并离开"
    cancel-text="取消"
  >
    <p style="margin: 0;">两侧按钮 + warning 头像。</p>
  </CfDrawer>

  <CfDrawer
    v-model:open="error"
    tone="error"
    title="即将永久删除项目"
    description="该操作不可撤销。"
    ok-text="删除"
    cancel-text="取消"
  >
    <p style="margin: 0;">tone="error" 时默认 OK 按钮自动切到 danger 红色。</p>
  </CfDrawer>
</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, CfDrawer, CfInput, toast } from '@chufix-design/vue';

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

function onBeforeOk(): Promise<boolean | void> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (name.value.trim().length < 2) {
        toast({ type: 'error', message: '名称至少 2 个字符' });
        reject(new Error('invalid'));
        return;
      }
      toast({ type: 'success', message: `已保存:${name.value}` });
      resolve();
    }, 900);
  });
}
</script>

<template>
  <CfButton @click="open = true">编辑配置</CfButton>
  <CfDrawer
    v-model:open="open"
    placement="right"
    size="md"
    title="编辑配置"
    description="保存按钮接异步 onBeforeOk —— 期间所有关闭路径屏蔽。"
    ok-text="保存"
    cancel-text="取消"
    :on-before-ok="onBeforeOk"
    @ok="open = false; name = ''"
  >
    <div style="display: grid; gap: 8px;">
      <label style="font-size: 12px; color: var(--fg-3);">名称</label>
      <CfInput v-model="name" placeholder="输入≥2字符" />
    </div>
  </CfDrawer>
</template>

Edge-grip resize

resizable turns the inner edge into a drag handle — users can resize horizontally (left/right drawers) or vertically (top/bottom drawers). Min size 240×160.

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

const right = ref(false);
const bottom = ref(false);
</script>

<template>
  <div style="display: flex; gap: 8px;">
    <CfButton @click="right = true">右侧可缩放</CfButton>
    <CfButton variant="tertiary" @click="bottom = true">底部可缩放</CfButton>
  </div>

  <CfDrawer
    v-model:open="right"
    placement="right"
    size="md"
    resizable
    title="右侧可缩放"
    description="抓住面板的左边缘横向拖拽(最小 240px)。"
  >
    <p style="margin: 0;">这种交互对“调试 / 详情”类抽屉非常实用 —— 用户可以根据自己屏幕宽度自由调整。</p>
  </CfDrawer>

  <CfDrawer
    v-model:open="bottom"
    placement="bottom"
    size="md"
    resizable
    title="底部可缩放"
    description="抓住面板的上边缘竖向拖拽(最小 160px)。"
  >
    <p style="margin: 0;">底部抽屉常用于日志、控制台、playground 等可调高度的工具区。</p>
  </CfDrawer>
</template>

No-mask mode

Default mask=true renders an overlay and locks body scroll. Setting mask={false} floats the drawer above the page without overlay or scroll lock — ideal for persistent reference panels or live logs.

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

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

<template>
  <CfButton @click="open = true">打开(不锁背景)</CfButton>
  <p style="margin: 8px 0 0; color: var(--fg-3); font-size: 12px;">
    抽屉打开后页面其他地方仍可继续滚动 / 点击 —— 适合“常驻参考面板”型场景。
  </p>
  <CfDrawer
    v-model:open="open"
    placement="right"
    size="sm"
    :mask="false"
    title="参考面板"
    description="mask=false 时不渲染遮罩、不锁滚动。"
  >
    <p style="margin: 0;">尝试在抽屉打开的状态下滚动或点击页面其他位置 —— 都能正常交互。</p>
  </CfDrawer>
</template>

Lock close paths

For long forms or in-progress flows you may want to prevent accidental close. Set all three flags to false so users must explicitly submit or cancel via the footer buttons:

  • closeOnOverlay={false} — overlay click does not close
  • closeOnEsc={false} — ESC does not close
  • showClose={false} — hide the top-right ×

While async (onBeforeOk in flight) every close path is blocked automatically.

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

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

<template>
  <CfButton @click="open = true">打开(仅按钮可关)</CfButton>
  <CfDrawer
    v-model:open="open"
    title="表单填写中"
    :close-on-overlay="false"
    :close-on-esc="false"
    :show-close="false"
  >
    <p style="margin: 0;">遮罩 / Esc / 右上角 × 全部禁用 — 用户必须通过底部按钮显式提交或取消。</p>
    <template #footer>
      <CfButton variant="ghost" @click="open = false">取消</CfButton>
      <CfButton @click="open = false">提交</CfButton>
    </template>
  </CfDrawer>
</template>

Imperative service

drawer.open / confirm / danger opens a drawer and returns Promise<boolean> — no state binding required.

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

const ok = await drawer.confirm({
  title: 'Settings',
  description: 'Quickly tune service parameters',
  placement: 'right',
  content: SettingsForm,
  onOk: async () => {
    await api.save(); // throw or return false to block close
  },
});
背景
<script setup lang="ts">
import { CfButton, drawer, toast } from '@chufix-design/vue';

async function openSettings() {
  const ok = await drawer.confirm({
    title: '设置',
    description: '快速调整服务参数 —— 不需要关心 v-model。',
    placement: 'right',
    size: 'md',
    content: '这里通常会嵌入一个 Form 子组件。点击保存以应用更改。',
    onOk: async () => {
      await new Promise((r) => setTimeout(r, 600));
    },
  });
  if (ok) toast({ type: 'success', message: '设置已保存' });
}

async function deleteFlow() {
  const ok = await drawer.danger({
    title: '永久删除该工作区?',
    description: '所有数据将被清除,且无法恢复。',
    placement: 'right',
    okText: '我已了解,删除',
    cancelText: '保留',
    content: '建议先导出工作区作为备份。',
  });
  toast({
    type: ok ? 'error' : 'info',
    message: ok ? '工作区已删除' : '已取消删除',
  });
}
</script>

<template>
  <div style="display: flex; flex-wrap: wrap; gap: 8px;">
    <CfButton @click="openSettings">drawer.confirm()</CfButton>
    <CfButton variant="danger" @click="deleteFlow">drawer.danger()</CfButton>
  </div>
</template>

API

PropTypeDefaultDescription
openbooleanfalseControlled state
titlestringHeader title text
descriptionstringSubtitle below the title
placement'left' | 'right' | 'top' | 'bottom''right'Slide-in edge
size'sm' | 'md' | 'lg' | 'xl' | 'full''md'Horizontal → width; vertical → height
tone'default' | 'info' | 'success' | 'warning' | 'error''default'Visual tone + auto icon
widthnumber | stringCustom width (left/right)
heightnumber | stringCustom height (top/bottom)
resizablebooleanfalseInner-edge drag-resize
maskbooleantrueRender overlay and lock body scroll
closeOnOverlaybooleantrueClose on overlay click
closeOnEscbooleantrueClose on ESC
showClosebooleantrueTop-right × button
footerAlign'start' | 'center' | 'end' | 'space-between''end'Footer alignment
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.

Service methods

MethodtoneDefault buttons
drawer.open(opts)from optsfrom opts
drawer.confirm(opts)warningOK + Cancel
drawer.danger(opts)errorred OK + Cancel

反馈与讨论

Drawer · Discussion

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