Slideover

Une boîte de dialogue qui glisse de n'importe quel côté de l'écran.

Utilisation

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

Ensuite, utilisez l'emplacement #content pour ajouter le contenu affiché lorsque le Slideover est ouvert.

<template>
  <USlideover>
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="h-full m-4" />
    </template>
  </USlideover>
</template>

Vous pouvez également utiliser les emplacements #header, #body et #footer pour personnaliser le contenu du Slideover.

Titre

Utilisez la prop title pour définir le titre de l'en-tête du Slideover.

<template>
  <USlideover title="Slideover with title">
    <UButton label="Open" color="neutral" variant="subtle" />

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

Description

Utilisez la prop description pour définir la description de l'en-tête du Slideover.

<template>
  <USlideover
    title="Slideover with description"
    description="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
  >
    <UButton label="Open" color="neutral" variant="subtle" />

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

Fermeture

Utilisez la prop close pour personnaliser ou masquer le bouton de fermeture (avec la valeur false) affiché dans l'en-tête du Slideover.

Puede pasar cualquier propiedad del componente Button para personalizarlo.

<template>
  <USlideover
    title="Slideover with close button"
    :close="{
      color: 'primary',
      variant: 'outline',
      class: 'rounded-full'
    }"
  >
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </USlideover>
</template>
El botón de cierre no se muestra si se utiliza la ranura #content, ya que es parte del encabezado.

Fermer l'icône

Utilisez la prop close-icon pour personnaliser le bouton de fermeture Icon.

<template>
  <USlideover title="Slideover with close button" close-icon="i-lucide-arrow-right">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </USlideover>
</template>
Vous pouvez personnaliser cette icône globalement dans votre app.config.ts sous la touche ui.icons.close.
Vous pouvez personnaliser cette icône globalement dans votre vite.config.ts sous la touche ui.icons.close.

Side

Utilisez le prop side pour définir le côté de l'écran où le Slideover va glisser de. Defaults à right.

<template>
  <USlideover side="left" title="Slideover with side">
    <UButton label="Open" color="neutral" variant="subtle" />

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

Inset: badge{label="4.3+" class="align-text-top"}

Utilisez le prop inset pour insérer le Slideover depuis les bords.

<template>
  <USlideover side="right" inset title="Slideover with inset">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="min-w-96 min-h-96 size-full" />
    </template>
  </USlideover>
</template>

Transition

Utilisez la prop transition pour contrôler si le Slideover est animé ou non.

<template>
  <USlideover :transition="false" title="Slideover without transition">
    <UButton label="Open" color="neutral" variant="subtle" />

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

Overlay

Utilisez la prop overlay pour contrôler si le Slideover a une superposition ou non. Par défaut, true.

<template>
  <USlideover :overlay="false" title="Slideover without overlay">
    <UButton label="Open" color="neutral" variant="subtle" />

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

Modale

Utilisez la prop modal pour contrôler si le Slideover bloque l'interaction avec le contenu extérieur.

Lorsque modal est défini sur false, la superposition est automatiquement désactivée et le contenu extérieur devient interactif.
<template>
  <USlideover :modal="false" title="Slideover interactive">
    <UButton label="Open" color="neutral" variant="subtle" />

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

Dismissible

Utilisez la prop dismissible pour contrôler si le Slideover est éliminable lorsque vous cliquez en dehors de celui-ci ou appuyez sur escape.

Un événement close:prevent sera émis lorsque l'utilisateur essaiera de le fermer.
Vous pouvez combiner modal: false avec dismissible: false pour rendre l'arrière-plan du Slideover interactif sans le fermer.
<template>
  <USlideover :dismissible="false" title="Slideover non-dismissible">
    <UButton label="Open" color="neutral" variant="subtle" />

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

Unmount: badge{label="4.10+" class="align-text-top"}

Utilisez la prop unmount-on-hide pour empêcher que le contenu du Slideover ne soit démonté lorsqu 'il est fermé. Par défaut, true.

<template>
  <USlideover :unmount-on-hide="false" title="Slideover">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </USlideover>
</template>
Vous pouvez inspecter le DOM pour voir le contenu du Slideover en cours de rendu même lorsqu 'il est fermé.
Lorsque la prop portal est définie sur false, le contenu est également rendu sur le serveur. Ceci est utile pour rendre un slideover ouvert pendant SSR sans flash sur le chargement de la page, ou pour exposer son contenu pour le référencement.

Exemples

Control état ouvert

Vous pouvez contrôler l'état ouvert en utilisant la prop default-open ou la directive v-model:open.

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

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

<template>
  <USlideover v-model:open="open">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="h-full m-4" />
    </template>
  </USlideover>
</template>
<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)

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

<template>
  <USlideover v-model:open="open">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="h-full m-4" />
    </template>
  </USlideover>
</template>
Dans cet exemple, en utilisant defineShortcuts, vous pouvez basculer le Slideover en appuyant sur: kbd{value="O"}.
Cela vous permet de déplacer le déclencheur en dehors du Slideover ou de le supprimer complètement.

Utilisation programmatique

Vous pouvez utiliser le composable useOverlay pour ouvrir un Slideover par programmation.

Assurez-vous d'envelopper votre application avec le composant App qui utilise le composant OverlayProvider.

Tout d'abord, créez un composant de glissement qui sera ouvert par programme:

SlideoverExample.vue
<script setup lang="ts">
defineProps<{
  count: number
}>()

const emit = defineEmits<{ close: [boolean] }>()
</script>

<template>
  <USlideover
    :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">
        <UButton color="neutral" label="Dismiss" @click="emit('close', false)" />
        <UButton label="Success" @click="emit('close', true)" />
      </div>
    </template>
  </USlideover>
</template>
Nous émettons un événement close lorsque le slideover est fermé ou rejeté ici. Vous pouvez émettre n'importe quelle donnée via l'événement close, et ces données deviennent la valeur résolue de open(). L'événement doit être émis pour que la promesse se résolve.

Ensuite, utilisez-le dans votre 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>
  <UButton 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>
  <UButton label="Open" color="neutral" variant="subtle" @click="open" />
</template>
Vous pouvez fermer le coulissant dans le composant coulissant en émettant emit('close').

Slide-overs imbriqués

Vous pouvez imbriquer des glissières les unes dans les autres.

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

<template>
  <USlideover v-model:open="first" title="First slideover" :ui="{ footer: 'justify-end' }">
    <UButton color="neutral" variant="subtle" label="Open" />

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

    <template #footer>
      <UButton label="Close" color="neutral" variant="outline" @click="first = false" />

      <USlideover v-model:open="second" title="Second slideover" :ui="{ footer: 'justify-end' }">
        <UButton label="Open second" color="neutral" />

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

        <template #footer>
          <UButton label="Close" color="neutral" variant="outline" @click="second = false" />
        </template>
      </USlideover>
    </template>
  </USlideover>
</template>
<script setup lang="ts">
import { ref } from 'vue'

const first = ref(false)
const second = ref(false)
</script>

<template>
  <USlideover v-model:open="first" title="First slideover" :ui="{ footer: 'justify-end' }">
    <UButton color="neutral" variant="subtle" label="Open" />

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

    <template #footer>
      <UButton label="Close" color="neutral" variant="outline" @click="first = false" />

      <USlideover v-model:open="second" title="Second slideover" :ui="{ footer: 'justify-end' }">
        <UButton label="Open second" color="neutral" />

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

        <template #footer>
          <UButton label="Close" color="neutral" variant="outline" @click="second = false" />
        </template>
      </USlideover>
    </template>
  </USlideover>
</template>

With slot de pied de page

Utilisez le slot #footer pour ajouter du contenu après le corps du Slideover.

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

<template>
  <USlideover v-model:open="open" title="Slideover with footer" description="This is useful when you want a form in a Slideover." :ui="{ footer: 'justify-end' }">
    <UButton label="Open" color="neutral" variant="subtle" />

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

    <template #footer="{ close }">
      <UButton label="Cancel" color="neutral" variant="outline" @click="close" />
      <UButton label="Submit" color="neutral" />
    </template>
  </USlideover>
</template>
<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)
</script>

<template>
  <USlideover v-model:open="open" title="Slideover with footer" description="This is useful when you want a form in a Slideover." :ui="{ footer: 'justify-end' }">
    <UButton label="Open" color="neutral" variant="subtle" />

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

    <template #footer="{ close }">
      <UButton label="Cancel" color="neutral" variant="outline" @click="close" />
      <UButton label="Submit" color="neutral" />
    </template>
  </USlideover>
</template>

API équipement

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'
"right" | "top" | "bottom" | "left"

The side of the slideover.

inset
false
boolean

Whether to inset the slideover from the edges.

portal
true
string | false | true | HTMLElement

Render the slideover in a portal.

close
true
boolean | Omit<ButtonProps, LinkPropsKeys>

Display a close button to dismiss the slideover. { size: 'md', color: 'neutral', variant: 'ghost' }

closeIcon
appConfig.ui.icons.close
any

The icon displayed in the close button.

dismissible
true
boolean

When false, the slideover will not close when clicking outside or pressing escape.

open
boolean

The controlled open state of the dialog. Can be binded as v-model:open.

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
true
boolean

The modality of the dialog When set to true,
interaction with outside elements will be disabled and only dialog content will be visible to screen readers.

unmountOnHide
true
boolean

When set to false, the dialog content will not be unmounted when closed, but instead hidden with CSS.
Useful for SEO or when you want to improve performance by not remounting the component on every open.

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
leave
[]
after:leave
[]
enter
[]
after:enter
[]
close:prevent
[]
update:open
[value: boolean]

Thème

app.config.ts
export default defineAppConfig({
  ui: {
    slideover: {
      slots: {
        overlay: 'fixed inset-0 bg-elevated/75',
        content: 'fixed bg-default divide-y divide-default sm:ring ring-default sm:shadow-lg flex flex-col focus:outline-none',
        header: 'flex items-center gap-1.5 p-4 sm:px-6 min-h-(--ui-header-height)',
        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: 'text-highlighted font-semibold',
        description: 'mt-1 text-muted text-sm',
        close: 'absolute top-4 end-4'
      },
      variants: {
        side: {
          top: {
            content: ''
          },
          right: {
            content: 'max-w-md'
          },
          bottom: {
            content: ''
          },
          left: {
            content: 'max-w-md'
          }
        },
        inset: {
          true: {
            content: 'rounded-lg'
          }
        },
        transition: {
          true: {
            overlay: 'data-[state=open]:animate-[fade-in_200ms_var(--ease-out)] data-[state=closed]:animate-[fade-out_200ms_var(--ease-out)]'
          }
        }
      },
      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-[slide-in-from-top_200ms_var(--ease-out)] data-[state=closed]:animate-[slide-out-to-top_200ms_var(--ease-out)]'
          }
        },
        {
          transition: true,
          side: 'right',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-right_200ms_var(--ease-out)] data-[state=closed]:animate-[slide-out-to-right_200ms_var(--ease-out)]'
          }
        },
        {
          transition: true,
          side: 'bottom',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-bottom_200ms_var(--ease-out)] data-[state=closed]:animate-[slide-out-to-bottom_200ms_var(--ease-out)]'
          }
        },
        {
          transition: true,
          side: 'left',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-left_200ms_var(--ease-out)] data-[state=closed]:animate-[slide-out-to-left_200ms_var(--ease-out)]'
          }
        }
      ]
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        slideover: {
          slots: {
            overlay: 'fixed inset-0 bg-elevated/75',
            content: 'fixed bg-default divide-y divide-default sm:ring ring-default sm:shadow-lg flex flex-col focus:outline-none',
            header: 'flex items-center gap-1.5 p-4 sm:px-6 min-h-(--ui-header-height)',
            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: 'text-highlighted font-semibold',
            description: 'mt-1 text-muted text-sm',
            close: 'absolute top-4 end-4'
          },
          variants: {
            side: {
              top: {
                content: ''
              },
              right: {
                content: 'max-w-md'
              },
              bottom: {
                content: ''
              },
              left: {
                content: 'max-w-md'
              }
            },
            inset: {
              true: {
                content: 'rounded-lg'
              }
            },
            transition: {
              true: {
                overlay: 'data-[state=open]:animate-[fade-in_200ms_var(--ease-out)] data-[state=closed]:animate-[fade-out_200ms_var(--ease-out)]'
              }
            }
          },
          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-[slide-in-from-top_200ms_var(--ease-out)] data-[state=closed]:animate-[slide-out-to-top_200ms_var(--ease-out)]'
              }
            },
            {
              transition: true,
              side: 'right',
              class: {
                content: 'data-[state=open]:animate-[slide-in-from-right_200ms_var(--ease-out)] data-[state=closed]:animate-[slide-out-to-right_200ms_var(--ease-out)]'
              }
            },
            {
              transition: true,
              side: 'bottom',
              class: {
                content: 'data-[state=open]:animate-[slide-in-from-bottom_200ms_var(--ease-out)] data-[state=closed]:animate-[slide-out-to-bottom_200ms_var(--ease-out)]'
              }
            },
            {
              transition: true,
              side: 'left',
              class: {
                content: 'data-[state=open]:animate-[slide-in-from-left_200ms_var(--ease-out)] data-[state=closed]:animate-[slide-out-to-left_200ms_var(--ease-out)]'
              }
            }
          ]
        }
      }
    })
  ]
})

Changelog écrit.

No recent changes