+
diff --git a/docs/app/components/search/Search.vue b/docs/app/components/search/Search.vue
index d7375ead63..ca6d8fdd9b 100644
--- a/docs/app/components/search/Search.vue
+++ b/docs/app/components/search/Search.vue
@@ -14,7 +14,7 @@ defineProps<{
navigation?: ContentNavigationItem[]
}>()
-const { links, groups, fullscreen, chat, searchTerm, messages } = useSearch()
+const { links, groups, searchTerm } = useSearch()
const { track } = useAnalytics()
watchDebounced(searchTerm, (term) => {
@@ -22,12 +22,6 @@ watchDebounced(searchTerm, (term) => {
track('Search Performed', { term })
}
}, { debounce: 500 })
-
-function onClose() {
- chat.value = false
-
- fullscreen.value = false
-}
@@ -37,11 +31,6 @@ function onClose() {
:files="files"
:groups="groups"
:navigation="navigation"
- :fullscreen="fullscreen"
:fuse="{ resultLimit: 115 }"
- >
-
-
-
-
+ />
diff --git a/docs/app/composables/useChat.ts b/docs/app/composables/useChat.ts
new file mode 100644
index 0000000000..089de4efe7
--- /dev/null
+++ b/docs/app/composables/useChat.ts
@@ -0,0 +1,12 @@
+import type { UIMessage } from 'ai'
+import { createSharedComposable } from '@vueuse/core'
+
+export const useChat = createSharedComposable(() => {
+ const open = ref(false)
+ const messages = ref([])
+
+ return {
+ open,
+ messages
+ }
+})
diff --git a/docs/app/composables/useSearch.ts b/docs/app/composables/useSearch.ts
index a675f40b54..622137b743 100644
--- a/docs/app/composables/useSearch.ts
+++ b/docs/app/composables/useSearch.ts
@@ -1,18 +1,12 @@
-import type { UIMessage } from 'ai'
-
export function useSearch() {
const route = useRoute()
const { frameworks } = useFrameworks()
const { track } = useAnalytics()
+ const { open, messages } = useChat()
- const chat = ref(false)
- const fullscreen = ref(false)
const searchTerm = ref('')
- const messages = ref([])
-
- function onSelect(e: any) {
- e.preventDefault()
+ function onSelect() {
track('AI Chat Opened', { hasSearchTerm: !!searchTerm.value })
messages.value = searchTerm.value
@@ -27,7 +21,7 @@ export function useSearch() {
parts: [{ type: 'text', text: 'Hello, how can I help you today?' }]
}]
- chat.value = true
+ open.value = true
}
const links = computed(() => [!searchTerm.value && {
@@ -134,9 +128,6 @@ export function useSearch() {
return {
links,
groups,
- chat,
- fullscreen,
- searchTerm,
- messages
+ searchTerm
}
}
diff --git a/docs/app/pages/docs/[...slug].vue b/docs/app/pages/docs/[...slug].vue
index 8a17c3be22..3b2f2d8f17 100644
--- a/docs/app/pages/docs/[...slug].vue
+++ b/docs/app/pages/docs/[...slug].vue
@@ -86,16 +86,24 @@ useHead({
]
})
-const communityLinks = computed(() => [{
+const { open, messages } = useChat()
+
+const links = computed(() => [{
icon: 'i-lucide-file-pen',
label: 'Edit this page',
to: `https://github.com/nuxt/ui/edit/v4/docs/content/${page?.value?.stem}.md`,
target: '_blank'
}, {
- icon: 'i-lucide-star',
- label: 'Star on GitHub',
- to: `https://github.com/nuxt/ui`,
- target: '_blank'
+ icon: 'i-lucide-bot',
+ label: 'Explain with AI',
+ onClick: () => {
+ messages.value = [{
+ id: String(Date.now()),
+ role: 'user',
+ parts: [{ type: 'text', text: `Read the documentation page at ${page.value?.path} and summarize it. I want to ask questions about it.` }]
+ }]
+ open.value = true
+ }
}])
@@ -148,11 +156,11 @@ const communityLinks = computed(() => [{
-
+
-
+
diff --git a/docs/content/docs/1.getting-started/5.theme/2.css-variables.md b/docs/content/docs/1.getting-started/5.theme/2.css-variables.md
index b0d5202390..9610a89068 100644
--- a/docs/content/docs/1.getting-started/5.theme/2.css-variables.md
+++ b/docs/content/docs/1.getting-started/5.theme/2.css-variables.md
@@ -373,7 +373,7 @@ Nuxt UI provides a `--ui-header-height` CSS variable that controls the height of
```css
:root {
- --ui-header-height: --spacing(16);
+ --ui-header-height: 4rem;
}
```
diff --git a/docs/content/docs/2.components/dashboard-sidebar.md b/docs/content/docs/2.components/dashboard-sidebar.md
index 9fec5099fe..3b61bd6094 100644
--- a/docs/content/docs/2.components/dashboard-sidebar.md
+++ b/docs/content/docs/2.components/dashboard-sidebar.md
@@ -10,7 +10,13 @@ links:
## Usage
-The DashboardSidebar component is used to display a sidebar. Its state (size, collapsed, etc.) will be saved based on the `storage` and `storage-key` props you provide to the [DashboardGroup](/docs/components/dashboard-group#props) component.
+The DashboardSidebar component is used to display a sidebar in a dashboard layout. It supports drag-to-resize, state persistence and integrates with [DashboardGroup](/docs/components/dashboard-group), [DashboardPanel](/docs/components/dashboard-panel) and [DashboardNavbar](/docs/components/dashboard-navbar).
+
+::tip{to="/docs/components/sidebar"}
+**DashboardSidebar vs Sidebar**: This component is designed for dashboard layouts with drag-to-resize, state persistence and `DashboardGroup` integration. For a simple, standalone sidebar (chat panel, settings, navigation), use [Sidebar](/docs/components/sidebar) instead.
+::
+
+Its state (size, collapsed, etc.) will be saved based on the `storage` and `storage-key` props you provide to the [DashboardGroup](/docs/components/dashboard-group#props) component.
Use it inside the default slot of the [DashboardGroup](/docs/components/dashboard-group) component:
diff --git a/docs/content/docs/2.components/sidebar.md b/docs/content/docs/2.components/sidebar.md
new file mode 100644
index 0000000000..9ac32857cd
--- /dev/null
+++ b/docs/content/docs/2.components/sidebar.md
@@ -0,0 +1,274 @@
+---
+title: Sidebar
+description: 'A collapsible sidebar with multiple visual variants.'
+category: layout
+links:
+ - label: GitHub
+ icon: i-simple-icons-github
+ to: https://github.com/nuxt/ui/blob/v4/src/runtime/components/Sidebar.vue
+navigation.badge: Soon
+---
+
+## Usage
+
+The Sidebar component is a standalone, fixed sidebar that pushes the page content. On desktop, it renders inline and can be collapsed; on mobile, it opens as a sheet (Modal, Slideover or Drawer).
+
+::tip{to="/docs/components/dashboard-sidebar"}
+**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](/docs/components/dashboard-group), use [DashboardSidebar](/docs/components/dashboard-sidebar) instead.
+::
+
+Use the `title`, `description` and `close` props to customize the sidebar header just like the [Modal](/docs/components/modal), [Slideover](/docs/components/slideover) and [Drawer](/docs/components/drawer) components.
+
+Use the `body`, `default` and `footer` slots to customize the sidebar content. The `v-model:open` directive is viewport-aware: on desktop it controls the expanded/collapsed state, on mobile it controls the sheet menu.
+
+::component-example
+---
+collapse: true
+name: 'sidebar-example'
+class: '!p-0 !justify-start'
+overflowHidden: true
+containerClass: 'h-[500px]'
+---
+::
+
+### Variant
+
+Use the `variant` prop to change the visual style of the sidebar. Defaults to `sidebar`.
+
+::component-example
+---
+collapse: true
+name: 'sidebar-example'
+class: '!p-0 !justify-start'
+overflowHidden: true
+containerClass: 'h-[500px]'
+options:
+ - name: 'variant'
+ label: 'variant'
+ default: 'sidebar'
+ items:
+ - sidebar
+ - floating
+ - inset
+---
+::
+
+### Collapsible
+
+Use the `collapsible` prop to change the collapse behavior of the sidebar. Defaults to `none`.
+
+- `offcanvas`: The sidebar slides out of view completely.
+- `icon`: The sidebar shrinks to icon-only width.
+- `none`: The sidebar is not collapsible.
+
+::component-example
+---
+collapse: true
+name: 'sidebar-example'
+class: '!p-0 !justify-start'
+overflowHidden: true
+containerClass: 'h-[500px]'
+options:
+ - name: 'collapsible'
+ label: 'collapsible'
+ default: 'icon'
+ items:
+ - offcanvas
+ - icon
+ - none
+---
+::
+
+::tip{to="#slots"}
+You can access the `state` in the slot props to customize the content of the sidebar when it is collapsed.
+::
+
+### Side
+
+Use the `side` prop to change the side of the sidebar. Defaults to `left`.
+
+::component-example
+---
+collapse: true
+name: 'sidebar-example'
+class: '!p-0 !justify-start'
+overflowHidden: true
+containerClass: 'h-[500px]'
+options:
+ - name: 'side'
+ label: 'side'
+ default: 'left'
+ items:
+ - left
+ - right
+---
+::
+
+### Title
+
+Use the `title` prop to set the title of the sidebar header.
+
+::component-example
+---
+collapse: true
+name: 'sidebar-title-example'
+class: '!p-0 !justify-start'
+overflowHidden: true
+containerClass: 'h-[500px]'
+---
+::
+
+### Description
+
+Use the `description` prop to set the description of the sidebar header.
+
+::component-example
+---
+collapse: true
+name: 'sidebar-description-example'
+class: '!p-0 !justify-start'
+overflowHidden: true
+containerClass: 'h-[500px]'
+---
+::
+
+### Close
+
+Use the `close` prop to display a close button in the sidebar header. The close button is only rendered when `collapsible` is not `none`.
+
+You can pass any property from the [Button](/docs/components/button) component to customize it.
+
+::component-example
+---
+collapse: true
+name: 'sidebar-close-example'
+class: '!p-0 !justify-start'
+overflowHidden: true
+containerClass: 'h-[500px]'
+---
+::
+
+### Close Icon
+
+Use the `close-icon` prop to customize the close button [Icon](/docs/components/icon). Defaults to `i-lucide-x`.
+
+::component-example
+---
+collapse: true
+name: 'sidebar-close-icon-example'
+class: '!p-0 !justify-start'
+overflowHidden: true
+containerClass: 'h-[500px]'
+---
+::
+
+::tip
+You can use the `#title`, `#description` and `#close` slots to customize them.
+::
+
+### Width
+
+The sidebar width is controlled by the `--sidebar-width` CSS variable (defaults to `28rem`). The collapsed icon width is controlled by `--sidebar-width-icon` (defaults to `4rem`).
+
+Override them globally in your CSS or per-instance with the `style` attribute:
+
+```vue
+
+```
+
+### With Navbar
+
+To position the sidebar below a fixed navbar, customize the container position using the `ui` prop:
+
+```vue
+
+```
+
+::note
+The `--ui-header-height` variable defaults to `4rem` and is used by the [Header](/docs/components/header) and [DashboardNavbar](/docs/components/dashboard-navbar) components. Adjust it if your navbar uses a different height.
+::
+
+### Mode
+
+Use the `mode` prop to change the mode of the sidebar menu on mobile. Defaults to `slideover`.
+
+::component-example
+---
+collapse: true
+name: 'sidebar-example'
+class: '!p-0 !justify-start'
+overflowHidden: true
+containerClass: 'h-[500px]'
+options:
+ - name: 'mode'
+ label: 'mode'
+ default: 'slideover'
+ items:
+ - modal
+ - slideover
+ - drawer
+---
+::
+
+::tip{to="#props"}
+You can use the `menu` prop to customize the menu of the sidebar, it will adapt depending on the mode you choose.
+::
+
+## Examples
+
+### Control open state
+
+You can control the open state by using the `open` prop or the `v-model:open` directive. On desktop it controls the expanded/collapsed state, on mobile it opens/closes the sheet menu.
+
+::component-example
+---
+name: 'sidebar-open-example'
+class: '!p-0 !justify-start'
+overflowHidden: true
+containerClass: 'h-[500px]'
+---
+::
+
+::note
+In this example, leveraging [`defineShortcuts`](/docs/composables/define-shortcuts), you can toggle the open state of the Sidebar by pressing :kbd{value="O"}.
+::
+
+### Persist open state
+
+Use [`useLocalStorage`](https://vueuse.org/core/useLocalStorage/) from VueUse or [`useCookie`](https://nuxt.com/docs/4.x/api/composables/use-cookie) instead of `ref` to persist the sidebar state across page reloads.
+
+::component-example
+---
+name: 'sidebar-persist-example'
+class: '!p-0 !justify-start'
+overflowHidden: true
+containerClass: 'h-[500px]'
+---
+::
+
+::note
+The only difference with the previous example is replacing `ref(true)` with `useLocalStorage('sidebar-open', true)`.
+::
+
+## API
+
+### Props
+
+:component-props
+
+### Slots
+
+:component-slots
+
+## Theme
+
+:component-theme
+
+## Changelog
+
+:component-changelog
diff --git a/docs/public/components/dark/sidebar.png b/docs/public/components/dark/sidebar.png
new file mode 100644
index 0000000000..5c368a6838
Binary files /dev/null and b/docs/public/components/dark/sidebar.png differ
diff --git a/docs/public/components/light/sidebar.png b/docs/public/components/light/sidebar.png
new file mode 100644
index 0000000000..3191d1beef
Binary files /dev/null and b/docs/public/components/light/sidebar.png differ
diff --git a/playgrounds/nuxt/app/app.vue b/playgrounds/nuxt/app/app.vue
index 2e635104d3..3d2df62db2 100644
--- a/playgrounds/nuxt/app/app.vue
+++ b/playgrounds/nuxt/app/app.vue
@@ -51,7 +51,9 @@ provide('components', components)
+
+import type { UIMessage } from 'ai'
+import { Chat } from '@ai-sdk/vue'
+import theme from '#build/ui/sidebar'
+
+const variants = Object.keys(theme.variants.variant)
+
+const input = ref('')
+const openLeft = ref(false)
+const openRight = ref(true)
+const variant = ref('sidebar' as const)
+
+const messages: UIMessage[] = [{
+ id: '1',
+ role: 'user',
+ parts: [{ type: 'text', text: 'What is Nuxt UI?' }]
+}, {
+ id: '2',
+ role: 'assistant',
+ parts: [{ type: 'text', text: 'Nuxt UI is a Vue component library built on Reka UI, Tailwind CSS, and Tailwind Variants. It provides 125+ accessible components for building modern web apps.' }]
+}]
+
+const chat = new Chat({
+ messages,
+ onError() {}
+})
+
+function onSubmit() {
+ chat.sendMessage({ text: input.value })
+ input.value = ''
+}
+
+
+
+