Demos
How to import
<script setup lang="ts">
import { SideSheet } from '@fibrous/semi-ui-vue';
</script>
Basic Usage
By default, SideSheet slides from the right side of the screen and could be closed by clicking on the mask.
Use v-model:visible to control visibility: the close button, the mask and Esc update visible first, then emit @cancel. With :visible and @cancel only, the sheet does not close by itself; set visible to false in the @cancel handler.
Code
<script setup lang="ts">
import { ref } from 'vue';
import { Button, SideSheet } from '@fibrous/semi-ui-vue';
const visible = ref(false);
</script>
<template>
<Button @click="visible = !visible">Open SideSheet</Button>
<SideSheet v-model:visible="visible" title="滑动侧边栏">
<p>This is the content of a basic sidesheet.</p>
<p>Here is more content...</p>
</SideSheet>
</template>Placement
You could use placement to set the position from which SideSheet comes in, supporting one of top, bottom, left, right.
Code
<script setup lang="ts">
import { ref } from 'vue';
import { Button, Radio, RadioGroup, SideSheet, type RadioValue, type SideSheetProps } from '@fibrous/semi-ui-vue';
const visible = ref(false);
const placement = ref<RadioValue | undefined>('right');
</script>
<template>
<RadioGroup v-model="placement" aria-label="侧边栏位置">
<Radio value="right">right</Radio>
<Radio value="left">left</Radio>
<Radio value="top">top</Radio>
<Radio value="bottom">bottom</Radio>
</RadioGroup>
<br />
<br />
<Button @click="visible = !visible">Open SideSheet</Button>
<SideSheet v-model:visible="visible" :placement="placement as SideSheetProps['placement']" title="滑动侧边栏">
<p>This is the content of a basic sidesheet.</p>
<p>Here is more content...</p>
</SideSheet>
</template>Size
You could use size to set the size of SideSheet, supporting one of small(448px), medium(684px), and large(920px). Only takes effects when placement is set to left or right. If the default size does not meet your needs, you can also set the width by setting the width property, for example :width="900" / width="800px"
Code
<script setup lang="ts">
import { ref } from 'vue';
import { Button, Radio, RadioGroup, SideSheet, type RadioValue, type SideSheetProps } from '@fibrous/semi-ui-vue';
const visible = ref(false);
const size = ref<RadioValue | undefined>('small');
</script>
<template>
<RadioGroup v-model="size" aria-label="侧边栏尺寸">
<Radio value="small">small</Radio>
<Radio value="medium">medium</Radio>
<Radio value="large">large</Radio>
</RadioGroup>
<br />
<br />
<Button @click="visible = !visible">Open SideSheet</Button>
<SideSheet v-model:visible="visible" :size="size as SideSheetProps['size']" title="滑动侧边栏">
<p>This is the content of a basic sidesheet.</p>
<p>Here is more content...</p>
</SideSheet>
</template>Non-blocking Workflow
You could set :mask="false" to continue working on the area outside SideSheet.
Code
<script setup lang="ts">
import { ref } from 'vue';
import { Button, SideSheet, TextArea } from '@fibrous/semi-ui-vue';
const visible = ref(false);
const value = ref('');
</script>
<template>
<Button @click="visible = true">Open SideSheet</Button>
<TextArea v-model="value" placeholder="Please enter something" style="margin-top: 12px" />
<SideSheet v-model:visible="visible" :disableScroll="false" :mask="false" title="可操作外部的侧边栏">
<p>这里是输入的内容:</p>
<p>{{ value }}</p>
</SideSheet>
</template>Rendered Inside Container
You could use getPopupContainer to render SideSheet in targeted DOM.
Code
<script setup lang="ts">
import { ref } from 'vue';
import { Button, SideSheet } from '@fibrous/semi-ui-vue';
const visible = ref(false);
const getContainer = () => document.querySelector('.sidesheet-container') as HTMLElement;
</script>
<template>
<div
class="sidesheet-container"
:style="{
height: '320px',
overflow: 'hidden',
position: 'relative',
border: '1px solid var(--semi-color-border)',
borderRadius: '2px',
padding: '24px',
textAlign: 'center',
background: 'var(--semi-color-fill-0)',
}"
>
<span>Render in this</span>
<br />
<br />
<Button @click="visible = true">Open SideSheet</Button>
<SideSheet v-model:visible="visible" :getPopupContainer="getContainer" title="渲染在指定容器内部" :width="220">
<p>This is the content of a basic sidesheet.</p>
<p>Here is more content...</p>
</SideSheet>
</div>
</template>Customized Content
Use the #title and #footer slots and other Semi Components, you could create customized information display layers.
Code
<script setup lang="ts">
import { ref } from 'vue';
import { Banner, Button, FormDatePicker, FormRadio, FormRadioGroup, FormSelect, Form, SideSheet, TypographyText } from '@fibrous/semi-ui-vue';
const visible = ref(false);
const createTime = new Date();
const border = { borderBottom: '1px solid var(--semi-color-border)' };
</script>
<template>
<Button @click="visible = true">More Information</Button>
<SideSheet v-model:visible="visible" :bodyStyle="border" :closable="false" :headerStyle="border" title="创建资源包">
<template #footer>
<div style="display: flex; justify-content: flex-end">
<Button style="margin-right: 8px">重置</Button>
<Button theme="solid">提交</Button>
</div>
</template>
<Form>
<FormDatePicker field="date" :initValue="createTime" :label="{ text: '创建时间', required: true }" style="width: 272px" type="dateTime" />
<FormRadioGroup direction="horizontal" field="type" initValue="all" label="目标操作系统">
<FormRadio value="all">全平台</FormRadio>
<FormRadio value="ios">iOS</FormRadio>
<FormRadio value="android">Android</FormRadio>
<FormRadio value="web">Web</FormRadio>
</FormRadioGroup>
<FormRadioGroup direction="horizontal" field="origin" initValue="scm" label="资源包来源">
<FormRadio value="scm">从SCM上传</FormRadio>
<FormRadio value="manual">手动上传</FormRadio>
</FormRadioGroup>
<Banner bordered :fullMode="false" type="warning">
<template #icon />
<template #description>
<TypographyText strong>当前部署环境:线上部署</TypographyText>
<br />
<TypographyText>请选择正确的SCM构建产物,防止出现不符合预期的发布操作。</TypographyText>
</template>
</Banner>
<br />
<FormSelect field="users" :initValue="['1', '2', '3', '4']" :label="{ text: '创建用户', required: true }" multiple style="width: 560px">
<FormSelect.Option value="1">曲晨一</FormSelect.Option>
<FormSelect.Option value="2">夏可曼</FormSelect.Option>
<FormSelect.Option value="3">曲晨三</FormSelect.Option>
<FormSelect.Option value="4">蔡妍</FormSelect.Option>
</FormSelect>
</Form>
</SideSheet>
</template>API Reference
Props
| Property | Description | Type | Default |
|---|---|---|---|
| bodyStyle | Style of the panel body. | StyleValue | semiGlobalDefault("SideSheet", "bodyStyle") |
| canVerticalSetWidth | Whether width applies when placement is top or bottom. By default those sheets span the full width. | boolean | semiGlobalDefault("SideSheet", "canVerticalSetWidth") |
| closable | Whether the close button in the corner is shown. | boolean | semiDefault("SideSheet", "closable", true) |
| closeOnEsc | Whether Esc closes the sheet. | boolean | semiDefault("SideSheet", "closeOnEsc", false) |
| disableScroll | Whether body scrolling is locked (overflow: hidden on body) while a masked sheet is rendered in document.body. | boolean | semiDefault("SideSheet", "disableScroll", true) |
| getPopupContainer | Parent element the sheet is rendered into. A custom container needs position: relative; this changes where the sheet sits in the DOM, not where it is shown. | Type(() => HTMLElement) | undefined |
| headerStyle | Style of the panel header. | StyleValue | semiGlobalDefault("SideSheet", "headerStyle") |
| height | Height, used when placement is top or bottom. A number is in pixels; a string is any valid CSS length. 448 when omitted. | string | number | semiGlobalDefault("SideSheet", "height") |
| keepDOM | Whether the sheet content stays mounted while closed. | boolean | semiDefault("SideSheet", "keepDOM", false) |
| mask | Whether the mask is shown. When false the content outside stays operable. | boolean | semiDefault("SideSheet", "mask", true) |
| maskClosable | Whether clicking the mask closes the sheet. | boolean | semiDefault("SideSheet", "maskClosable", true) |
| maskStyle | Style of the mask. | StyleValue | semiGlobalDefault("SideSheet", "maskStyle") |
| motion | Whether animation is enabled. | boolean | semiDefault("SideSheet", "motion", true) |
| placement | Edge the sheet slides in from. | Placement | semiDefault("SideSheet", "placement", "right") |
| size | Size: small is 448px, medium 684px, large 920px. Used only when placement is left or right. | "small" | "medium" | "large" | semiDefault("SideSheet", "size", "small") |
| title | Plain-text title. The title slot wins when both are given. | string | semiGlobalDefault("SideSheet", "title") |
| visible | Whether the sheet is visible, used with v-model:visible. | boolean | semiGlobalDefault("SideSheet", "visible") |
| width | Width, used when placement is left or right. A number is in pixels; a string is any valid CSS length. Decided by size when omitted. | string | number | semiGlobalDefault("SideSheet", "width") |
| zIndex | z-index of the layer. | number | semiDefault("SideSheet", "zIndex", 1000) |
Events
| Event | Description | Callback parameters |
|---|---|---|
| cancel | Emitted after update:visible when the user closes the sheet with the close button, the mask or Esc. The payload is the native MouseEvent, or the native KeyboardEvent for Esc. | [event: MouseEvent | KeyboardEvent] |
| update:visible | Emitted with false before cancel when the close button, the mask or Esc closes the sheet, for v-model:visible to write back. | [visible: boolean] |
| afterVisibleChange | Emitted with true right after the commit that shows the sheet, without waiting for the enter animation. Emitted with false after the leave animation, and once with false if the sheet is unmounted while shown. Not emitted when a KeepAlive deactivates or reactivates it. | [visible: boolean] |
Slots
| Slot | Description | Scope parameters |
|---|---|---|
| default | Body of the sheet. | any |
| title | Title. It wins over the title prop and names the dialog. Nothing is rendered when it is empty. | any |
| footer | Footer of the sheet. Nothing is rendered when it is empty. | any |
| closeIcon | Icon inside the close button. An empty slot falls back to the default icon; hide the button with closable set to false. | any |
Exposes
| Name | Description | Type |
|---|---|---|
| root | The outer .semi-sidesheet element inside the portal, or null while nothing is rendered. | HTMLDivElement | null |
aria-* attributes and style land on the role="dialog" element (.semi-sidesheet-inner), as in React. class and other native attributes land on the outermost .semi-sidesheet element inside the portal, which is also the root exposed through the component ref.
Accessibility
ARIA
- SideSheet has a
dialogrole to indicate that it is a pop-up component. The title element (.semi-sidesheet-title) has theheadingrole andaria-level="2"and names the dialog througharia-labelledby; the header itself has no role. Whenaria-labelis passed it wins and that reference is not generated. - Every
aria-*attribute andstyleland on therole="dialog"element; other undeclared props land on the outer element. aria-modalis output only when the dialog is modal. A dialog is modal when it has a mask (mask) and is rendered in the body (judged by the actual container, so thegetPopupContainerof ConfigProvider counts). Withmaskset tofalse, or a mask confined to a custom container, it is not output.
Keyboard and focus
- Focus moves into the sheet when it opens.
- With
closeOnEscset, pressingEsccloses the sheet (off by default). - A modal dialog (with a mask, rendered in the body): Tab cycles inside the sheet, the rest of the page is inert, and focus returns to the element focused before it opened when it closes. A sheet with a mask rendered in a custom container does not make the page inert; see the next item.
- A non-modal dialog (
maskset tofalse, or a mask confined to a custom container): Tab can leave the sheet for the page and the page outside the container stays usable; focus returns on close only if it is still inside the sheet or has fallen to the body, and focus the user has moved elsewhere on the page stays. With a mask confined to a custom container, other content inside that container can still be reached by Tab.
Design Tokens
| Sass variable | Value | Usage |
|---|---|---|
| $animation_duration-sideSheet_mask-show | 180ms | 侧边栏打开时-蒙层颜色-动画持续时间 |
| $animation_function-sideSheet_mask-show | cubic-bezier(0.25, 0.46, 0.45, 0.94) | 侧边栏打开时-蒙层颜色-过渡曲线 |
| $animation_delay-sideSheet_mask-show | 0ms | 侧边栏打开时-蒙层颜色-延迟时间 |
| $animation_duration-sideSheet_mask-hide | 180ms | 侧边栏关闭时-蒙层颜色-动画持续时间 |
| $animation_function-sideSheet_mask-hide | cubic-bezier(0.25, 0.46, 0.45, 0.94) | 侧边栏关闭时-蒙层颜色-过渡曲线 |
| $animation_delay-sideSheet_mask-hide | 0ms | 侧边栏关闭时-蒙层颜色-延迟时间 |
| $animation_duration-sideSheet_inner-show | 180ms | 侧边栏打开-动画持续时间 |
| $animation_function-sideSheet_inner-show | cubic-bezier(0.25, 0.46, 0.45, 0.94) | 侧边栏打开-过渡曲线 |
| $animation_delay-sideSheet_inner-show | 0ms | 侧边栏打开-延迟时间 |
| $animation_duration-sideSheet_inner-hide | 180ms | 侧边栏关闭-动画持续时间 |
| $animation_function-sideSheet_inner-hide | cubic-bezier(0.25, 0.46, 0.45, 0.94) | 侧边栏关闭-过渡曲线 |
| $animation_delay-sideSheet_inner-hide | 0ms | 侧边栏关闭-延迟时间 |
| $color-sideSheet_mask-bg | var(--semi-color-overlay-bg) | 侧边栏打开后蒙层颜色 |
| $color-sideSheet-bg | var(--semi-color-bg-2) | 侧边栏背景颜色 |
| $color-sideSheet_main-text | var(--semi-color-text-0) | 侧边栏默认文本颜色 |
| $color-sideSheet_header-borderBottom | var(--semi-color-border) | 侧边栏 header 底部边框颜色 |
| $spacing-sideSheet-margin | 0 | 侧边栏整体外边距 |
| $spacing-sideSheet_title-margin | 0 | 侧边栏标题外边距 |
| $spacing-sideSheet_header-padding | $spacing-loose | 侧边栏 header 内边距 |
| $spacing-sideSheet_header-paddingBottom | $spacing-base-loose | 侧边栏 header 底部内边距 |
| $spacing-sideSheet_body-paddingY | 0 | 侧边栏 body 垂直内边距 |
| $spacing-sideSheet_body-paddingX | $spacing-loose | 侧边栏 body 水平内边距 |
| $spacing-sideSheet_footer-padding | $spacing-loose | 侧边栏 footer 内边距 |
| $width-sideSheet_header-borderBottom | 0px | 侧边栏 header 底部边框宽度 |
| $width-sideSheet_size-small | 448px | 小尺寸侧边栏宽度 |
| $width-sideSheet_size-medium | 684px | 中尺寸侧边栏宽度 |
| $width-sideSheet_size-large | 920px | 大尺寸侧边栏宽度 |
| $font-sideSheet_title-fontWeight | $font-weight-bold | 侧边栏标题文本字重 |
| $font-sideSheet_title-fontSize | $font-size-header-5 | 侧边栏标题文本字号 |
| $filter-sideSheet_mask-bg | none | |
| $filter-sideSheet-bg | none |
FAQ
- Does SideSheet disable body scrolling automatically? When a SideSheet with a mask is rendered in the body by default (no
getPopupContainer), it addsoverflow: hiddento the body on open to block scrolling. SetdisableScrolltofalseto allow scrolling; scrolling is not locked whenmaskisfalse.