Drawer
A panel that slides out from the edge of the screen.
Basic Usage
A drawer that slides in from the chosen edge, controlled with v-model:show and the placement prop.
<template>
<HLDrawer id="drawer-basic-example" v-model:show="show" :placement="placement" :width="502">
<HLDrawerContent id="drawer-content-basic">
Dragon Ball is a Japanese media franchise created by Akira Toriyama in 1984. The initial manga, written and illustrated by Toriyama,
was serialized in Weekly Shōnen Jump from 1984 to 1995, with the 519 individual chapters collected in 42 tankōbon volumes by its
publisher Shueisha.
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent } from '@platform-ui/highrise'
const show = ref(false)
const placement = ref('right')
const activate = place => {
placement.value = place
show.value = true
}
</script>Sizing
Use width for left / right placements and height for top / bottom. The prop that doesn't match the current placement is ignored, so it's safe to set both when the placement is dynamic. Both accept a number (pixels) or a CSS length string.
<template>
<HLButton id="drawer-sizing-top-btn" ghost @click="openSizing('top')">Top (height 220)</HLButton>
<HLButton id="drawer-sizing-right-btn" ghost @click="openSizing('right')">Right (width 60%)</HLButton>
<!-- height is used for top/bottom, width for left/right -->
<HLDrawer id="drawer-sizing-example" v-model:show="show" :placement="placement" width="60%" :height="220">
<HLDrawerContent
id="drawer-content-sizing"
title="Sizing"
description="width applies to left/right, height to top/bottom"
closable
@update:show="show = false"
>
Current placement: <strong>{{ placement }}</strong>
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton } from '@platform-ui/highrise'
const show = ref(false)
const placement = ref('right')
const openSizing = place => {
placement.value = place
show.value = true
}
</script>Default Size
When width / height are omitted, the drawer falls back to defaultWidth (for left / right) and defaultHeight (for top / bottom). These are also the starting size for a resizable drawer, which is their main use — set defaultWidth for the initial size and let the user drag from there.
<template>
<!-- No width/height, so defaultWidth/defaultHeight decide the size -->
<HLDrawer
id="drawer-default-sizing-example"
v-model:show="show"
:placement="placement"
:default-width="320"
:default-height="180"
>
<HLDrawerContent id="drawer-content-default-sizing" title="Default size" closable @update:show="show = false">
No <code>width</code> or <code>height</code> was passed, so the drawer uses its default size.
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent } from '@platform-ui/highrise'
const show = ref(false)
const placement = ref('right')
</script>Resizable
Set resizable to let the user drag the drawer's inner edge to change its size. Bound the drag with minWidth / maxWidth (for left / right) and listen to @update:width to track the size — the drawer manages the size internally, so the event is for observing it, not for controlling it.
Current width: 400px (min 300, max 700)
<template>
<HLButton id="drawer-resizable-btn" ghost @click="show = true">Open Resizable Drawer</HLButton>
<p>Current width: {{ width }}px (min 300, max 700)</p>
<HLDrawer
id="drawer-resizable-example"
v-model:show="show"
placement="right"
resizable
:default-width="400"
:min-width="300"
:max-width="700"
@update:width="width = Math.round($event)"
>
<HLDrawerContent
id="drawer-content-resizable"
title="Resizable"
description="Drag the left edge of this drawer"
closable
@update:show="show = false"
>
Width is <strong>{{ width }}px</strong>. Drag the drawer's left edge — it stops at 300px and 700px.
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton } from '@platform-ui/highrise'
const show = ref(false)
const width = ref(400)
</script>Resizing a Top or Bottom Drawer
For top and bottom placements the vertical equivalents apply: minHeight / maxHeight bound the drag and @update:height reports the new size.
Current height: 300px (min 200, max 500)
<template>
<HLDrawer
id="drawer-resizable-vertical-example"
v-model:show="show"
placement="bottom"
resizable
:default-height="300"
:min-height="200"
:max-height="500"
@update:height="height = Math.round($event)"
>
<HLDrawerContent
id="drawer-content-resizable-vertical"
title="Resizable Height"
description="Drag the top edge of this drawer"
closable
@update:show="show = false"
>
Height is <strong>{{ height }}px</strong>.
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent } from '@platform-ui/highrise'
const show = ref(false)
const height = ref(300)
</script>With Default Header
The default header is shown when the title, description, or icon props are provided and the showHeader prop is set to true. You can also use the header slot to customize the header.
<template>
<HLButton id="drawer-default-header-btn" ghost @click="showDefaultHeader = true">Open Drawer</HLButton>
<HLDrawer id="drawer-default-header-example" v-model:show="showDefaultHeader" placement="right">
<HLDrawerContent
id="drawer-content-default-header"
title="Header Title"
description="Header subtitle"
:icon="AlertCircleIcon"
show-header
closable
@update:show="showDefaultHeader = $event"
/>
<!-- @update:show is handled in the DrawerContent component -->
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton } from '@platform-ui/highrise'
import { AlertCircleIcon } from '@gohighlevel/ghl-icons/24/outline'
const showDefaultHeader = ref(false)
</script>Using Custom Header and Footer Slots
The header and footer slots replace the default header and footer with custom content built from HLHeaderLite and HLSectionFooter.
<template>
<HLButton id="drawer-with-slots-btn" ghost @click="showWithSlots = true">Open Drawer</HLButton>
<HLDrawer id="drawer-with-slots-example" v-model:show="showWithSlots" placement="right">
<HLDrawerContent id="drawer-content-basic">
<template #header>
<HLHeaderLite title="Header Title" subtitle="Header subtitle" @update:close="showWithSlots = false">
<template #header-icons>
<HLIcon>
<Share01Icon />
</HLIcon>
</template>
<template #header-content>
<HLHeaderLiteItem justify="end" size="sm">
<HLButton variant="ghost" size="xs" color="gray"> Button </HLButton>
</HLHeaderLiteItem>
</template>
</HLHeaderLite>
</template>
Main content goes here
<template #footer>
<HLSectionFooter id="footer" :top-padding="false" :bottom-padding="false" :horizontal-padding="false">
<HLSectionFooterItem>
<HLButton id="cancel" @click="showWithSlots = false">Cancel</HLButton>
<HLButton id="save" color="blue" variant="primary">Save</HLButton>
</HLSectionFooterItem>
</HLSectionFooter>
</template>
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import {
HLDrawer,
HLDrawerContent,
HLHeaderLite,
HLHeaderLiteItem,
HLSectionFooter,
HLSectionFooterItem,
HLButton,
HLIcon
} from '@platform-ui/highrise'
import { Share01Icon } from '@gohighlevel/ghl-icons/24/outline'
const showWithSlots = ref(false)
</script>Header Without a Close Button
Set closable to false to render the default header without its close button. Use this when dismissal must go through an explicit action — pair it with a footer button and consider :mask-closable="false" and :close-on-esc="false" so the drawer can only be closed deliberately.
<template>
<HLButton id="drawer-not-closable-btn" ghost @click="show = true">Open Drawer</HLButton>
<HLDrawer id="drawer-not-closable-example" v-model:show="show" placement="right" :mask-closable="false" :close-on-esc="false">
<HLDrawerContent
id="drawer-content-not-closable"
title="Confirm your changes"
description="This drawer has no close button"
:closable="false"
>
There is no close icon in the header, and clicking the mask or pressing Esc won't dismiss the drawer. Use the footer button below.
<template #footer>
<HLSectionFooter id="footer-not-closable" :top-padding="false" :bottom-padding="false" :horizontal-padding="false">
<HLSectionFooterItem justify="end">
<HLButton id="not-closable-done" color="blue" variant="primary" @click="show = false">Done</HLButton>
</HLSectionFooterItem>
</HLSectionFooter>
</template>
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton, HLSectionFooter, HLSectionFooterItem } from '@platform-ui/highrise'
const show = ref(false)
</script>Styling the Header, Body, and Footer
HLDrawerContent exposes a class and a style prop for each region: headerClass / headerStyle, bodyClass / bodyStyle, and footerClass / footerStyle. Use them to adjust padding, background, or borders per region without wrapping your content in extra elements.
<template>
<HLButton id="drawer-styling-hooks-btn" ghost @click="show = true">Open Drawer</HLButton>
<HLDrawer id="drawer-styling-hooks-example" v-model:show="show" placement="right" :width="420">
<HLDrawerContent
id="drawer-content-styling-hooks"
title="Styled regions"
description="Each region has its own background"
closable
:header-style="{ backgroundColor: 'var(--blue-50)' }"
:body-style="{ backgroundColor: 'var(--gray-50)', padding: '20px' }"
:footer-style="{ backgroundColor: 'var(--blue-50)' }"
@update:show="show = false"
>
The header and footer share a tinted background while the body uses a lighter one with extra padding.
<template #footer>
<HLSectionFooter id="footer-styling-hooks" :top-padding="false" :bottom-padding="false" :horizontal-padding="false">
<HLSectionFooterItem justify="end">
<HLButton id="styling-hooks-close" @click="show = false">Close</HLButton>
</HLSectionFooterItem>
</HLSectionFooter>
</template>
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton, HLSectionFooter, HLSectionFooterItem } from '@platform-ui/highrise'
const show = ref(false)
</script>INFO
headerClass, bodyClass, and footerClass take the same regions as their *Style counterparts and are the better choice when the styling is reusable — define the class in your stylesheet and pass its name. bodyContentClass / bodyContentStyle target the scrollable node inside the body, which is where you set things like min-height on the scroll area itself.
Styling the Drawer Panel
contentClass and contentStyle apply to the drawer's own scrollable panel — the element that wraps everything, including the header and footer. Use these for panel-level concerns such as background, border radius, or a shadow; use the HLDrawerContent props above for the individual regions.
<template>
<HLButton id="drawer-content-style-btn" ghost @click="show = true">Open Drawer</HLButton>
<HLDrawer
id="drawer-content-style-example"
v-model:show="show"
placement="right"
:width="420"
content-class="my-drawer-panel"
:content-style="{ borderTopLeftRadius: '16px', borderBottomLeftRadius: '16px' }"
>
<HLDrawerContent id="drawer-content-content-style" title="Rounded panel" closable @update:show="show = false">
The panel's left corners are rounded via <code>contentStyle</code>, and <code>contentClass</code> adds
<code>my-drawer-panel</code> for stylesheet targeting.
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton } from '@platform-ui/highrise'
const show = ref(false)
</script>Custom Scrollbar
The drawer body uses the browser's native scrollbar by default. Set :native-scrollbar="false" on HLDrawerContent to use the styled scrollbar instead, and pass scrollbarProps to configure it — trigger: 'none' keeps the bar always visible rather than showing it on hover.
The bar's appearance comes from the theme rather than a direct prop, so set its colour through themeOverrides inside scrollbarProps. The example below uses a deliberately high-contrast blue with a wider bar so the styled scrollbar is easy to tell apart from the native one.
<template>
<HLButton id="drawer-scrollbar-btn" ghost @click="show = true">Open Drawer</HLButton>
<HLDrawer id="drawer-scrollbar-example" v-model:show="show" placement="right" :width="420">
<HLDrawerContent
id="drawer-content-scrollbar"
title="Styled scrollbar"
description="Scroll the body to see it"
closable
:native-scrollbar="false"
:scrollbar-props="{
trigger: 'none',
themeOverrides: {
color: 'var(--blue-500)',
colorHover: 'var(--blue-700)',
width: '10px',
borderRadius: '5px',
},
}"
@update:show="show = false"
>
<p v-for="n in 30" :key="n">Paragraph {{ n }} — the blue bar on the right is the styled scrollbar, always visible.</p>
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton } from '@platform-ui/highrise'
const show = ref(false)
</script>INFO
color and colorHover set the draggable bar. width widens it (use height instead for a horizontal bar), and borderRadius at half the width keeps the ends fully rounded. Reach for themeOverrides only when you need the scrollbar to stand out — the default grey is intentionally recessive so it doesn't compete with the drawer's content.
Mount Inside a Container
By default the drawer mounts on body. Pass a CSS selector to the to prop to mount it inside a specific element instead, scoping it to that container. The target element must have position: relative (and a defined size) so the drawer is positioned within it rather than the viewport.
<template>
<HLButton id="btn-to-prop" ghost @click="showToPropDrawer = true">Open Drawer</HLButton>
<!-- The target needs `position: relative` and a size so the drawer is scoped inside it -->
<div
id="drawer-target"
style="position: relative; width: 600px; height: 300px; overflow: hidden; border: 1px solid #ccc; margin-top: 10px; display: flex; align-items: center; justify-content: center;"
>
Target Area
</div>
<HLDrawer v-model:show="showToPropDrawer" :width="400" :placement="placement" :trapFocus="false" to="#drawer-target">
<HLDrawerContent> Stoner is a 1965 novel by the American writer John Williams. </HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton } from '@platform-ui/highrise'
const showToPropDrawer = ref(false)
const placement = ref('right')
</script>Detached Components Inside a Drawer
Components with floating overlays — HLSelect, HLDropdown, HLDatePicker — teleport their popover to body by default. Inside a drawer, that popover is detached from the drawer's scroll container, so scrolling the drawer body leaves the popover hanging in its original position.
Point each component's teleport target at an element inside the drawer so the popover scrolls with the content. Use the to prop on HLSelect / HLDatePicker, and :popover-props="{ to }" on HLDropdown.
INFO
Give the drawer body a stable selector (here an inner #drawer-fields wrapper) and pass that selector as the teleport target.
<template>
<HLButton id="btn-detached" ghost @click="show = true">Open Form Drawer</HLButton>
<HLDrawer v-model:show="show" :width="400">
<HLDrawerContent id="detached-drawer-content" title="Edit details">
<!-- Inner wrapper gives the popovers a teleport target inside the drawer -->
<div id="drawer-fields" style="position: relative; display: flex; flex-direction: column; gap: 16px;">
<HLFormItem label="Category">
<HLSelect
v-model:value="category"
:options="options"
placeholder="Select a category"
to="#drawer-fields"
/>
</HLFormItem>
<HLFormItem label="Date">
<HLDatePicker v-model:value="date" to="#drawer-fields" />
</HLFormItem>
<!-- HLDropdown teleports its popover via popover-props -->
<HLDropdown :options="options" :popover-props="{ to: '#drawer-fields' }">
<HLButton>Actions</HLButton>
</HLDropdown>
<!-- Extra content so the drawer body scrolls -->
<p v-for="n in 20" :key="n">Paragraph {{ n }} — scroll the drawer, then open a field above. The popover stays attached.</p>
</div>
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton, HLFormItem, HLSelect, HLDatePicker, HLDropdown } from '@platform-ui/highrise'
const show = ref(false)
const category = ref(null)
const date = ref(null)
const options = [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
]
</script>Error State
An error state rendered inside the drawer body using HLEmpty with an error icon and retry action.
<template>
<HLButton id="drawer-error-btn" ghost @click="showErrorDrawer = true">Show Error State</HLButton>
<HLDrawer id="drawer-error-example" v-model:show="showErrorDrawer" placement="right">
<HLDrawerContent id="drawer-content-error">
<div class="flex items-center justify-center h-full">
<HLEmpty
id="error-state"
size="md"
icon="error"
title="Something went wrong while fetching your appointments"
description="You can try again now or after 10 minutes"
negative-text="Retry"
positive-text=""
/>
</div>
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLEmpty, HLButton } from '@platform-ui/highrise'
const showErrorDrawer = ref(false)
const placement = ref('right')
</script>Loading State
A loading state rendered inside the drawer body by wrapping content in HLSpin.
<template>
<HLButton id="drawer-loading-btn" ghost @click="showLoadingDrawer = true">Show Loading State</HLButton>
<HLDrawer id="drawer-loading-example" v-model:show="showLoadingDrawer" placement="right">
<HLDrawerContent id="drawer-content-loading">
<div class="flex items-center justify-center h-full">
<HLSpin id="spin">
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text
ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived
not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
</HLSpin>
</div>
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLSpin, HLButton } from '@platform-ui/highrise'
const showLoadingDrawer = ref(false)
const placement = ref('right')
</script>Loading with Header and Footer
A loading state combined with custom header and footer slots, showing HLSpin in the body while the header and footer remain visible.
<template>
<HLButton id="drawer-loading-combined-btn" ghost @click="showLoadingCombinedDrawer = true"> Show Loading with Header & Footer </HLButton>
<HLDrawer id="drawer-loading-combined-example" v-model:show="showLoadingCombinedDrawer" placement="right">
<HLDrawerContent id="drawer-content-loading-combined">
<template #header>
<HLHeaderLite title="Dragon Ball" subtitle="Last updated 2m ago" @update:close="showLoadingCombinedDrawer = false">
<template #header-icons>
<HLIcon>
<Share01Icon />
</HLIcon>
</template>
<template #header-content>
<HLHeaderLiteItem justify="end" size="sm">
<HLButton variant="ghost" size="xs" color="gray"> test </HLButton>
</HLHeaderLiteItem>
</template>
</HLHeaderLite>
</template>
<div class="flex items-center justify-center h-full">
<HLSpin id="spin-combined">
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text
ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived
not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
</HLSpin>
</div>
<template #footer>
<HLSectionFooter id="footer-combined" :top-padding="false" :bottom-padding="false" :horizontal-padding="false">
<HLSectionFooterItem justify="start">
<HLButton id="cancel-combined" @click="showLoadingCombinedDrawer = false">Cancel</HLButton>
<HLButton id="save-combined" color="blue" variant="primary">Save</HLButton>
</HLSectionFooterItem>
</HLSectionFooter>
</template>
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import {
HLDrawer,
HLDrawerContent,
HLHeaderLite,
HLHeaderLiteItem,
HLSectionFooter,
HLSectionFooterItem,
HLButton,
HLIcon,
HLSpin
} from '@platform-ui/highrise'
import { Share01Icon } from '@gohighlevel/ghl-icons/24/outline'
const showLoadingCombinedDrawer = ref(false)
const placement = ref('right')
</script>Focus Trap
When trapFocus is enabled (default), pressing Tab cycles through focusable elements inside the drawer without escaping to the page behind it.
<template>
<HLButton id="drawer-focus-trap-btn" ghost @click="show = true">Open Form Drawer</HLButton>
<HLDrawer v-model:show="show" :width="400" :trap-focus="true">
<HLDrawerContent id="drawer-focus-trap-content" title="Add Team Member">
<div style="display: flex; flex-direction: column; gap: 16px;">
<HLFormItem label="Name">
<HLInput v-model:model-value="name" placeholder="Enter full name" />
</HLFormItem>
<HLFormItem label="Email">
<HLInput v-model:model-value="email" placeholder="Enter email address" />
</HLFormItem>
<HLFormItem label="Role">
<HLSelect v-model:value="role" :options="roleOptions" placeholder="Select a role" to="#drawer-focus-trap-content" />
</HLFormItem>
</div>
<template #footer>
<HLSectionFooter id="footer" :top-padding="false" :bottom-padding="false" :horizontal-padding="false">
<HLSectionFooterItem justify="end">
<HLButton @click="show = false">Cancel</HLButton>
<HLButton variant="primary" color="blue" @click="show = false">Save</HLButton>
</HLSectionFooterItem>
</HLSectionFooter>
</template>
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton, HLFormItem, HLInput, HLSelect, HLSectionFooter, HLSectionFooterItem } from '@platform-ui/highrise'
const show = ref(false)
const name = ref('')
const email = ref('')
const role = ref(null)
const roleOptions = [
{ label: 'Admin', value: 'admin' },
{ label: 'Editor', value: 'editor' },
{ label: 'Viewer', value: 'viewer' },
]
</script>Note
If you're facing issues with unfocusable input elements when you have multiple instances open, try setting :trapFocus="false" on the underlying instance of the component.
Accessibility
- Focus trapping keeps keyboard users within dialogs/drawers so they can’t tab into the page behind the overlay. Disable it only when you intentionally need the background to stay reachable (for example, nested panels) and provide guidance on how to return to the original surface.
- When you turn trapping off, manage focus manually: move focus to the element that should be active next and offer a clear close button so users can re-enter the dialog flow without relying on a mouse.
Dismiss Behavior
Three props control how the drawer can be dismissed, and each has a matching event:
maskClosable— clicking the mask closes the drawer.@mask-clickfires on every mask click regardless of this prop.closeOnEsc— pressing Esc closes the drawer.@escfires when Esc is pressed and focus is inside the drawer.showMask— renders the dimmed backdrop. Turning it off also disables focus trapping, so the page behind stays interactive.
Toggle the switches below, open the drawer, then try clicking the mask and pressing Esc.
Event Log:
<template>
<HLButton id="drawer-dismiss-btn" ghost @click="show = true">Open Drawer</HLButton>
<div>
<p>Event Log:</p>
<div v-for="(entry, i) in eventLog" :key="i">{{ entry }}</div>
</div>
<HLDrawer
id="drawer-dismiss-example"
v-model:show="show"
placement="right"
:width="400"
:mask-closable="maskClosable"
:close-on-esc="closeOnEsc"
:show-mask="showMask"
@mask-click="log('mask-click')"
@esc="log('esc')"
>
<HLDrawerContent
id="drawer-content-dismiss"
title="Dismiss Behavior"
description="Try the mask and the Esc key"
closable
@update:show="show = false"
>
Content
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton } from '@platform-ui/highrise'
const show = ref(false)
const maskClosable = ref(true)
const closeOnEsc = ref(true)
const showMask = ref(true)
const eventLog = ref<string[]>([])
const log = (event: string) => {
eventLog.value.unshift(event)
if (eventLog.value.length > 5) eventLog.value.pop()
}
</script>INFO
@mask-click and @esc fire whether or not the drawer actually closes, so they're the right place to hook "are you sure?" behavior — set :mask-closable="false" and :close-on-esc="false", then decide in the handler whether to close.
Lifecycle Events
@after-enter fires once the open transition finishes and @after-leave once the close transition finishes. Use @after-enter for work that needs the drawer's final layout — measuring, focusing a specific element, or initialising a chart — and @after-leave to reset state after the drawer is fully out of view, so the teardown isn't visible mid-animation.
Event Log:
<template>
<HLButton id="drawer-lifecycle-btn" ghost @click="show = true">Open Drawer</HLButton>
<div>
<p>Event Log:</p>
<div v-for="(entry, i) in eventLog" :key="i">{{ entry }}</div>
</div>
<!-- autoFocus is off so focus is placed deliberately once the transition ends -->
<HLDrawer
id="drawer-lifecycle-example"
v-model:show="show"
placement="right"
:width="400"
:auto-focus="false"
@after-enter="handleAfterEnter"
@after-leave="log('after-leave — safe to reset state')"
>
<HLDrawerContent id="drawer-content-lifecycle" title="Lifecycle Events" closable @update:show="show = false">
<HLFormItem label="Focused after the open transition">
<HLInput ref="inputRef" placeholder="Focus arrives here on after-enter" />
</HLFormItem>
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton, HLFormItem, HLInput } from '@platform-ui/highrise'
const show = ref(false)
const inputRef = ref(null)
const eventLog = ref<string[]>([])
const log = (event: string) => {
eventLog.value.unshift(event)
if (eventLog.value.length > 5) eventLog.value.pop()
}
const handleAfterEnter = () => {
log('after-enter — drawer fully open, focusing input')
inputRef.value?.focus?.()
}
</script>Block Scroll
blockScroll (default true) locks scrolling on the page behind the drawer while it's open, so a scroll gesture over the mask doesn't move the page underneath. Set it to false when the background should stay scrollable — typically alongside :show-mask="false" for a non-blocking side panel.
Currently: blockScroll = true. Open the drawer, then try scrolling this page.
<template>
<HLButton id="drawer-block-scroll-off-btn" ghost @click="() => { blockScroll = false; show = true }">
Open without blockScroll
</HLButton>
<!-- A non-blocking panel: background stays scrollable and no mask is drawn -->
<HLDrawer
id="drawer-block-scroll-example"
v-model:show="show"
placement="right"
:width="360"
:block-scroll="blockScroll"
:show-mask="blockScroll"
>
<HLDrawerContent id="drawer-content-block-scroll" title="Block Scroll" closable @update:show="show = false">
blockScroll is <strong>{{ blockScroll }}</strong>. Try scrolling the page behind this drawer.
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton } from '@platform-ui/highrise'
const show = ref(false)
const blockScroll = ref(true)
</script>Display Directive
displayDirective decides what happens to the drawer's content when it's closed.
'if' (default) removes the content from the DOM, so it's rebuilt from scratch on every open — anything the user typed is gone. Prefer this when the drawer is expensive to keep mounted or should always start clean.
'show' keeps the content mounted and hides it with CSS, so state, scroll position, and in-progress input survive a close/reopen cycle. Prefer this for multi-step forms or anything a user might close by accident.
Type into the field, close the drawer, and reopen it to compare the two.
Currently: displayDirective = 'if'
<template>
<HLButton id="drawer-display-show-btn" ghost @click="() => { displayDirective = 'show'; show = true }">Open with 'show'</HLButton>
<HLDrawer
id="drawer-display-directive-example"
v-model:show="show"
placement="right"
:width="400"
:display-directive="displayDirective"
>
<HLDrawerContent id="drawer-content-display-directive" title="Display Directive" closable @update:show="show = false">
<HLFormItem label="Type something, then close and reopen">
<HLInput v-model:model-value="value" placeholder="Your text survives only with 'show'" />
</HLFormItem>
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent, HLButton, HLFormItem, HLInput } from '@platform-ui/highrise'
const show = ref(false)
const displayDirective = ref<'if' | 'show'>('if')
const value = ref('')
</script>Stacking Order
zIndex sets the stacking order of the drawer and its mask (default 1000). Raise it when the drawer must sit above another fixed-position layer in your app; leave it alone otherwise so drawers, modals, and popovers keep their default relative order.
<template>
<!-- Render above an app chrome layer that sits at z-index 1500 -->
<HLDrawer id="drawer-z-index-example" v-model:show="show" placement="right" :z-index="2000">
<HLDrawerContent id="drawer-content-z-index" title="Above the app chrome" closable @update:show="show = false">
Content
</HLDrawerContent>
</HLDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLDrawer, HLDrawerContent } from '@platform-ui/highrise'
const show = ref(false)
</script>Accessibility
- Apply
role="dialog"witharia-modal="true"when the drawer blocks background interaction, and tie the heading id toaria-labelledby. - Connect supplemental text through
aria-describedby, and keep the trigger’saria-expanded/aria-controlssynced with the drawer id. - If content loads asynchronously, mark the body
aria-busy="true"until it’s ready.
Imports
import { HLDrawer, HLDrawerContent } from '@platform-ui/highrise'
//based on the need, refer props
import type { ScrollbarProps } from '@platform-ui/naive-ui'
import type { CSSProperties } from 'vue'Props
Drawer
| Prop | Type | Default | Description |
|---|---|---|---|
| id * | string | undefined | undefined | Unique identifier for the drawer |
| autoFocus | boolean | true | Whether to focus the first focusable element inside drawer. Takes effect only when trapFocus and showMask are both true. |
| blockScroll | boolean | true | Whether to disabled body scrolling when it's active. |
| closeOnEsc | boolean | true | Whether to close drawer on Esc is pressed. |
| contentClass | string | undefined | undefined | Class of drawer's scrollable content node. |
| contentStyle | string | CSSProperties | undefined | undefined | Style of drawer's scrollable content node. |
| defaultHeight | number | string | 251 | Default height of the drawer, works when placement is top and bottom. |
| defaultWidth | number | string | 502 | Default width of the drawer, works when placement is left and right. |
| displayDirective | 'if' | 'show' | 'if' | The display directive to use when n-drawer is rendered. 'if' corresponds to v-if and 'show' corresponds to v-show. |
| height | number | string | undefined | undefined | Height of the Drawer. Works when placement is top and bottom. |
| maskClosable | boolean | true | Ability to close on clicking the mask and to emit hide event. |
| maxHeight | number | undefined | undefined | Max height of draggable drawer |
| maxWidth | number | undefined | undefined | Max width of draggable drawer. |
| minHeight | number | undefined | undefined | Min height of draggable drawer. |
| minWidth | number | undefined | undefined | Min width of draggable drawer. |
| placement | 'top' | 'right' | 'bottom' | 'left' | 'right' | Drawer placement. |
| resizable | boolean | false | Whether to resize the width / height of drawer. |
| scrollbarProps | ScrollbarProps | undefined | undefined | Options for the styled scrollbar, e.g. { trigger: 'none' } to keep it always visible or { xScrollable: true } to allow horizontal scrolling. Applies only when the scrollable node uses the styled scrollbar. |
| show | boolean | false | Whether to show drawer. |
| showMask | boolean | true | Whether to show mask. If set to false, trap-focus will be disabled. |
| to | string | HTMLElement | 'body' | Container node of the drawer. |
| trapFocus | boolean | true | Whether to trap focus inside drawer. |
| width | number | string | undefined | undefined | Width of the drawer. Works when placement is left and right. |
| zIndex | number | undefined | 1000 | Z index of the drawer. |
DrawerContent
| Prop | Type | Default | Description |
|---|---|---|---|
| id * | string | undefined | Unique identifier for the drawer content |
| bodyClass | string | undefined | Drawer content's body class. |
| bodyContentClass | string | undefined | Class of body's scrollable content node. |
| bodyContentStyle | string | CSSProperties | undefined | Style of body's scrollable content node. |
| bodyStyle | string | CSSProperties | undefined | Drawer content's body style. |
| closable | boolean | true | Whether to show the close button. Handle the update:show event in DrawerContent to close the drawer. |
| description | string | undefined | Drawer content's header description. |
| footerClass | string | undefined | Drawer content's footer class. |
| footerStyle | string | CSSProperties | undefined | Drawer content's footer style. |
| headerClass | string | undefined | Drawer content's header class. |
| headerStyle | string | CSSProperties | undefined | Drawer content's header style. |
| icon | Component | undefined | Drawer content's header icon. |
| nativeScrollbar | boolean | true | Whether to use native scrollbar on body part. |
| scrollbarProps | ScrollbarProps | undefined | Options for the styled body scrollbar, e.g. { trigger: 'none' } to keep it always visible. Applies only when nativeScrollbar is false. |
| showHeader | boolean | true | Whether to show the header. |
| title | string | undefined | Drawer content's header title. |
Emits
Drawer
| Name | Parameters | Description |
|---|---|---|
@after-enter | () => void | Callback after drawer is opened. |
@after-leave | () => void | Callback after drawer is closed. |
@esc | () => void | Callback fired when the escape key is pressed and focus is within drawer. |
@mask-click | (e: MouseEvent) => void | Callback triggered on mask clicked. |
@update:height | (height: number) => void | Callback trigger on drawer height change. |
@update:show | (show: boolean) => void | Callback triggered on drawer display status would change. |
@update:width | (width: number) => void | Callback trigger on drawer width change. |
DrawerContent
| Name | Parameters | Description |
|---|---|---|
@update:show | (show: boolean) => void | Callback triggered on drawer display status would change. To be handled when default header is used. |
Slots
Drawer
| Name | Parameters | Description |
|---|---|---|
| default | () | The default content slot |
DrawerContent
| Name | Parameters | Description |
|---|---|---|
| default | () | The default content slot |
| header | () | Custom header content slot |
| footer | () | The footer content slot |