Slideover

DialogGitLab
Um diálogo que desliza a partir de qualquer lado da tela.

Uso

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

Depois, use o slot #content para adicionar o conteúdo exibido quando o Slideover está aberto.

<template>
  <NSlideover>
    <NButton label="Open" color="neutral" variant="subtle" />

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

Você também pode usar os slots #header, #body e #footer para personalizar o conteúdo do Slideover.

Título

Use a prop title para definir o título do cabeçalho do Slideover.

<template>
  <NSlideover title="Slideover with title">
    <NButton label="Open" color="neutral" variant="subtle" />

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

Descrição

Use a prop description para definir a descrição do cabeçalho do Slideover.

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

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

Fechar

Use a prop close para personalizar ou ocultar o botão de fechar (com o valor false) exibido no cabeçalho do Slideover.

Você pode passar qualquer propriedade do componente Button para personalizá-lo.

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

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </NSlideover>
</template>
O botão de fechar não é exibido se o slot #content for usado, pois ele faz parte do cabeçalho.

Ícone de fechar

Use a prop close-icon para personalizar o Icon do botão de fechar. O padrão é i-lucide-x.

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

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </NSlideover>
</template>
Você pode personalizar esse ícone globalmente no seu app.config.ts na chave ui.icons.close.
Você pode personalizar esse ícone globalmente no seu vite.config.ts na chave ui.icons.close.

Lado

Use a prop side para definir o lado da tela de onde o Slideover vai deslizar. O padrão é right.

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

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

Inset 4.3+

Use a prop inset para recuar o Slideover em relação às bordas.

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

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

Transição

Use a prop transition para controlar se o Slideover é animado ou não. O padrão é true.

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

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

Overlay

Use a prop overlay para controlar se o Slideover tem um overlay ou não. O padrão é true.

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

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

Use a prop modal para controlar se o Slideover bloqueia a interação com o conteúdo externo. O padrão é true.

Quando modal é definido como false, o overlay é automaticamente desabilitado e o conteúdo externo se torna interativo.
<template>
  <NSlideover :modal="false" title="Slideover interactive">
    <NButton label="Open" color="neutral" variant="subtle" />

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

Dispensável

Use a prop dismissible para controlar se o Slideover pode ser dispensado ao clicar fora dele ou pressionar escape. O padrão é true.

Um evento close:prevent será emitido quando o usuário tentar fechá-lo.
Você pode combinar modal: false com dismissible: false para tornar o fundo do Slideover interativo sem fechá-lo.
<template>
  <NSlideover :dismissible="false" title="Slideover non-dismissible">
    <NButton label="Open" color="neutral" variant="subtle" />

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

Desmontar Soon

Use a prop unmount-on-hide para evitar que o conteúdo do Slideover seja desmontado quando ele é fechado. O padrão é true.

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

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </NSlideover>
</template>
Você pode inspecionar o DOM para ver o conteúdo do Slideover sendo renderizado mesmo enquanto ele está fechado.
Quando a prop portal é definida como false, o conteúdo também é renderizado no servidor. Isso é útil para renderizar um Slideover aberto durante o SSR sem uma piscada no carregamento da página, ou para expor seu conteúdo para SEO.

Exemplos

Controlar o estado de abertura

Você pode controlar o estado de abertura usando a prop default-open ou a diretiva v-model:open.

Neste exemplo, aproveitando o defineShortcuts, você pode abrir e fechar o Slideover pressionando O.
Isso permite mover o gatilho para fora do Slideover ou removê-lo por completo.

Uso programático

Você pode usar o composable useOverlay para abrir um Slideover programaticamente.

Certifique-se de envolver seu app com o componente App, que usa o componente OverlayProvider.

Primeiro, crie um componente de slideover que será aberto programaticamente:

undefined.vue
Aqui emitimos um evento close quando o slideover é fechado ou dispensado. Você pode emitir qualquer dado por meio do evento close, porém o evento precisa ser emitido para que o valor de retorno seja capturado.

Depois, use-o no seu app:

Você pode fechar o slideover dentro do componente de slideover emitindo emit('close').

Slideovers aninhados

Você pode aninhar slideovers uns dentro dos outros.

Com slot de rodapé

Use o slot #footer para adicionar conteúdo após o corpo do Slideover.

API

Props

Prop Default Type
title string
description string
content DialogContentProps & Partial<EmitsToProps<DialogContentImplEmits>>

The content of the slideover.

overlaytrueboolean

Render an overlay behind the slideover.

transitiontrueboolean

Animate the slideover when opening or closing.

side'right' "right" | "top" | "bottom" | "left"

The side of the slideover.

insetfalseboolean

Whether to inset the slideover from the edges.

portaltrue string | false | true | HTMLElement

Render the slideover in a portal.

closetrueboolean | Omit<ButtonProps, LinkPropsKeys>

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

closeIconappConfig.ui.icons.closeany

The icon displayed in the close button.

dismissibletrueboolean

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

openboolean

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

defaultOpenboolean

The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.

modaltrueboolean

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.

unmountOnHidetrueboolean

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; loading?: 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]

Tema

app.config.ts
export default defineAppConfig({
  ui: {
    slideover: {
      slots: {
        overlay: 'fixed inset-0 bg-secondary/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',
        loading: 'w-full h-full flex justify-center items-center'
      },
      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_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]'
          }
        }
      },
      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_ease-in-out] data-[state=closed]:animate-[slide-out-to-top_200ms_ease-in-out]'
          }
        },
        {
          transition: true,
          side: 'right',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-right_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-right_200ms_ease-in-out]'
          }
        },
        {
          transition: true,
          side: 'bottom',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-bottom_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-bottom_200ms_ease-in-out]'
          }
        },
        {
          transition: true,
          side: 'left',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-left_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-left_200ms_ease-in-out]'
          }
        }
      ]
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nitro/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        slideover: {
          slots: {
            overlay: 'fixed inset-0 bg-secondary/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',
            loading: 'w-full h-full flex justify-center items-center'
          },
          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_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]'
              }
            }
          },
          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_ease-in-out] data-[state=closed]:animate-[slide-out-to-top_200ms_ease-in-out]'
              }
            },
            {
              transition: true,
              side: 'right',
              class: {
                content: 'data-[state=open]:animate-[slide-in-from-right_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-right_200ms_ease-in-out]'
              }
            },
            {
              transition: true,
              side: 'bottom',
              class: {
                content: 'data-[state=open]:animate-[slide-in-from-bottom_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-bottom_200ms_ease-in-out]'
              }
            },
            {
              transition: true,
              side: 'left',
              class: {
                content: 'data-[state=open]:animate-[slide-in-from-left_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-left_200ms_ease-in-out]'
              }
            }
          ]
        }
      }
    })
  ]
})

Changelog

No recent changes