Skip to content
API 风格:

Drawer ​

A panel that slides in from a screen edge — better suited than a dialog to edge-anchored, momentary tasks such as details, filters, or wizard steps. It mirrors the Dialog contract: role="dialog" + aria-modal="true", focus trapped inside, and both Escape and mask clicks close it.

Basic usage ​

Right drawer
vue
<template>
  <LhButton @click="basic = true">Open drawer</LhButton>
  <LhDrawer v-model="basic" title="Order details">
    <p>Body content goes here.</p>
  </LhDrawer>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const basic = ref(false)
</script>
vue
<script>
export default {
  data() {
    return { basic: false }
  },
}
</script>

<template>
  <LhButton @click="basic = true">Open drawer</LhButton>
  <LhDrawer v-model="basic" title="Order details">
    <p>Body content goes here.</p>
  </LhDrawer>
</template>

Directions ​

right / left / top / bottom
vue
<script setup lang="ts">
import { ref } from 'vue'
const dirVisible = ref(false)
const direction = ref<'right' | 'left' | 'top' | 'bottom'>('right')
function openDir(d: 'right' | 'left' | 'top' | 'bottom') {
  direction.value = d
  dirVisible.value = true
}
</script>

<template>
  <!-- Open the right drawer -->
  <LhButton @click="openDir('right')">right</LhButton>
  <!-- Open the left drawer -->
  <LhButton @click="openDir('left')">left</LhButton>
  <!-- Open the top drawer -->
  <LhButton @click="openDir('top')">top</LhButton>
  <!-- Open the bottom drawer -->
  <LhButton @click="openDir('bottom')">bottom</LhButton>
  <!-- Slides out in the given direction -->
  <LhDrawer v-model="dirVisible" :title="`${direction} drawer`" :direction="direction" size="40%">
    <p>Direction: {{ direction }}</p>
  </LhDrawer>
</template>
vue
<script>
export default {
  data() {
    return { dirVisible: false, direction: 'right' }
  },
  methods: {
    openDir(d) {
      this.direction = d
      this.dirVisible = true
    },
  },
}
</script>

<template>
  <!-- Open the right drawer -->
  <LhButton @click="openDir('right')">right</LhButton>
  <!-- Open the left drawer -->
  <LhButton @click="openDir('left')">left</LhButton>
  <!-- Open the top drawer -->
  <LhButton @click="openDir('top')">top</LhButton>
  <!-- Open the bottom drawer -->
  <LhButton @click="openDir('bottom')">bottom</LhButton>
  <!-- Slides out in the given direction -->
  <LhDrawer v-model="dirVisible" :title="`${direction} drawer`" :direction="direction" size="40%">
    <p>Direction: {{ direction }}</p>
  </LhDrawer>
</template>
size and the footer slot
vue
<script setup lang="ts">
import { ref } from 'vue'

const sized = ref(false)
</script>

<template>
  <!-- Open a 480px drawer -->
  <LhButton @click="sized = true">480px drawer</LhButton>
  <!-- Fixed-size drawer with a footer slot -->
  <LhDrawer v-model="sized" title="Filters" :size="480">
    <LhInput placeholder="Filter by name" />
    <LhInput placeholder="Filter by tag" />
    <template #footer>
      <LhButton @click="sized = false">Cancel</LhButton>
      <LhButton type="primary" @click="sized = false">Apply</LhButton>
    </template>
  </LhDrawer>
</template>
vue
<script>
export default {
  data() {
    return { sized: false }
  },
}
</script>

<template>
  <!-- Open a 480px drawer -->
  <LhButton @click="sized = true">480px drawer</LhButton>
  <!-- Fixed-size drawer with a footer slot -->
  <LhDrawer v-model="sized" title="Filters" :size="480">
    <LhInput placeholder="Filter by name" />
    <LhInput placeholder="Filter by tag" />
    <template #footer>
      <LhButton @click="sized = false">Cancel</LhButton>
      <LhButton type="primary" @click="sized = false">Apply</LhButton>
    </template>
  </LhDrawer>
</template>

A numeric size is pixels; a string may be a percentage such as '40%'. Left/right apply to width, top/bottom to height.

Behavior switches ​

mask-closable disables closing via the mask, show-close hides the close button, close-on-esc disables the Esc key, lock-scroll keeps the page scrollable while open, and teleport-target selects the mount container.

Behavior switches
vue
<script setup lang="ts">
import { ref } from 'vue'

const adv1 = ref(false)
const adv2 = ref(false)
const adv3 = ref(false)
</script>

<template>
  <!-- Open the "Buttons only" drawer -->
  <LhButton @click="adv1 = true">Buttons only</LhButton>
  <!-- Open the "No scroll lock" drawer -->
  <LhButton @click="adv2 = true">No scroll lock</LhButton>
  <!-- Neither mask clicks nor Esc close it -->
  <LhDrawer
    v-model="adv1"
    title="Buttons only"
    :mask-closable="false"
    :show-close="false"
    :close-on-esc="false"
  >
    <p>Neither mask clicks nor Esc close this drawer.</p>
    <template #footer>
      <LhButton type="primary" @click="adv1 = false">Close</LhButton>
    </template>
  </LhDrawer>
  <!-- The page stays scrollable while open -->
  <LhDrawer v-model="adv2" title="No scroll lock" :lock-scroll="false">
    <p>The page stays scrollable while the drawer is open.</p>
  </LhDrawer>
  <!-- Teleported into a specific container -->
  <LhDrawer v-model="adv3" title="Mount target" teleport-target="#drawer-host">
    <p>The panel is teleported into the #drawer-host container.</p>
  </LhDrawer>
</template>
vue
<script>
export default {
  data() {
    return { adv1: false, adv2: false, adv3: false }
  },
}
</script>

<template>
  <!-- Open the "Buttons only" drawer -->
  <LhButton @click="adv1 = true">Buttons only</LhButton>
  <!-- Open the "No scroll lock" drawer -->
  <LhButton @click="adv2 = true">No scroll lock</LhButton>
  <!-- Neither mask clicks nor Esc close it -->
  <LhDrawer
    v-model="adv1"
    title="Buttons only"
    :mask-closable="false"
    :show-close="false"
    :close-on-esc="false"
  >
    <p>Neither mask clicks nor Esc close this drawer.</p>
    <template #footer>
      <LhButton type="primary" @click="adv1 = false">Close</LhButton>
    </template>
  </LhDrawer>
  <!-- The page stays scrollable while open -->
  <LhDrawer v-model="adv2" title="No scroll lock" :lock-scroll="false">
    <p>The page stays scrollable while the drawer is open.</p>
  </LhDrawer>
  <!-- Teleported into a specific container -->
  <LhDrawer v-model="adv3" title="Mount target" teleport-target="#drawer-host">
    <p>The panel is teleported into the #drawer-host container.</p>
  </LhDrawer>
</template>

Drawer API ​

Props ​

PropDescriptionTypeDefault
modelValueVisibility (v-model)boolean—
titleTitle (or the header slot)string—
directionDirection"right" | "left" | "top" | "bottom"'right'
sizeSize: a number is px, a string supports percentagesstring | number'30%'
maskClosableWhether clicking the mask closes itbooleantrue
showCloseWhether to show the close buttonbooleantrue
teleportTargetTeleport target (selector or element, defaults to body)string | HTMLElement'body'
lockScrollWhether to lock page scrollingbooleantrue
closeOnEscWhether to close on Escbooleantrue
themeVarsComponent-level theme variables, overriding ConfigProvider configuration{ [x: string]: string; }—
bgPatternComponent-level background pattern, overriding ConfigProvider configurationstring—
sxsx quick-layout props (applied to the panel)LhSxProps—

Events ​

EventDescriptionArgs
update:model-valueVisibility changed(value: boolean)
openAfter opening—
closeAfter closing—

Slots ​

SlotDescription
defaultBody content
headerCustom header (title area)
footerCustom footer actions

Methods ​

MethodDescription
openOpen programmatically
closeClose programmatically

Accessibility ​

  • role="dialog" + aria-modal="true"; the title is linked via aria-labelledby, falling back to the drawer.title locale key as an aria-label
  • Focus moves into the panel on open, Tab cycles inside, and focus returns to the trigger on close
  • Escape closes the drawer (disable with close-on-esc); the mask is decorative (aria-hidden)
  • The close button is a real <button> with an aria-label from the drawer.close locale key
  • The slide transition degrades to a plain fade under prefers-reduced-motion

Type Definitions ​

The TypeScript type structures exported by this component (auto-extracted from packages/components/drawer/src/types.ts and @lh-ui/share/core/* sources; click each entry to expand; see the Props table above for attribute descriptions):

LhDrawerProps
ts
export interface LhDrawerProps {
  modelValue?: boolean
  title?: string
  direction?: LhDrawerDirection
  size?: string | number
  maskClosable?: boolean
  showClose?: boolean
  teleportTarget?: string | HTMLElement
  lockScroll?: boolean
  closeOnEsc?: boolean
  themeVars?: Record<string, string>
  bgPattern?: string
  sx?: LhSxProps
}
LhDrawerDirection (from @lh-ui/share/core/drawer (logic.ts))
ts

export type LhDrawerDirection = 'right' | 'left' | 'top' | 'bottom'

Beauty of tradition, foundation of code