Usage
Use a Button or any other component in the default slot of the Slideover.
Then, use the #content slot to add the content displayed when the Slideover is open.
<template>
<PSlideover>
<PButton label="Open" color="neutral" variant="subtle" />
<template #content>
<Placeholder class="h-full m-4" />
</template>
</PSlideover>
</template>You can also use the #header, #body and #footer slots to customize the Slideover's content.
Title
Use the title prop to set the title of the Slideover's header.
<template>
<PSlideover title="Slideover with title">
<PButton label="Open" color="neutral" variant="subtle" />
<template #body>
<Placeholder class="h-full" />
</template>
</PSlideover>
</template>Description
Use the description prop to set the description of the Slideover's header.
<template>
<PSlideover
title="Slideover with description"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
>
<PButton label="Open" color="neutral" variant="subtle" />
<template #body>
<Placeholder class="h-full" />
</template>
</PSlideover>
</template>Close
Use the close prop to customize or hide the close button (with false value) displayed in the Slideover's header.
You can pass any property from the Button component to customize it.
<template>
<PSlideover
title="Slideover with close button"
:close="{
color: 'primary',
variant: 'outline',
class: 'rounded-full'
}"
>
<PButton label="Open" color="neutral" variant="subtle" />
<template #body>
<Placeholder class="h-full" />
</template>
</PSlideover>
</template>#content slot is used as it's a part of the header.Close Icon
Use the close-icon prop to customize the close button Icon. Defaults to i-lucide-x.
<template>
<PSlideover title="Slideover with close button" close-icon="i-lucide-arrow-right">
<PButton label="Open" color="neutral" variant="subtle" />
<template #body>
<Placeholder class="h-full" />
</template>
</PSlideover>
</template>Side
Use the side prop to set the side of the screen where the Slideover will slide in from. Defaults to right.
<template>
<PSlideover side="left" title="Slideover with side">
<PButton label="Open" color="neutral" variant="subtle" />
<template #body>
<Placeholder class="h-full min-h-48" />
</template>
</PSlideover>
</template>Inset
Use the inset prop to inset the Slideover from the edges.
<template>
<PSlideover side="right" inset title="Slideover with inset">
<PButton label="Open" color="neutral" variant="subtle" />
<template #body>
<Placeholder class="min-w-96 min-h-96 size-full" />
</template>
</PSlideover>
</template>Transition
Use the transition prop to control whether the Slideover is animated or not. Defaults to true.
<template>
<PSlideover :transition="false" title="Slideover without transition">
<PButton label="Open" color="neutral" variant="subtle" />
<template #body>
<Placeholder class="h-full" />
</template>
</PSlideover>
</template>Overlay
Use the overlay prop to control whether the Slideover has an overlay or not. Defaults to true.
<template>
<PSlideover :overlay="false" title="Slideover without overlay">
<PButton label="Open" color="neutral" variant="subtle" />
<template #body>
<Placeholder class="h-full" />
</template>
</PSlideover>
</template>Modal
Use the modal prop to control whether the Slideover blocks interaction with outside content. Defaults to true.
modal is set to false, the overlay is automatically disabled and outside content becomes interactive.<template>
<PSlideover title="Slideover interactive">
<PButton label="Open" color="neutral" variant="subtle" />
<template #body>
<Placeholder class="h-full" />
</template>
</PSlideover>
</template>Dismissible
Use the dismissible prop to control whether the Slideover is dismissible when clicking outside of it or pressing escape. Defaults to true.
close:prevent event will be emitted when the user tries to close it.modal: false with dismissible: false to make the Slideover's background interactive without closing it.<template>
<PSlideover :dismissible="false" modal title="Slideover non-dismissible">
<PButton label="Open" color="neutral" variant="subtle" />
<template #body>
<Placeholder class="h-full" />
</template>
</PSlideover>
</template>Unmount
Use the unmount-on-hide prop to prevent the Slideover's content from being unmounted when it is closed. Defaults to true.
<template>
<PSlideover :unmount-on-hide="false" title="Slideover">
<PButton label="Open" color="neutral" variant="subtle" />
<template #body>
<Placeholder class="h-full" />
</template>
</PSlideover>
</template>portal prop is set to false, the content is also rendered on the server. This is useful to render an open Slideover during SSR without a flash on page load, or to expose its content for SEO.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>
<PSlideover v-model:open="open">
<PButton
label="Open"
color="neutral"
variant="subtle"
/>
<template #content>
<Placeholder class="m-4 h-full" />
</template>
</PSlideover>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const open = ref(false);
defineShortcuts({
o: () => open.value = !open.value,
});
</script>
<template>
<PSlideover v-model:open="open">
<PButton
label="Open"
color="neutral"
variant="subtle"
/>
<template #content>
<Placeholder class="m-4 h-full" />
</template>
</PSlideover>
</template>
defineShortcuts, you can toggle the Slideover by pressing O.Programmatic usage
You can use the useOverlay composable to open a Slideover programmatically.
App component which uses the OverlayProvider component.First, create a slideover component that will be opened programmatically:
<script setup lang="ts">
defineProps<{
count: number
}>()
const emit = defineEmits<{ close: [boolean] }>()
</script>
<template>
<PSlideover
:close="{ onClick: () => emit('close', false) }"
:description="`This slideover was opened programmatically ${count} times`"
>
<template #body>
<Placeholder class="h-full" />
</template>
<template #footer>
<div class="flex gap-2">
<PButton color="neutral" label="Dismiss" @click="emit('close', false)" />
<PButton label="Success" @click="emit('close', true)" />
</div>
</template>
</PSlideover>
</template>close event when the slideover is closed or dismissed here. You can emit any data through the close event, and that data becomes the resolved value of open(). The event must be emitted for the promise to resolve.Then, use it in your app:
<script setup lang="ts">
import { LazySlideoverExample } from '#components';
const count = ref(0);
const toast = useToast();
const overlay = useOverlay();
const slideover = overlay.create(LazySlideoverExample);
async function open() {
const instance = slideover.open({
count: count.value,
});
const shouldIncrement = await instance.result;
if (shouldIncrement) {
count.value++;
toast.add({
title: `Success: ${shouldIncrement}`,
color: 'success',
id: 'slideover-success',
});
// Update the count
slideover.patch({
count: count.value,
});
return;
}
toast.add({
title: `Dismissed: ${shouldIncrement}`,
color: 'error',
id: 'slideover-dismiss',
});
}
</script>
<template>
<PButton
label="Open"
color="neutral"
variant="subtle"
@click="open"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { LazySlideoverExample } from '#components';
const count = ref(0);
const toast = useToast();
const overlay = useOverlay();
const slideover = overlay.create(LazySlideoverExample);
async function open() {
const instance = slideover.open({
count: count.value,
});
const shouldIncrement = await instance.result;
if (shouldIncrement) {
count.value++;
toast.add({
title: `Success: ${shouldIncrement}`,
color: 'success',
id: 'slideover-success',
});
// Update the count
slideover.patch({
count: count.value,
});
return;
}
toast.add({
title: `Dismissed: ${shouldIncrement}`,
color: 'error',
id: 'slideover-dismiss',
});
}
</script>
<template>
<PButton
label="Open"
color="neutral"
variant="subtle"
@click="open"
/>
</template>
emit('close').Nested slideovers
You can nest slideovers within each other.
<script setup lang="ts">
const first = ref(false);
const second = ref(false);
</script>
<template>
<PSlideover
v-model:open="first"
title="First slideover"
:ui="{ footer: 'justify-end' }"
>
<PButton
color="neutral"
variant="subtle"
label="Open"
/>
<template #body>
<Placeholder class="h-full" />
</template>
<template #footer>
<PButton
label="Close"
color="neutral"
variant="outline"
@click="first = false"
/>
<PSlideover
v-model:open="second"
title="Second slideover"
:ui="{ footer: 'justify-end' }"
>
<PButton
label="Open second"
color="neutral"
/>
<template #body>
<Placeholder class="h-full" />
</template>
<template #footer>
<PButton
label="Close"
color="neutral"
variant="outline"
@click="second = false"
/>
</template>
</PSlideover>
</template>
</PSlideover>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const first = ref(false);
const second = ref(false);
</script>
<template>
<PSlideover
v-model:open="first"
title="First slideover"
:ui="{ footer: 'justify-end' }"
>
<PButton
color="neutral"
variant="subtle"
label="Open"
/>
<template #body>
<Placeholder class="h-full" />
</template>
<template #footer>
<PButton
label="Close"
color="neutral"
variant="outline"
@click="first = false"
/>
<PSlideover
v-model:open="second"
title="Second slideover"
:ui="{ footer: 'justify-end' }"
>
<PButton
label="Open second"
color="neutral"
/>
<template #body>
<Placeholder class="h-full" />
</template>
<template #footer>
<PButton
label="Close"
color="neutral"
variant="outline"
@click="second = false"
/>
</template>
</PSlideover>
</template>
</PSlideover>
</template>
With footer slot
Use the #footer slot to add content after the Slideover's body.
<script setup lang="ts">
const open = ref(false);
</script>
<template>
<PSlideover
v-model:open="open"
title="Slideover with footer"
description="This is useful when you want a form in a Slideover."
:ui="{ footer: 'justify-end' }"
>
<PButton
label="Open"
color="neutral"
variant="subtle"
/>
<template #body>
<Placeholder class="h-full" />
</template>
<template #footer="{ close }">
<PButton
label="Cancel"
color="neutral"
variant="outline"
@click="close"
/>
<PButton
label="Submit"
color="neutral"
/>
</template>
</PSlideover>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const open = ref(false);
</script>
<template>
<PSlideover
v-model:open="open"
title="Slideover with footer"
description="This is useful when you want a form in a Slideover."
:ui="{ footer: 'justify-end' }"
>
<PButton
label="Open"
color="neutral"
variant="subtle"
/>
<template #body>
<Placeholder class="h-full" />
</template>
<template #footer="{ close }">
<PButton
label="Cancel"
color="neutral"
variant="outline"
@click="close"
/>
<PButton
label="Submit"
color="neutral"
/>
</template>
</PSlideover>
</template>
API
Props
| Prop | Default | Type |
|---|---|---|
title | string | |
description | string | |
content | DialogContentProps & Partial<EmitsToProps<DialogContentImplEmits>>The content of the slideover. | |
overlay | true | boolean Render an overlay behind the slideover. |
transition | true | boolean Animate the slideover when opening or closing. |
side | 'right' | "top" | "right" | "bottom" | "left"The side of the slideover. |
inset | false | boolean Whether to inset the slideover from the edges. |
portal | true | string | false | true | HTMLElementRender the slideover in a portal. |
close | true | boolean | Omit<ButtonProps, LinkPropsKeys> Display a close button to dismiss the slideover.
|
closeIcon | appConfig.ui.icons.close | anyThe icon displayed in the close button. |
dismissible | true | boolean When |
open | boolean The controlled open state of the dialog. Can be binded as | |
defaultOpen | boolean The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. | |
modal | boolean The modality of the dialog When set to | |
unmountOnHide | true | boolean When set to |
ui | { overlay?: SlotClass; content?: SlotClass; header?: SlotClass; wrapper?: SlotClass; body?: SlotClass; footer?: SlotClass; title?: SlotClass; description?: SlotClass; close?: SlotClass; } |
Slots
| Slot | Type |
|---|---|
default | { open: boolean; } |
content | { close: () => void; } |
header | { close: () => void; } |
title | {} |
description | {} |
actions | {} |
close | { ui: object; } |
body | { close: () => void; } |
footer | { close: () => void; } |
Emits
| Event | Type |
|---|---|
enter | [] |
leave | [] |
after:leave | [] |
after:enter | [] |
close:prevent | [] |
update:open | [value: boolean] |
Theme
export default defineAppConfig({
ui: {
slideover: {
slots: {
overlay: 'fixed inset-0 bg-background-elevated/75',
content: 'bg-background flex flex-col ring-border fixed divide-border divide-y focus:outline-none sm:ring sm:shadow-lg',
header: 'p-4 flex gap-1.5 min-h-$ui-header-height items-center sm:px-6',
wrapper: '',
body: 'flex-1 overflow-y-auto p-4 sm:p-6',
footer: 'flex items-center gap-1.5 p-4 sm:px-6',
title: 'color-text-highlighted font-600',
description: 'mt-1 color-text-muted text-sm',
close: 'absolute top-4 end-4'
},
variants: {
side: {
top: {
content: ''
},
right: {
content: 'sm:max-w-md'
},
bottom: {
content: ''
},
left: {
content: 'sm:max-w-md'
}
},
inset: {
true: {
content: 'rounded-lg'
}
},
transition: {
true: {
overlay: 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0'
}
}
},
compoundVariants: [
{
side: 'top',
inset: true,
class: {
content: 'max-h-[calc(100%-2rem)] inset-x-4 top-4'
}
},
{
side: 'top',
inset: false,
class: {
content: 'max-h-full inset-x-0 top-0'
}
},
{
side: 'right',
inset: true,
class: {
content: 'w-[calc(100%-2rem)] inset-y-4 right-4'
}
},
{
side: 'right',
inset: false,
class: {
content: 'w-full inset-y-0 right-0'
}
},
{
side: 'bottom',
inset: true,
class: {
content: 'max-h-[calc(100%-2rem)] inset-x-4 bottom-4'
}
},
{
side: 'bottom',
inset: false,
class: {
content: 'max-h-full inset-x-0 bottom-0'
}
},
{
side: 'left',
inset: true,
class: {
content: 'w-[calc(100%-2rem)] inset-y-4 left-4'
}
},
{
side: 'left',
inset: false,
class: {
content: 'w-full inset-y-0 left-0'
}
},
{
transition: true,
side: 'top',
class: {
content: 'data-[state=open]:animate-in data-[state=open]:slide-in-from-top data-[state=closed]:animate-out data-[state=closed]:slide-out-to-top'
}
},
{
transition: true,
side: 'right',
class: {
content: 'data-[state=open]:animate-in data-[state=open]:slide-in-from-right data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right'
}
},
{
transition: true,
side: 'bottom',
class: {
content: 'data-[state=open]:animate-in data-[state=open]:slide-in-from-bottom data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom'
}
},
{
transition: true,
side: 'left',
class: {
content: 'data-[state=open]:animate-in data-[state=open]:slide-in-left data-[state=closed]:animate-out data-[state=closed]:slide-out-left'
}
}
]
}
}
})import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from 'pohon-ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
slideover: {
slots: {
overlay: 'fixed inset-0 bg-background-elevated/75',
content: 'bg-background flex flex-col ring-border fixed divide-border divide-y focus:outline-none sm:ring sm:shadow-lg',
header: 'p-4 flex gap-1.5 min-h-$ui-header-height items-center sm:px-6',
wrapper: '',
body: 'flex-1 overflow-y-auto p-4 sm:p-6',
footer: 'flex items-center gap-1.5 p-4 sm:px-6',
title: 'color-text-highlighted font-600',
description: 'mt-1 color-text-muted text-sm',
close: 'absolute top-4 end-4'
},
variants: {
side: {
top: {
content: ''
},
right: {
content: 'sm:max-w-md'
},
bottom: {
content: ''
},
left: {
content: 'sm:max-w-md'
}
},
inset: {
true: {
content: 'rounded-lg'
}
},
transition: {
true: {
overlay: 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0'
}
}
},
compoundVariants: [
{
side: 'top',
inset: true,
class: {
content: 'max-h-[calc(100%-2rem)] inset-x-4 top-4'
}
},
{
side: 'top',
inset: false,
class: {
content: 'max-h-full inset-x-0 top-0'
}
},
{
side: 'right',
inset: true,
class: {
content: 'w-[calc(100%-2rem)] inset-y-4 right-4'
}
},
{
side: 'right',
inset: false,
class: {
content: 'w-full inset-y-0 right-0'
}
},
{
side: 'bottom',
inset: true,
class: {
content: 'max-h-[calc(100%-2rem)] inset-x-4 bottom-4'
}
},
{
side: 'bottom',
inset: false,
class: {
content: 'max-h-full inset-x-0 bottom-0'
}
},
{
side: 'left',
inset: true,
class: {
content: 'w-[calc(100%-2rem)] inset-y-4 left-4'
}
},
{
side: 'left',
inset: false,
class: {
content: 'w-full inset-y-0 left-0'
}
},
{
transition: true,
side: 'top',
class: {
content: 'data-[state=open]:animate-in data-[state=open]:slide-in-from-top data-[state=closed]:animate-out data-[state=closed]:slide-out-to-top'
}
},
{
transition: true,
side: 'right',
class: {
content: 'data-[state=open]:animate-in data-[state=open]:slide-in-from-right data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right'
}
},
{
transition: true,
side: 'bottom',
class: {
content: 'data-[state=open]:animate-in data-[state=open]:slide-in-from-bottom data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom'
}
},
{
transition: true,
side: 'left',
class: {
content: 'data-[state=open]:animate-in data-[state=open]:slide-in-left data-[state=closed]:animate-out data-[state=closed]:slide-out-left'
}
}
]
}
}
})
]
})