Sidebar

GitLab
Uma sidebar recolhível com várias variantes visuais.

Uso

O componente Sidebar é uma sidebar fixa e independente que empurra o conteúdo da página. No desktop, ela é renderizada inline e pode ser recolhida; no mobile, ela abre um componente Modal, Slideover ou Drawer.

Sidebar vs DashboardSidebar: This component is a simple, standalone sidebar you can drop anywhere (chat panel, settings, navigation). If you need drag-to-resize, state persistence and integration with DashboardGroup, use DashboardSidebar instead.

Use os slots header, default e footer para personalizar o conteúdo da sidebar. A diretiva v-model:open é sensível à viewport: no desktop ela controla o estado expandido/recolhido, no mobile ela controla o menu.

Variante

Use a prop variant para alterar o estilo visual da sidebar. O padrão é sidebar.

Recolhível

Use a prop collapsible para alterar o comportamento de recolhimento da sidebar. O padrão é offcanvas.

  • offcanvas: The sidebar slides out of view completely.
  • icon: The sidebar shrinks to icon-only width.
  • none: The sidebar is not collapsible.
Você pode acessar o state nas props de slot para personalizar o conteúdo da sidebar quando ela está recolhida.

Lado

Use a prop side para alterar o lado da sidebar. O padrão é left.

Título

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

<template>
  <NSidebar title="Navigation">
    <Placeholder class="h-full" />
  </NSidebar>
</template>

Descrição

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

<template>
  <NSidebar title="Navigation" description="Browse your workspace">
    <Placeholder class="h-full" />
  </NSidebar>
</template>

Trilho

Use a prop rail para exibir uma borda interativa fina na sidebar que alterna o estado recolhido ao clicar. O trilho só é renderizado quando collapsible não é none.

<template>
  <NSidebar rail collapsible="icon" title="Navigation">
    <Placeholder class="h-full" />
  </NSidebar>
</template>

Fechar

Use a prop close para exibir um botão de fechar no cabeçalho da sidebar. O botão de fechar só é renderizado quando collapsible não é none.

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

<template>
  <NSidebar close rail collapsible="icon" title="Navigation">
    <Placeholder class="h-full" />
  </NSidebar>
</template>

Ícone de fechar

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

<template>
  <NSidebar
    close
    close-icon="i-lucide-panel-right-close"
    rail
    collapsible="icon"
    side="right"
    title="Navigation"
  >
    <Placeholder class="h-full" />
  </NSidebar>
</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.

Modo

Use a prop mode para alterar o modo do menu da sidebar no mobile. O padrão é slideover.

Você pode usar a prop menu para personalizar o menu da sidebar; ele se adapta dependendo do modo que você escolher.

Exemplos

Controlar o estado de abertura

Você pode controlar o estado de abertura usando a prop open ou a diretiva v-model:open. No desktop ela controla o estado expandido/recolhido, no mobile ela abre/fecha o menu deslizante.

Neste exemplo, aproveitando o defineShortcuts, você pode alternar o estado de abertura da Sidebar pressionando O.

Persistir o estado de abertura

Use useLocalStorage do VueUse ou useCookie em vez de ref para persistir o estado da sidebar entre recarregamentos de página.

A única diferença em relação ao exemplo anterior é substituir ref(true) por useLocalStorage('sidebar-open', true).

Com largura personalizada

A largura da sidebar é controlada pela variável CSS --sidebar-width (o padrão é 16rem). A largura do ícone recolhido é controlada por --sidebar-width-icon (o padrão é 4rem).

Sobrescreva-as globalmente no seu CSS ou por instância com o atributo style.

Com cabeçalho

Para posicionar a sidebar abaixo de um Header, personalize o gap e o container usando a prop ui.

A variável --ui-header-height tem o padrão 4rem e é usada pelo Header. Ajuste-a se a sua navbar usar uma altura diferente.

Com chat de IA

Use a sidebar no lado direito com ChatMessages e ChatPrompt para criar um painel de chat de IA.

API

Props

Prop Default Type
as'aside'any

The element or component this component should render as.

variant'sidebar' "floating" | "sidebar" | "inset"

The visual variant of the sidebar.

collapsible'offcanvas' "offcanvas" | "icon" | "none"

The collapse behavior of the sidebar.

  • offcanvas: The sidebar slides out of view completely.
  • icon: The sidebar shrinks to icon-only width.
  • none: The sidebar is not collapsible.
side'left' "left" | "right"

The side to render the sidebar on.

title string

The title displayed in the sidebar header.

description string

The description displayed in the sidebar header.

closefalseboolean | Omit<ButtonProps, LinkPropsKeys>

Display a close button to collapse the sidebar. Only renders when collapsible is not none. { size: 'md', color: 'neutral', variant: 'ghost' }

closeIconappConfig.ui.icons.closeany

The icon displayed in the close button.

railfalseboolean

Display a rail on the sidebar edge to toggle collapse. Only renders when collapsible is not none.

transitiontrueboolean

Animate the sidebar when collapsing or expanding.

mode'slideover' T

The mode of the sidebar menu on mobile.

menu SidebarMenu<T>

The props for the sidebar menu component on mobile.

opentrueboolean
ui { root?: SlotClass; gap?: SlotClass; container?: SlotClass; inner?: SlotClass; header?: SlotClass; wrapper?: SlotClass; title?: SlotClass; description?: SlotClass; actions?: SlotClass; close?: SlotClass; body?: SlotClass; footer?: SlotClass; rail?: SlotClass; }

Slots

Slot Type
header{ state: SidebarState; open: boolean; close: () => void; }
title{ state: SidebarState; }
description{ state: SidebarState; }
actions{ state: SidebarState; }
close{ ui: object; }
default{ state: SidebarState; open: boolean; close: () => void; }
footer{ state: SidebarState; open: boolean; close: () => void; }
rail{ ui: object; }
content{ close: () => void; }

Tema

app.config.ts
export default defineAppConfig({
  ui: {
    sidebar: {
      slots: {
        root: 'peer [--sidebar-width:16rem] [--sidebar-width-icon:4rem]',
        gap: 'relative w-(--sidebar-width) bg-transparent',
        container: 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) lg:flex',
        inner: 'flex size-full flex-col overflow-hidden divide-y divide-default',
        header: 'flex items-center gap-1.5 overflow-hidden px-4 min-h-(--ui-header-height)',
        wrapper: 'min-w-0 flex-1',
        title: 'text-highlighted font-semibold truncate',
        description: 'text-muted text-sm truncate',
        actions: 'flex items-center gap-1.5 shrink-0',
        close: '',
        body: 'flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4',
        footer: 'flex items-center gap-1.5 overflow-hidden p-4',
        rail: [
          'absolute inset-y-0 z-20 hidden w-4 after:absolute after:inset-y-0 after:left-1/2 after:w-px lg:flex hover:after:bg-(--ui-border-accented)',
          'after:transition-colors'
        ]
      },
      variants: {
        transition: {
          true: {
            gap: 'transition-[width] duration-200 ease-out',
            container: 'transition-[left,right,width] duration-200 ease-out',
            rail: 'transition-all ease-out'
          }
        },
        side: {
          left: {
            container: 'left-0 border-e border-default',
            rail: 'end-0 translate-x-1/2'
          },
          right: {
            container: 'right-0 border-s border-default',
            rail: '-start-px -translate-x-1/2'
          }
        },
        collapsible: {
          offcanvas: {
            root: 'group/sidebar hidden lg:block',
            gap: 'data-[state=collapsed]:w-0'
          },
          icon: {
            root: 'group/sidebar hidden lg:block',
            gap: 'data-[state=collapsed]:w-(--sidebar-width-icon)',
            container: 'data-[state=collapsed]:w-(--sidebar-width-icon)',
            actions: 'group-data-[state=collapsed]/sidebar:hidden',
            body: 'group-data-[state=collapsed]/sidebar:overflow-hidden'
          },
          none: {
            root: 'h-full w-(--sidebar-width)'
          }
        },
        variant: {
          sidebar: {},
          floating: {
            container: 'p-4 border-transparent',
            inner: 'rounded-lg ring ring-default shadow-lg',
            rail: 'inset-y-4'
          },
          inset: {
            container: 'py-4 border-transparent',
            inner: 'divide-transparent',
            rail: 'inset-y-4'
          }
        }
      },
      compoundVariants: [
        {
          side: 'left',
          collapsible: [
            'offcanvas',
            'icon'
          ],
          class: {
            rail: 'cursor-w-resize data-[state=collapsed]:cursor-e-resize'
          }
        },
        {
          side: 'right',
          collapsible: [
            'offcanvas',
            'icon'
          ],
          class: {
            rail: 'cursor-e-resize data-[state=collapsed]:cursor-w-resize'
          }
        },
        {
          side: 'left',
          collapsible: 'none',
          class: {
            root: 'border-e border-default'
          }
        },
        {
          side: 'right',
          collapsible: 'none',
          class: {
            root: 'border-s border-default'
          }
        },
        {
          side: 'left',
          collapsible: 'offcanvas',
          class: {
            container: 'data-[state=collapsed]:-left-(--sidebar-width)'
          }
        },
        {
          side: 'right',
          collapsible: 'offcanvas',
          class: {
            container: 'data-[state=collapsed]:-right-(--sidebar-width)'
          }
        },
        {
          variant: 'floating',
          collapsible: 'icon',
          class: {
            gap: 'data-[state=collapsed]:w-[calc(var(--sidebar-width-icon)+--spacing(8))]',
            container: 'data-[state=collapsed]:w-[calc(var(--sidebar-width-icon)+--spacing(8)+2px)]'
          }
        },
        {
          variant: 'floating',
          collapsible: 'none',
          class: {
            root: 'p-4 border-0'
          }
        },
        {
          variant: 'inset',
          collapsible: 'none',
          class: {
            root: 'py-4 border-0'
          }
        },
        {
          variant: 'floating',
          side: 'left',
          class: {
            rail: 'end-4'
          }
        },
        {
          variant: 'floating',
          side: 'right',
          class: {
            rail: 'start-[calc(--spacing(4)-1px)]'
          }
        }
      ]
    }
  }
})
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: {
        sidebar: {
          slots: {
            root: 'peer [--sidebar-width:16rem] [--sidebar-width-icon:4rem]',
            gap: 'relative w-(--sidebar-width) bg-transparent',
            container: 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) lg:flex',
            inner: 'flex size-full flex-col overflow-hidden divide-y divide-default',
            header: 'flex items-center gap-1.5 overflow-hidden px-4 min-h-(--ui-header-height)',
            wrapper: 'min-w-0 flex-1',
            title: 'text-highlighted font-semibold truncate',
            description: 'text-muted text-sm truncate',
            actions: 'flex items-center gap-1.5 shrink-0',
            close: '',
            body: 'flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4',
            footer: 'flex items-center gap-1.5 overflow-hidden p-4',
            rail: [
              'absolute inset-y-0 z-20 hidden w-4 after:absolute after:inset-y-0 after:left-1/2 after:w-px lg:flex hover:after:bg-(--ui-border-accented)',
              'after:transition-colors'
            ]
          },
          variants: {
            transition: {
              true: {
                gap: 'transition-[width] duration-200 ease-out',
                container: 'transition-[left,right,width] duration-200 ease-out',
                rail: 'transition-all ease-out'
              }
            },
            side: {
              left: {
                container: 'left-0 border-e border-default',
                rail: 'end-0 translate-x-1/2'
              },
              right: {
                container: 'right-0 border-s border-default',
                rail: '-start-px -translate-x-1/2'
              }
            },
            collapsible: {
              offcanvas: {
                root: 'group/sidebar hidden lg:block',
                gap: 'data-[state=collapsed]:w-0'
              },
              icon: {
                root: 'group/sidebar hidden lg:block',
                gap: 'data-[state=collapsed]:w-(--sidebar-width-icon)',
                container: 'data-[state=collapsed]:w-(--sidebar-width-icon)',
                actions: 'group-data-[state=collapsed]/sidebar:hidden',
                body: 'group-data-[state=collapsed]/sidebar:overflow-hidden'
              },
              none: {
                root: 'h-full w-(--sidebar-width)'
              }
            },
            variant: {
              sidebar: {},
              floating: {
                container: 'p-4 border-transparent',
                inner: 'rounded-lg ring ring-default shadow-lg',
                rail: 'inset-y-4'
              },
              inset: {
                container: 'py-4 border-transparent',
                inner: 'divide-transparent',
                rail: 'inset-y-4'
              }
            }
          },
          compoundVariants: [
            {
              side: 'left',
              collapsible: [
                'offcanvas',
                'icon'
              ],
              class: {
                rail: 'cursor-w-resize data-[state=collapsed]:cursor-e-resize'
              }
            },
            {
              side: 'right',
              collapsible: [
                'offcanvas',
                'icon'
              ],
              class: {
                rail: 'cursor-e-resize data-[state=collapsed]:cursor-w-resize'
              }
            },
            {
              side: 'left',
              collapsible: 'none',
              class: {
                root: 'border-e border-default'
              }
            },
            {
              side: 'right',
              collapsible: 'none',
              class: {
                root: 'border-s border-default'
              }
            },
            {
              side: 'left',
              collapsible: 'offcanvas',
              class: {
                container: 'data-[state=collapsed]:-left-(--sidebar-width)'
              }
            },
            {
              side: 'right',
              collapsible: 'offcanvas',
              class: {
                container: 'data-[state=collapsed]:-right-(--sidebar-width)'
              }
            },
            {
              variant: 'floating',
              collapsible: 'icon',
              class: {
                gap: 'data-[state=collapsed]:w-[calc(var(--sidebar-width-icon)+--spacing(8))]',
                container: 'data-[state=collapsed]:w-[calc(var(--sidebar-width-icon)+--spacing(8)+2px)]'
              }
            },
            {
              variant: 'floating',
              collapsible: 'none',
              class: {
                root: 'p-4 border-0'
              }
            },
            {
              variant: 'inset',
              collapsible: 'none',
              class: {
                root: 'py-4 border-0'
              }
            },
            {
              variant: 'floating',
              side: 'left',
              class: {
                rail: 'end-4'
              }
            },
            {
              variant: 'floating',
              side: 'right',
              class: {
                rail: 'start-[calc(--spacing(4)-1px)]'
              }
            }
          ]
        }
      }
    })
  ]
})

Changelog

No recent changes