diff --git a/docs/app/components/content/examples/sidebar/SidebarExample.vue b/docs/app/components/content/examples/sidebar/SidebarExample.vue
index 22cde75c6a..f5baa20aa8 100644
--- a/docs/app/components/content/examples/sidebar/SidebarExample.vue
+++ b/docs/app/components/content/examples/sidebar/SidebarExample.vue
@@ -136,6 +136,7 @@ defineShortcuts(extractShortcuts(teamsItems.value))
v-model:open="open"
collapsible="icon"
rail
+ resizable
:ui="{
container: 'h-full',
inner: 'bg-elevated/25 divide-transparent',
diff --git a/docs/content/docs/2.components/dashboard-sidebar.md b/docs/content/docs/2.components/dashboard-sidebar.md
index 3b61bd6094..7e1798e115 100644
--- a/docs/content/docs/2.components/dashboard-sidebar.md
+++ b/docs/content/docs/2.components/dashboard-sidebar.md
@@ -13,7 +13,7 @@ links:
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.
+**DashboardSidebar vs Sidebar**: This component is designed for dashboard layouts with `DashboardGroup` integration. For a standalone sidebar (chat panel, settings, navigation), use [Sidebar](/docs/components/sidebar) instead — it also supports drag-to-resize and state persistence.
::
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.
diff --git a/docs/content/docs/2.components/sidebar.md b/docs/content/docs/2.components/sidebar.md
index 1388190c27..70cb8ca83e 100644
--- a/docs/content/docs/2.components/sidebar.md
+++ b/docs/content/docs/2.components/sidebar.md
@@ -14,7 +14,7 @@ navigation.badge: New
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 a [Modal](/docs/components/modal), [Slideover](/docs/components/slideover) or [Drawer](/docs/components/drawer) component.
::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.
+**Sidebar vs DashboardSidebar**: This component is a standalone sidebar you can drop anywhere (chat panel, settings, navigation) with optional drag-to-resize and state persistence. If you need integration with [DashboardGroup](/docs/components/dashboard-group) use [DashboardSidebar](/docs/components/dashboard-sidebar) instead.
::
Use the `header`, `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 menu.
@@ -191,6 +191,84 @@ class: '!p-0 !justify-start h-[500px] contain-[paint]'
:placeholder{class="h-full"}
::
+### Resizable
+
+Use the `resizable` prop to make the sidebar resizable by dragging the rail. Requires `rail` to be enabled.
+
+::component-code
+---
+prettier: true
+ignore:
+ - rail
+ - collapsible
+ - title
+ - ui.container
+hide:
+ - minSize
+ - defaultSize
+ - maxSize
+ - ui
+ - class
+props:
+ rail: true
+ resizable: true
+ collapsible: icon
+ minSize: 12
+ defaultSize: 16
+ maxSize: 24
+ title: Navigation
+ ui.container: h-full
+items:
+ resizable:
+ - true
+ - false
+slots:
+ default: |
+
+
+class: '!p-0 !justify-start h-[500px] contain-[paint]'
+---
+
+:placeholder{class="h-full"}
+::
+
+When `collapsible` is not `none`, dragging below `min-size` snaps the sidebar to its collapsed state. Click the rail to toggle collapsed, or double-click to reset to `default-size`.
+
+### Size
+
+Use the `min-size`, `max-size`, `default-size` and `collapsed-size` props to customize the size of the sidebar in `rem`.
+
+::component-code
+---
+prettier: true
+ignore:
+ - rail
+ - resizable
+ - title
+ - ui.container
+hide:
+ - ui
+ - class
+props:
+ rail: true
+ resizable: true
+ collapsible: icon
+ minSize: 14
+ defaultSize: 18
+ maxSize: 28
+ collapsedSize: 10
+ title: Navigation
+ ui.container: h-full
+slots:
+ default: |
+
+
+class: '!p-0 !justify-start h-[500px] contain-[paint]'
+---
+
+:placeholder{class="h-full"}
+::
+
### 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`.
@@ -348,9 +426,9 @@ The only difference with the previous example is replacing `ref(true)` with `use
### With custom width
-The sidebar width is controlled by the `--sidebar-width` CSS variable (defaults to `16rem`). The collapsed icon width is controlled by `--sidebar-width-icon` (defaults to `4rem`).
+When using the `resizable` prop, the sidebar width is controlled by the `default-size`, `min-size` and `max-size` props. Without `resizable`, the width is controlled by the `--sidebar-width` CSS variable (defaults to `16rem`). 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.
+Override the CSS variables globally in your CSS or per-instance with the `style` attribute.
::component-example
---
diff --git a/playgrounds/nuxt/app/pages/components/sidebar.vue b/playgrounds/nuxt/app/pages/components/sidebar.vue
index 25b264a3da..a4e9439a59 100644
--- a/playgrounds/nuxt/app/pages/components/sidebar.vue
+++ b/playgrounds/nuxt/app/pages/components/sidebar.vue
@@ -46,6 +46,7 @@ function onSubmit() {
collapsible="icon"
close
rail
+ resizable
:ui="{ container: 'relative', body: 'py-2' }"
>
diff --git a/src/runtime/components/Sidebar.vue b/src/runtime/components/Sidebar.vue
index 4f6641da07..0ab9ecd160 100644
--- a/src/runtime/components/Sidebar.vue
+++ b/src/runtime/components/Sidebar.vue
@@ -2,6 +2,7 @@
import type { VNode } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/sidebar'
+import type { UseResizableProps } from '../composables/useResizable'
import type { ButtonProps, DrawerProps, IconProps, ModalProps, SlideoverProps, LinkPropsKeys } from '../types'
import type { ComponentConfig } from '../types/tv'
@@ -11,7 +12,7 @@ type SidebarState = 'expanded' | 'collapsed'
type SidebarMode = 'modal' | 'slideover' | 'drawer'
type SidebarMenu = T extends 'modal' ? ModalProps : T extends 'slideover' ? SlideoverProps : T extends 'drawer' ? DrawerProps : never
-export interface SidebarProps {
+export interface SidebarProps extends Pick {
/**
* The element or component this component should render as.
* @defaultValue 'aside'
@@ -58,10 +59,18 @@ export interface SidebarProps {
closeIcon?: IconProps['name']
/**
* Display a rail on the sidebar edge to toggle collapse.
- * Only renders when `collapsible` is not `none`.
+ * When `resizable` is also enabled, the rail acts as a drag-to-resize handle.
* @defaultValue false
*/
rail?: boolean
+ /**
+ * Whether to allow the user to resize the sidebar by dragging the rail.
+ * Requires `rail` to be enabled. Drag to resize between `minSize` and `maxSize`.
+ * When `collapsible` is not `none`, dragging near `collapsedSize` snaps to collapsed.
+ * Double-click the rail to reset to `defaultSize`.
+ * @defaultValue false
+ */
+ resizable?: boolean
/**
* The mode of the sidebar menu on mobile.
* @defaultValue 'slideover'
@@ -83,19 +92,20 @@ export interface SidebarSlots {
close?(props: { ui: Sidebar['ui'], state: SidebarState }): VNode[]
default?(props: { state: SidebarState, open: boolean, close: () => void }): VNode[]
footer?(props: { state: SidebarState, open: boolean, close: () => void }): VNode[]
- rail?(props: { ui: Sidebar['ui'], state: SidebarState }): VNode[]
+ rail?(props: { ui: Sidebar['ui'], state: SidebarState, onMouseDown: (e: MouseEvent) => void, onTouchStart: (e: TouchEvent) => void, onDoubleClick: (e: MouseEvent) => void }): VNode[]
content?(props: { close: () => void }): VNode[]
}