Drawer New

A toggleable drawer with fluid enter/exit transitions.

Usage

Use a Button or any other component in the default slot of the Drawer.

Then, use the #content slot to add the content displayed when the Drawer is open.

<template>
  <B24Drawer>
    <B24Button label="Open" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </B24Drawer>
</template>

You can also use the #header, #body and #footer slots to customize the Drawer's content.

Title

Use the title prop to set the title of the Drawer's header.

<template>
  <B24Drawer title="Drawer with title">
    <B24Button label="Open" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </B24Drawer>
</template>

Description

Use the description prop to set the description of the Drawer's header.

<template>
  <B24Drawer
    title="Drawer with description"
    description="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
  >
    <B24Button label="Open" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </B24Drawer>
</template>

Direction

Use the direction prop to control the direction of the Drawer. Defaults to bottom.

<template>
  <B24Drawer direction="right">
    <B24Button label="Open" />

    <template #content>
      <Placeholder class="min-w-96 min-h-96 size-full m-4" />
    </template>
  </B24Drawer>
</template>

Inset

Use the inset prop to inset the Drawer from the edges.

<template>
  <B24Drawer direction="right" inset>
    <B24Button label="Open" />

    <template #content>
      <Placeholder class="min-w-96 min-h-96 size-full m-4" />
    </template>
  </B24Drawer>
</template>

Handle

Use the handle prop to control whether the Drawer has a handle or not. Defaults to true.

<template>
  <B24Drawer :handle="false">
    <B24Button label="Open" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </B24Drawer>
</template>

Handle Only

Use the handle-only prop to only allow the Drawer to be dragged by the handle.

<template>
  <B24Drawer handle-only>
    <B24Button label="Open" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </B24Drawer>
</template>

Overlay

Use the overlay prop to control whether the Drawer has an overlay or not. Defaults to true.

<template>
  <B24Drawer :overlay="false">
    <B24Button label="Open" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </B24Drawer>
</template>

@todo add overlayBlur

Use the modal prop to control whether the Drawer blocks interaction with outside content. Defaults to true.

When modal is set to false, the overlay is automatically disabled and outside content becomes interactive.
<template>
  <B24Drawer :modal="false">
    <B24Button label="Open" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </B24Drawer>
</template>

Dismissible

Use the dismissible prop to control whether the Drawer is dismissible when clicking outside of it or pressing escape. Defaults to true.

A close:prevent event will be emitted when the user tries to close it.
You can combine modal: false with dismissible: false to make the Drawer's background interactive without closing it.
<script setup lang="ts">
import Cross50Icon from '@bitrix24/b24icons-vue/actions/Cross50Icon'

const open = ref(false)
</script>

<template>
  <B24Drawer v-model:open="open" :dismissible="false" :modal="false" :handle="false">
    <B24Button label="Open" use-dropdown :b24ui="{ trailingIcon: 'rotate-180' }" />

    <template #body>
      <div class="flex items-center justify-between gap-4 mb-4">
        <ProseH2 class="mb-0"> Drawer non-dismissible </ProseH2>

        <B24Button color="air-tertiary" :icon="Cross50Icon" @click="open = false" />
      </div>

      <Placeholder class="size-full min-h-48" />
    </template>
  </B24Drawer>
</template>

Scale Background

Use the should-scale-background prop to scale the background when the Drawer is open, creating a visual depth effect. You can set the set-background-color-on-scale prop to false to prevent changing the background color.

<template>
  <B24Drawer should-scale-background set-background-color-on-scale>
    <B24Button label="Open" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </B24Drawer>
</template>
Make sure to add the data-vaul-drawer-wrapper directive to a parent element of your app to make this work.@todo fix this bg-default
app.vue
<template>
  <B24App>
    <div class="bg-default" data-vaul-drawer-wrapper>
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </div>
  </B24App>
</template>
@todo fix this bg-default
nuxt.config.ts
export default defineNuxtConfig({
  app: {
    rootAttrs: {
      'data-vaul-drawer-wrapper': '',
      'class': 'bg-default'
    }
  }
})

Examples

Control open state

You can control the open state by using the default-open prop or the v-model:open directive.

<script setup lang="ts">
const open = ref(false)

defineShortcuts({
  o: () => (open.value = !open.value)
})
</script>

<template>
  <B24Drawer v-model:open="open">
    <B24Button label="Open" use-dropdown :b24ui="{ trailingIcon: 'rotate-180' }" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </B24Drawer>
</template>
In this example, leveraging defineShortcuts, you can toggle the Drawer by pressing O.
This allows you to move the trigger outside of the Drawer or remove it entirely.

Responsive drawer

You can render a Modal component on desktop and a Drawer on mobile for example.

<script lang="ts" setup>
import { createReusableTemplate, useMediaQuery } from '@vueuse/core'

const [DefineFormTemplate, ReuseFormTemplate] = createReusableTemplate()
const isDesktop = useMediaQuery('(min-width: 768px)')

const open = ref(false)

const state = reactive({
  email: undefined
})

const title = 'Edit profile'
const description = "Make changes to your profile here. Click save when you're done."
</script>

<template>
  <DefineFormTemplate>
    <B24Form :state="state" class="space-y-4">
      <B24FormField label="Email" name="email" required>
        <B24Input v-model="state.email" placeholder="shadcn@example.com" required />
      </B24FormField>

      <B24Button label="Save changes" color="air-primary-success" type="submit" />
    </B24Form>
  </DefineFormTemplate>

  <B24Modal v-if="isDesktop" v-model:open="open" :title="title" :description="description">
    <B24Button label="Edit profile" />

    <template #body>
      <ReuseFormTemplate />
    </template>
  </B24Modal>

  <B24Drawer v-else v-model:open="open" :title="title" :description="description">
    <B24Button label="Edit profile" />

    <template #body>
      <ReuseFormTemplate />
    </template>
  </B24Drawer>
</template>

Nested drawers

You can nest drawers within each other by using the nested prop.

<template>
  <B24Drawer :b24ui="{ content: 'h-full' }">
    <B24Button label="Open" use-dropdown :b24ui="{ trailingIcon: 'rotate-180' }" />

    <template #footer>
      <B24Drawer nested :b24ui="{ content: 'h-full' }">
        <B24Button label="Open nested" />

        <template #content>
          <Placeholder class="flex-1 m-4" />
        </template>
      </B24Drawer>
    </template>
  </B24Drawer>
</template>

Use the #footer slot to add content after the Drawer's body.

<script setup lang="ts">
const open = ref(false)
</script>

<template>
  <B24Drawer
    v-model:open="open"
    title="Drawer with footer"
    description="This is useful when you want a form in a Drawer."
    :b24ui="{ container: 'max-w-xl mx-auto' }"
  >
    <B24Button label="Open" use-dropdown :b24ui="{ trailingIcon: 'rotate-180' }" />

    <template #body>
      <Placeholder class="h-48" />
    </template>

    <template #footer>
      <B24Button label="Submit" color="air-primary-success" />
      <B24Button label="Cancel" @click="open = false" />
    </template>
  </B24Drawer>
</template>

With command palette

You can use a CommandPalette component inside the Drawer's content.

<script setup lang="ts">
import PersonSearchIcon from '@bitrix24/b24icons-vue/outline/PersonSearchIcon'

const searchTerm = ref('')

const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'command-palette-users',
  params: { q: searchTerm },
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
  },
  lazy: true,
  onRequestError({ request }) { console.warn('[fetch request error]', request) }
})

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || [],
  ignoreFilter: true
}])
</script>

<template>
  <B24Drawer :handle="false">
    <B24Button
      label="Search users..."
      :icon="PersonSearchIcon"
      use-dropdown
      :b24ui="{ trailingIcon: 'rotate-180' }"
    />

    <template #content>
      <B24CommandPalette
        v-model:search-term="searchTerm"
        :loading="status === 'pending'"
        :groups="groups"
        placeholder="Search users..."
        class="h-[320px]"
      />
    </template>
  </B24Drawer>
</template>

API

Props

Prop Default Type
as'div'any

The element or component this component should render as.

title string
description string
insetfalseboolean

Whether to inset the drawer from the edges.

content DialogContentProps & Partial<EmitsToProps<DialogContentImplEmits>>

The content of the drawer.

overlaytrueboolean

Render an overlay behind the drawer.

overlayBlur'auto' "auto" | "on" | "off"

Render an overlay blur behind the modal. auto use motion-safe.

handletrueboolean

Render a handle on the drawer.

portaltrue string | false | true | HTMLElement

Render the drawer in a portal.

nestedfalseboolean

Whether the drawer is nested in another drawer.

scrollbarThintrueboolean
activeSnapPoint null | string | number
closeThreshold number

Number between 0 and 1 that determines when the drawer should be closed. Example: threshold of 0.5 would close the drawer if the user swiped for 50% of the height of the drawer or more.

shouldScaleBackgroundboolean
setBackgroundColorOnScaleboolean

When false we don't change body's background color when the drawer is open.

scrollLockTimeout number

Duration for which the drawer is not draggable after scrolling content inside of the drawer.

fixedboolean

When true, don't move the drawer upwards if there's space, but rather only change it's height so it's fully scrollable when the keyboard is open

dismissibletrueboolean

When false dragging, clicking outside, pressing esc, etc. will not close the drawer. Use this in combination with the open prop, otherwise you won't be able to open/close the drawer.

modaltrueboolean

When false it allows to interact with elements outside of the drawer without closing it.

openboolean
defaultOpenboolean

Opened by default, skips initial enter animation. Still reacts to open state changes

direction'bottom' "bottom" | "top" | "right" | "left"

Direction of the drawer. Can be top or bottom, left, right.

noBodyStylesboolean

When true the body doesn't get any styles assigned from Vaul

handleOnlyboolean

When true only allows the drawer to be dragged by the <Drawer.Handle /> component.

preventScrollRestorationboolean
snapPoints (string | number)[]

Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. Should go from least visible. Example [0.2, 0.5, 0.8]. You can also use px values, which doesn't take screen height into account.

b24ui { overlay?: ClassNameValue; content?: ClassNameValue; handle?: ClassNameValue; container?: ClassNameValue; header?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; }

Slots

Slot Type
default{}
content{}
header{}
title{}
description{}
body{}
footer{}

Emits

Event Type
close[]
drag[percentageDragged: number]
close:prevent[]
release[open: boolean]
update:open[open: boolean]
update:activeSnapPoint[val: string | number]
animationEnd[open: boolean]

Theme

app.config.ts
export default defineAppConfig({
  b24ui: {
    drawer: {
      slots: {
        overlay: 'fixed inset-0',
        content: 'fixed base-mode bg-(--ui-color-bg-content-primary) flex focus:outline-none ring ring-(--ui-color-divider-default)',
        handle: 'shrink-0 !bg-(--ui-color-divider-default) transition-opacity',
        container: 'w-full flex flex-col gap-4 p-4 overflow-y-auto',
        header: '',
        title: 'font-[family-name:var(--ui-font-family-primary)] text-(--b24ui-typography-label-color) font-(--ui-font-weight-medium) mb-0 text-[calc(var(--ui-font-size-2xl)+2px)]/(--ui-font-size-2xl)',
        description: 'mt-1 text-(--b24ui-typography-description-color) text-(length:--ui-font-size-sm)',
        body: 'flex-1',
        footer: 'flex flex-col gap-1.5'
      },
      variants: {
        overlayBlur: {
          auto: {
            overlay: 'motion-safe:backdrop-blur-[2px]'
          },
          on: {
            overlay: 'backdrop-blur-[2px]'
          },
          off: {
            overlay: ''
          }
        },
        scrollbarThin: {
          true: {
            body: 'scrollbar-thin'
          }
        },
        overlay: {
          true: {
            overlay: 'bg-[#003366]/20'
          }
        },
        direction: {
          top: {
            content: 'mb-24 flex-col-reverse',
            handle: 'mb-4'
          },
          right: {
            content: 'flex-row',
            handle: '!ml-4'
          },
          bottom: {
            content: 'mt-24 flex-col',
            handle: 'mt-4'
          },
          left: {
            content: 'flex-row-reverse',
            handle: '!mr-4'
          }
        },
        inset: {
          true: {
            content: 'rounded-lg after:hidden overflow-hidden [--initial-transform:calc(100%+1.5rem)]'
          }
        },
        snapPoints: {
          true: ''
        }
      },
      compoundVariants: [
        {
          direction: [
            'top',
            'bottom'
          ],
          class: {
            content: 'h-auto max-h-[96%]',
            handle: '!w-12 !h-1.5 mx-auto'
          }
        },
        {
          direction: [
            'top',
            'bottom'
          ],
          snapPoints: true,
          class: {
            content: 'h-full'
          }
        },
        {
          direction: [
            'right',
            'left'
          ],
          class: {
            content: 'w-auto max-w-[calc(100%-2rem)]',
            handle: '!h-12 !w-1.5 mt-auto mb-auto'
          }
        },
        {
          direction: [
            'right',
            'left'
          ],
          snapPoints: true,
          class: {
            content: 'w-full'
          }
        },
        {
          direction: 'top',
          inset: true,
          class: {
            content: 'inset-x-4 top-4'
          }
        },
        {
          direction: 'top',
          inset: false,
          class: {
            content: 'inset-x-0 top-0 rounded-b-lg'
          }
        },
        {
          direction: 'bottom',
          inset: true,
          class: {
            content: 'inset-x-4 bottom-4'
          }
        },
        {
          direction: 'bottom',
          inset: false,
          class: {
            content: 'inset-x-0 bottom-0 rounded-t-lg'
          }
        },
        {
          direction: 'left',
          inset: true,
          class: {
            content: 'inset-y-4 left-4'
          }
        },
        {
          direction: 'left',
          inset: false,
          class: {
            content: 'inset-y-0 left-0 rounded-r-lg'
          }
        },
        {
          direction: 'right',
          inset: true,
          class: {
            content: 'inset-y-4 right-4'
          }
        },
        {
          direction: 'right',
          inset: false,
          class: {
            content: 'inset-y-0 right-0 rounded-l-lg'
          }
        }
      ]
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import bitrix24UIPluginVite from '@bitrix24/b24ui-nuxt/vite'

export default defineConfig({
  plugins: [
    vue(),
    bitrix24UIPluginVite({
      b24ui: {
        drawer: {
          slots: {
            overlay: 'fixed inset-0',
            content: 'fixed base-mode bg-(--ui-color-bg-content-primary) flex focus:outline-none ring ring-(--ui-color-divider-default)',
            handle: 'shrink-0 !bg-(--ui-color-divider-default) transition-opacity',
            container: 'w-full flex flex-col gap-4 p-4 overflow-y-auto',
            header: '',
            title: 'font-[family-name:var(--ui-font-family-primary)] text-(--b24ui-typography-label-color) font-(--ui-font-weight-medium) mb-0 text-[calc(var(--ui-font-size-2xl)+2px)]/(--ui-font-size-2xl)',
            description: 'mt-1 text-(--b24ui-typography-description-color) text-(length:--ui-font-size-sm)',
            body: 'flex-1',
            footer: 'flex flex-col gap-1.5'
          },
          variants: {
            overlayBlur: {
              auto: {
                overlay: 'motion-safe:backdrop-blur-[2px]'
              },
              on: {
                overlay: 'backdrop-blur-[2px]'
              },
              off: {
                overlay: ''
              }
            },
            scrollbarThin: {
              true: {
                body: 'scrollbar-thin'
              }
            },
            overlay: {
              true: {
                overlay: 'bg-[#003366]/20'
              }
            },
            direction: {
              top: {
                content: 'mb-24 flex-col-reverse',
                handle: 'mb-4'
              },
              right: {
                content: 'flex-row',
                handle: '!ml-4'
              },
              bottom: {
                content: 'mt-24 flex-col',
                handle: 'mt-4'
              },
              left: {
                content: 'flex-row-reverse',
                handle: '!mr-4'
              }
            },
            inset: {
              true: {
                content: 'rounded-lg after:hidden overflow-hidden [--initial-transform:calc(100%+1.5rem)]'
              }
            },
            snapPoints: {
              true: ''
            }
          },
          compoundVariants: [
            {
              direction: [
                'top',
                'bottom'
              ],
              class: {
                content: 'h-auto max-h-[96%]',
                handle: '!w-12 !h-1.5 mx-auto'
              }
            },
            {
              direction: [
                'top',
                'bottom'
              ],
              snapPoints: true,
              class: {
                content: 'h-full'
              }
            },
            {
              direction: [
                'right',
                'left'
              ],
              class: {
                content: 'w-auto max-w-[calc(100%-2rem)]',
                handle: '!h-12 !w-1.5 mt-auto mb-auto'
              }
            },
            {
              direction: [
                'right',
                'left'
              ],
              snapPoints: true,
              class: {
                content: 'w-full'
              }
            },
            {
              direction: 'top',
              inset: true,
              class: {
                content: 'inset-x-4 top-4'
              }
            },
            {
              direction: 'top',
              inset: false,
              class: {
                content: 'inset-x-0 top-0 rounded-b-lg'
              }
            },
            {
              direction: 'bottom',
              inset: true,
              class: {
                content: 'inset-x-4 bottom-4'
              }
            },
            {
              direction: 'bottom',
              inset: false,
              class: {
                content: 'inset-x-0 bottom-0 rounded-t-lg'
              }
            },
            {
              direction: 'left',
              inset: true,
              class: {
                content: 'inset-y-4 left-4'
              }
            },
            {
              direction: 'left',
              inset: false,
              class: {
                content: 'inset-y-0 left-0 rounded-r-lg'
              }
            },
            {
              direction: 'right',
              inset: true,
              class: {
                content: 'inset-y-4 right-4'
              }
            },
            {
              direction: 'right',
              inset: false,
              class: {
                content: 'inset-y-0 right-0 rounded-l-lg'
              }
            }
          ]
        }
      }
    })
  ]
})
Releases
Published under MIT License.

Copyright © 2024-present Bitrix24