---
name: sheet
title: Sheet
url: https://ui.frontic.com/components/ui/sheet.md
tier: ui
install: npx @frontic/ui add sheet
files:
  - components/ui/sheet/Sheet.vue
  - components/ui/sheet/SheetBody.vue
  - components/ui/sheet/SheetClose.vue
  - components/ui/sheet/SheetContent.vue
  - components/ui/sheet/SheetDescription.vue
  - components/ui/sheet/SheetFooter.vue
  - components/ui/sheet/SheetHeader.vue
  - components/ui/sheet/SheetOverlay.vue
  - components/ui/sheet/SheetTitle.vue
  - components/ui/sheet/SheetTrigger.vue
  - components/ui/sheet/index.ts
registryDependencies:
  - button
dependencies:
  - "@lucide/vue"
  - "@vueuse/core"
  - class-variance-authority
  - reka-ui
---

# Sheet

Slides a panel in from a screen edge over the page, with a header, a scrolling body and a footer, for carts, filters and menus.

## Install

```sh
npx @frontic/ui add sheet
```

Built from: [Button](https://ui.frontic.com/components/ui/button.md)

Used by: [Sidebar](https://ui.frontic.com/components/ui/sidebar.md), [Navigation Shop](https://ui.frontic.com/components/sections/navigation-shop.md), [Cart Compact](https://ui.frontic.com/components/templates/cart-compact.md), [Category Filter](https://ui.frontic.com/components/templates/category-filter.md), [Category Grid](https://ui.frontic.com/components/templates/category-grid.md), [Filter Sheet](https://ui.frontic.com/components/templates/filter-sheet.md), [Filter Sidebar](https://ui.frontic.com/components/templates/filter-sidebar.md), [Filter Toolbar](https://ui.frontic.com/components/templates/filter-toolbar.md)

npm packages: `@lucide/vue`, `@vueuse/core`, `class-variance-authority`, `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Image } from '@/components/ui/image'
import { Price } from '@/components/ui/price'
import { Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'

const image = (name: string) => `https://static.frontic.com/cdn-cgi/image/width=160,format=auto/ui/products/${name}.jpg`
const eur = (amount: number) => ({ amount, currency: 'EUR' })
const items = [
  { name: 'Frost Escape Parka', variant: 'Sun yellow · M', price: eur(14900), image: image('frost-escape-parka') },
  { name: 'Blizzard Defender', variant: 'Aubergine · L', price: eur(12600), image: image('blizzard-defender-aubergine') },
  { name: 'Polar Light Ski Jacket', variant: 'Dusty pink · S', price: eur(7900), image: image('polar-light-ski-jacket') },
  { name: 'Snowflake Ski Goggles', variant: 'Mint', price: eur(12700), image: image('snowflake-ski-goggles') },
  { name: 'Mountain View Ski Goggles', variant: 'One size', price: eur(9900), image: image('mountain-view-ski-goggles') },
  { name: 'TrailMaster 22', variant: 'Brown · 20L', price: eur(14100), image: image('trailmaster-22') },
]
const subtotal = eur(items.reduce((sum, item) => sum + item.price.amount, 0))
</script>

<template>
  <Sheet default-open>
    <SheetTrigger as-child>
      <Button variant="outline" color="neutral">Open cart</Button>
    </SheetTrigger>
    <SheetContent close-label="Close cart">
      <SheetHeader>
        <SheetTitle>Your cart</SheetTitle>
        <SheetDescription>{{ items.length }} items, ready for checkout.</SheetDescription>
      </SheetHeader>
      <SheetBody>
        <ul class="flex flex-col gap-4">
          <li v-for="item in items" :key="item.name" class="flex gap-4">
            <Image :src="item.image" :alt="item.name" class="size-20 shrink-0" />
            <div class="flex flex-1 flex-col gap-1 text-sm">
              <span class="font-medium">{{ item.name }}</span>
              <span class="text-muted-foreground">{{ item.variant }}</span>
              <Price :price="item.price" />
            </div>
          </li>
        </ul>
      </SheetBody>
      <SheetFooter>
        <div class="flex items-center justify-between text-sm">
          <span>Subtotal</span>
          <Price :price="subtotal" />
        </div>
        <Button>Checkout</Button>
        <SheetClose as-child>
          <Button variant="outline" color="neutral">Keep shopping</Button>
        </SheetClose>
      </SheetFooter>
    </SheetContent>
  </Sheet>
</template>
```

The example also uses `image` and `price`, which `add sheet` leaves out: `npx @frontic/ui add image price`.

## Anatomy

```vue
<Sheet>
  <SheetTrigger />
  <SheetContent>
    <SheetHeader>
      <SheetTitle />
      <SheetDescription />
    </SheetHeader>
    <SheetBody />
    <SheetFooter>
      <SheetClose />
    </SheetFooter>
  </SheetContent>
</Sheet>
```

## API reference

### Sheet

Groups all parts of the sheet, a panel that slides in from an edge.

`data-slot="sheet"` · `Sheet.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` |  | The controlled open state of the dialog. Can be binded as `v-model:open`. |
| `defaultOpen` | `boolean` | `false` | The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. |
| `modal` | `boolean` | `true` | The modality of the dialog When set to `true`,<br><br>interaction with outside elements will be disabled and only dialog content will be visible to screen readers. |
| `unmountOnHide` | `boolean` | `true` | When set to `false`, the dialog content will not be unmounted when closed, but instead hidden with CSS.<br><br>Useful for SEO or when you want to improve performance by not remounting the component on every open. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:open` | `value: boolean` | Event handler called when the open state of the dialog changes. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ open: boolean; close: () => void; }` | Holds the sheet's trigger and content; gets whether it is open and a function that closes it. |

### SheetBody

Holds the sheet's main content and scrolls it, keeping the header and footer in view. Renders a `<div>`.

`data-slot="sheet-body"` · `SheetBody.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### SheetClose

Closes the sheet. Renders a `<button>`.

`data-slot="sheet-close"` · `SheetClose.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'button'` | The element or component this component should render as. Can be overwritten by `asChild`. |

### SheetContent

Holds what the sheet shows while open.

`data-slot="sheet-content"` · `SheetContent.vue`

Styling: `sheet-content`, which extends [`overlay-sheet`](https://ui.frontic.com/DESIGN.md#overlay-sheet). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'right'` | The screen edge the sheet slides in from. |
| `showCloseButton` | `boolean` | `true` | Shows the close button in the top corner on the end side. |
| `closeLabel` | `string` | `'Close'` | Read by screen readers on the close button. |
| `forceMount` | `boolean` | `false` | Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` |  | The element or component this component should render as. Can be overwritten by `asChild`. |
| `disableOutsidePointerEvents` | `boolean` |  | When `true`, hover/focus/click interactions will be disabled on elements outside the `DismissableLayer`. Users will need to click twice on outside elements to interact with them: once to close the `DismissableLayer`, and again to trigger the element. |

| Event | Payload | Description |
| --- | --- | --- |
| `escapeKeyDown` | `event: KeyboardEvent` | Event handler called when the escape key is down. Can be prevented. |
| `pointerDownOutside` | `event: PointerDownOutsideEvent` | Event handler called when a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
| `focusOutside` | `event: FocusOutsideEvent` | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
| `interactOutside` | `event: PointerDownOutsideEvent \| FocusOutsideEvent` | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown` event happens outside or focus moves outside of it. Can be prevented. |
| `openAutoFocus` | `event: Event` | Event handler called when auto-focusing on open. Can be prevented. |
| `closeAutoFocus` | `event: Event` | Event handler called when auto-focusing on close. Can be prevented. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-dismissable-layer` |  | Marks a layer that closes on Escape or a click outside it. |
| `data-side` | `'top' \| 'right' \| 'bottom' \| 'left'` | Carries the `side` prop. |
| `data-state` | `'open' \| 'closed'` | The state the part is in, one value per state. |

### SheetDescription

Explains the purpose of the sheet below its title. Renders a `<p>`.

`data-slot="sheet-description"` · `SheetDescription.vue`

Styling: `sheet-description`, which extends [`part-description`](https://ui.frontic.com/DESIGN.md#part-description). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'p'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### SheetFooter

Holds the actions at the bottom of the sheet. Renders a `<div>`.

`data-slot="sheet-footer"` · `SheetFooter.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### SheetHeader

Groups the title and description at the top of the sheet. Renders a `<div>`.

`data-slot="sheet-header"` · `SheetHeader.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### SheetTitle

Names the sheet. Renders a `<h2>`.

`data-slot="sheet-title"` · `SheetTitle.vue`

Styling: `sheet-title`, which extends [`part-title`](https://ui.frontic.com/DESIGN.md#part-title). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'h2'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### SheetTrigger

Opens the sheet. Renders a `<button>`.

`data-slot="sheet-trigger"` · `SheetTrigger.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'button'` | The element or component this component should render as. Can be overwritten by `asChild`. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-state` | `'open' \| 'closed'` | The state the part is in, one value per state. |

## Styling

| Part | Recipe |
| --- | --- |
| `header` | [`sheet-header`](https://ui.frontic.com/DESIGN.md#sheet-header) |
| `footer` | [`sheet-footer`](https://ui.frontic.com/DESIGN.md#sheet-footer) |
| `content` | [`sheet-content`](https://ui.frontic.com/DESIGN.md#sheet-content) |
| `body` | [`sheet-body`](https://ui.frontic.com/DESIGN.md#sheet-body) |
| `overlay` | [`sheet-overlay`](https://ui.frontic.com/DESIGN.md#sheet-overlay) |
| `title` | [`sheet-title`](https://ui.frontic.com/DESIGN.md#sheet-title) |
| `description` | [`sheet-description`](https://ui.frontic.com/DESIGN.md#sheet-description) |

## Source

`components/ui/sheet/Sheet.vue`

```vue
<script lang="ts">
/** Groups all parts of the sheet, a panel that slides in from an edge. */
export default {}
</script>

<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
import { DialogRoot, useForwardPropsEmits } from 'reka-ui'

const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()

const forwarded = useForwardPropsEmits(props, emits)

defineSlots<{
  /** Holds the sheet's trigger and content; gets whether it is open and a function that closes it. */
  default?: (props: { open: boolean; close: () => void }) => unknown
}>()
</script>

<template>
  <DialogRoot v-slot="slotProps" data-slot="sheet" v-bind="forwarded">
    <slot v-bind="slotProps" />
  </DialogRoot>
</template>
```

`components/ui/sheet/SheetBody.vue`

```vue
<script lang="ts">
/** Holds the sheet's main content and scrolls it, keeping the header and footer in view. */
export default {}
</script>

<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
import type { HTMLAttributes } from 'vue'

const props = withDefaults(
  defineProps<
    PrimitiveProps & {
      class?: HTMLAttributes['class']
    }
  >(),
  { as: 'div' },
)
</script>

<template>
  <Primitive :as="props.as" :as-child="props.asChild" data-slot="sheet-body" :class="['fui-sheet-body overflow-y-auto', props.class]">
    <slot />
  </Primitive>
</template>
```

`components/ui/sheet/SheetClose.vue`

```vue
<script lang="ts">
/** Closes the sheet. */
export default {}
</script>

<script setup lang="ts">
import type { DialogCloseProps } from 'reka-ui'
import { DialogClose, useForwardProps } from 'reka-ui'

const props = defineProps<DialogCloseProps>()
const forwarded = useForwardProps(props)
</script>

<template>
  <DialogClose data-slot="sheet-close" v-bind="forwarded">
    <slot />
  </DialogClose>
</template>
```

`components/ui/sheet/SheetContent.vue`

```vue
<script lang="ts">
/** Holds what the sheet shows while open. */
export default {}
</script>

<script setup lang="ts">
import { XIcon } from '@lucide/vue'

import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { cva } from 'class-variance-authority'
import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui'
import { Button } from '@library/components/ui/button'
import SheetOverlay from './SheetOverlay.vue'

interface SheetContentProps extends DialogContentProps {
  class?: HTMLAttributes['class']
  /** The screen edge the sheet slides in from. */
  side?: 'top' | 'right' | 'bottom' | 'left'
  /** Shows the close button in the top corner on the end side. */
  showCloseButton?: boolean
  /** Read by screen readers on the close button. */
  closeLabel?: string
}

defineOptions({
  inheritAttrs: false,
})

const props = withDefaults(defineProps<SheetContentProps>(), {
  side: 'right',
  showCloseButton: true,
  closeLabel: 'Close',
})
const emits = defineEmits<DialogContentEmits>()

const delegatedProps = reactiveOmit(props, 'class', 'side', 'showCloseButton', 'closeLabel')

const forwarded = useForwardPropsEmits(delegatedProps, emits)
const content = cva('fui-sheet-content fixed z-50 bg-clip-padding data-open:animate-in data-closed:animate-out data-open:fade-in-0 data-closed:fade-out-0 transition duration-200 ease-in-out', {
  variants: {
    side: {
      top: 'fui-sheet-content-top',
      right: 'fui-sheet-content-right',
      bottom: 'fui-sheet-content-bottom',
      left: 'fui-sheet-content-left',
    },
  },
  defaultVariants: { side: 'right' },
})
const SIDE_LAYOUT = {
  top: 'data-side-top:data-open:slide-in-from-top-10 data-side-top:data-closed:slide-out-to-top-10 inset-x-0 top-0 h-auto',
  right: 'data-side-right:data-open:slide-in-from-right-10 data-side-right:data-closed:slide-out-to-right-10 inset-y-0 right-0 h-full max-w-3/4',
  bottom: 'data-side-bottom:data-open:slide-in-from-bottom-10 data-side-bottom:data-closed:slide-out-to-bottom-10 inset-x-0 bottom-0 h-auto',
  left: 'data-side-left:data-open:slide-in-from-left-10 data-side-left:data-closed:slide-out-to-left-10 inset-y-0 left-0 h-full max-w-3/4',
}
</script>

<template>
  <DialogPortal>
    <SheetOverlay />
    <DialogContent data-slot="sheet-content" :data-side="side" :class="[content({ side }), SIDE_LAYOUT[side], props.class]" v-bind="{ ...$attrs, ...forwarded }">
      <slot />

      <DialogClose v-if="showCloseButton" data-slot="sheet-close" as-child>
        <Button variant="ghost" color="neutral" size="icon-sm" class="absolute top-4 inset-e-4">
          <XIcon data-icon />
          <span class="sr-only">{{ closeLabel }}</span>
        </Button>
      </DialogClose>
    </DialogContent>
  </DialogPortal>
</template>
```

`components/ui/sheet/SheetDescription.vue`

```vue
<script lang="ts">
/** Explains the purpose of the sheet below its title. */
export default {}
</script>

<script setup lang="ts">
import type { DialogDescriptionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DialogDescription, useForwardProps } from 'reka-ui'

const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
</script>

<template>
  <DialogDescription data-slot="sheet-description" :class="['fui-sheet-description', props.class]" v-bind="forwarded">
    <slot />
  </DialogDescription>
</template>
```

`components/ui/sheet/SheetFooter.vue`

```vue
<script lang="ts">
/** Holds the actions at the bottom of the sheet. */
export default {}
</script>

<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
import type { HTMLAttributes } from 'vue'

const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(), { as: 'div' })
</script>

<template>
  <Primitive :as="props.as" :as-child="props.asChild" data-slot="sheet-footer" :class="['fui-sheet-footer', props.class]">
    <slot />
  </Primitive>
</template>
```

`components/ui/sheet/SheetHeader.vue`

```vue
<script lang="ts">
/** Groups the title and description at the top of the sheet. */
export default {}
</script>

<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
import type { HTMLAttributes } from 'vue'

const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(), { as: 'div' })
</script>

<template>
  <Primitive :as="props.as" :as-child="props.asChild" data-slot="sheet-header" :class="['fui-sheet-header', props.class]">
    <slot />
  </Primitive>
</template>
```

`components/ui/sheet/SheetOverlay.vue`

```vue
<script setup lang="ts">
import type { DialogOverlayProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DialogOverlay, useForwardProps } from 'reka-ui'

const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
</script>

<template>
  <DialogOverlay
    data-slot="sheet-overlay"
    :class="[
      'fui-sheet-overlay data-open:animate-in data-closed:animate-out data-open:fade-in-0 data-closed:fade-out-0 duration-100 fixed inset-0 z-50 supports-backdrop-filter:backdrop-blur-sm',
      props.class,
    ]"
    v-bind="forwarded"
  >
    <slot />
  </DialogOverlay>
</template>
```

`components/ui/sheet/SheetTitle.vue`

```vue
<script lang="ts">
/** Names the sheet. */
export default {}
</script>

<script setup lang="ts">
import type { DialogTitleProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DialogTitle, useForwardProps } from 'reka-ui'

const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
</script>

<template>
  <DialogTitle data-slot="sheet-title" :class="['fui-sheet-title', props.class]" v-bind="forwarded">
    <slot />
  </DialogTitle>
</template>
```

`components/ui/sheet/SheetTrigger.vue`

```vue
<script lang="ts">
/** Opens the sheet. */
export default {}
</script>

<script setup lang="ts">
import type { DialogTriggerProps } from 'reka-ui'
import { DialogTrigger, useForwardProps } from 'reka-ui'

const props = defineProps<DialogTriggerProps>()
const forwarded = useForwardProps(props)
</script>

<template>
  <DialogTrigger data-slot="sheet-trigger" v-bind="forwarded">
    <slot />
  </DialogTrigger>
</template>
```

## API of the items the example uses

### Button

[Full docs](https://ui.frontic.com/components/ui/button.md) · `npx @frontic/ui add button`

#### Button

Lets the user trigger an action, such as adding a product to the cart. Renders a `<button>`.

`data-slot="button"` · `Button.vue`

Styling: `button`, which extends [`action`](https://ui.frontic.com/DESIGN.md#action). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'button'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `variant` | `'solid' \| 'outline' \| 'soft' \| 'ghost' \| 'link'` | `'solid'` | The visual style: a filled, outlined, tinted, background-free or text-link button. |
| `color` | `'neutral' \| 'primary' \| 'destructive' \| 'buy' \| 'checkout' \| 'inverse'` | `'primary'` | The colour role, e.g. primary for the brand colour, buy for add-to-cart, checkout for the checkout step, destructive for removing. |
| `size` | `'md' \| 'xs' \| 'sm' \| 'lg' \| 'icon-xs' \| 'icon-sm' \| 'icon-md' \| 'icon-lg'` | `'md'` | The button's height and padding; the `icon-*` sizes make a square, icon-only button. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-color` | `'neutral' \| 'primary' \| 'destructive' \| 'buy' \| 'checkout' \| 'inverse'` | Carries the `color` prop. |
| `data-size` | `'md' \| 'xs' \| 'sm' \| 'lg' \| 'icon-xs' \| 'icon-sm' \| 'icon-md' \| 'icon-lg'` | Carries the `size` prop. |
| `data-variant` | `'solid' \| 'outline' \| 'soft' \| 'ghost' \| 'link'` | Carries the `variant` prop. |

### Image

[Full docs](https://ui.frontic.com/components/ui/image.md) · `npx @frontic/ui add image`

#### Image

Shows an image with a loading state and a fallback if it fails to load. Renders a `<span>`.

`data-slot="image"` · `Image.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `src` | `string` |  | URL of the image to show. |
| `alt` | `string` |  | Text alternative read by screen readers; leave empty for a purely decorative image. |
| `loadingEffect` | `LoadingEffect`: `'skeleton' \| 'blur' \| 'none'` | `'skeleton'` | What shows until the image has loaded: a pulsing placeholder, the `placeholderSrc` preview blurred, or nothing. Blur without a `placeholderSrc` shows the pulsing placeholder. |
| `fallbackSrc` | `string` |  | URL tried when `src` fails to load; the fallback slot shows when both fail. |
| `placeholderSrc` | `string` |  | URL of a tiny version of the image, e.g. 24px wide, shown blurred while `src` loads when `loadingEffect` is `blur`. |
| `priority` | `boolean` | `false` | Loads the image first, eagerly and at high fetch priority, instead of when it nears the viewport. Set it on the one image that is the page's largest paint, such as a hero or the first gallery image. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `fallback` |  | Replaces the placeholder shown when the image fails to load and no fallback URL loads either. |

#### ImageFallback

Shows a placeholder in place of an image that failed to load. Renders a `<span>`.

`data-slot="image-fallback"` · `ImageFallback.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### Price

[Full docs](https://ui.frontic.com/components/ui/price.md) · `npx @frontic/ui add price`

#### Price

Shows a price, with the original price and discount percentage when reduced. Renders a `<span>`.

`data-slot="price"` · `Price.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `PriceSize`: `'md' \| 'sm' \| 'lg'` | `'md'` | The text size of the price. |
| `align` | `PriceAlign`: `'start' \| 'end'` | `'start'` | Which side the badge, the prices and the hint line up on. |
| `price` (required) | `PriceType` |  | The price to show: `amount` and `ref` in minor units such as cents, `currency` as an ISO 4217 code. |
| `locale` | `string` |  | The locale that formats the price, e.g. `de-DE`; the ConfigProvider's when left out. |
| `showRef` | `boolean` | `true` | Shows the reference price struck through when it is higher than the amount. |
| `showDiscount` | `boolean` | `true` | Shows the saving as a percentage next to a discounted price. |
| `showHint` | `boolean` | `true` | Shows the hint below the price when the hint slot holds one. |
| `refLabel` | `string` | `'Was'` | Read out before the original price of a reduced product; hidden on screen. |
| `amountLabel` | `string` | `'Now'` | Read out before the current price of a reduced product; hidden on screen. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `hint` |  | A note below the price, such as tax and shipping. |

#### PriceAmount

Shows the current price of the product. Renders a `<span>`.

`data-slot="price-amount"` · `PriceAmount.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

#### PriceRef

Shows the original price of a discounted product. Renders a `<span>`.

`data-slot="price-ref"` · `PriceRef.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

#### PriceDiscount

Shows the percentage saved against the original price. Renders a `<span>`.

`data-slot="price-discount"` · `PriceDiscount.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ percent: number; }` | Replaces the discount badge; gets the percentage saved. |

#### PriceHint

Holds a note below the price, such as tax and shipping, which may link on. Renders a `<span>`.

`data-slot="price-hint"` · `PriceHint.vue`

Styling: `price-hint`, which extends [`link-foreground`](https://ui.frontic.com/DESIGN.md#link-foreground). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'span'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
