+---
+
+:div{class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"}[Right click here]
+::
+
+
### Disabled
Use the `disabled` prop to disable the ContextMenu.
diff --git a/docs/content/docs/2.components/drawer.md b/docs/content/docs/2.components/drawer.md
index da4af87a3b..bd95da43cc 100644
--- a/docs/content/docs/2.components/drawer.md
+++ b/docs/content/docs/2.components/drawer.md
@@ -216,6 +216,54 @@ slots:
:placeholder{class="h-48 m-4"}
::
+### Modal
+
+Use the `modal` prop to control whether the Drawer blocks interaction with outside content. Defaults to `true`.
+
+::note
+When `modal` is set to `false`, the overlay is automatically disabled and outside content becomes interactive.
+::
+
+::component-code
+---
+prettier: true
+props:
+ modal: false
+slots:
+ default: |
+
+
+
+ content: |
+
+
+---
+
+:u-button{label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up"}
+
+#content
+:placeholder{class="h-48 m-4"}
+::
+
+### Dismissible
+
+Use the `dismissible` prop to control whether the Drawer is dismissible when clicking outside of it or pressing escape. Defaults to `true`.
+
+::note
+A `close:prevent` event will be emitted when the user tries to close it.
+::
+
+::tip
+You can combine `modal: false` with `dismissible: false` to make the Drawer's background interactive without closing it.
+::
+
+::component-example
+---
+prettier: true
+name: 'drawer-dismissible-example'
+---
+::
+
### Scale Background
Use the `should-scale-background` prop to scale the background when the Drawer is open, creating a visual depth effect. You can set the `set-background-color-on-scale` prop to `false` to prevent changing the background color.
@@ -291,32 +339,6 @@ In this example, leveraging [`defineShortcuts`](/docs/composables/define-shortcu
This allows you to move the trigger outside of the Drawer or remove it entirely.
::
-### Disable dismissal
-
-Set the `dismissible` prop to `false` to prevent the Drawer from being closed when clicking outside of it or pressing escape. A `close:prevent` event will be emitted when the user tries to close it.
-
-::component-example
----
-prettier: true
-name: 'drawer-dismissible-example'
----
-::
-
-::note
-In this example, the `header` slot is used to add a close button which is not done by default.
-::
-
-### With interactive background
-
-Set the `overlay` and `modal` props to `false` alongside the `dismissible` prop to make the Drawer's background interactive without closing the Drawer.
-
-::component-example
----
-prettier: true
-name: 'drawer-modal-example'
----
-::
-
### Responsive drawer
You can render a [Modal](/docs/components/modal) component on desktop and a Drawer on mobile for example.
diff --git a/docs/content/docs/2.components/dropdown-menu.md b/docs/content/docs/2.components/dropdown-menu.md
index d9991fcbb2..a8514b98a6 100644
--- a/docs/content/docs/2.components/dropdown-menu.md
+++ b/docs/content/docs/2.components/dropdown-menu.md
@@ -307,6 +307,40 @@ The `size` prop will not be proxied to the Button, you need to set it yourself.
When using the same size, the DropdownMenu items will be perfectly aligned with the Button.
::
+### Modal
+
+Use the `modal` prop to control whether the DropdownMenu blocks interaction with outside content. Defaults to `true`.
+
+::component-code
+---
+prettier: true
+ignore:
+ - items
+ - ui.content
+external:
+ - items
+externalTypes:
+ - DropdownMenuItem[]
+props:
+ modal: false
+ items:
+ - label: Profile
+ icon: i-lucide-user
+ - label: Billing
+ icon: i-lucide-credit-card
+ - label: Settings
+ icon: i-lucide-cog
+ ui:
+ content: 'w-48'
+slots:
+ default: |
+
+
+---
+
+:u-button{label="Open" icon="i-lucide-menu" color="neutral" variant="outline"}
+::
+
### Disabled
Use the `disabled` prop to disable the DropdownMenu.
diff --git a/docs/content/docs/2.components/modal.md b/docs/content/docs/2.components/modal.md
index 12f38902c0..ae9ff85832 100644
--- a/docs/content/docs/2.components/modal.md
+++ b/docs/content/docs/2.components/modal.md
@@ -169,6 +169,34 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.cl
:::
::
+### Transition
+
+Use the `transition` prop to control whether the Modal is animated or not. Defaults to `true`.
+
+::component-code
+---
+prettier: true
+ignore:
+ - title
+props:
+ transition: false
+ title: 'Modal without transition'
+slots:
+ default: |
+
+
+
+ body: |
+
+
+---
+
+:u-button{label="Open" color="neutral" variant="subtle"}
+
+#body
+:placeholder{class="h-48"}
+::
+
### Overlay
Use the `overlay` prop to control whether the Modal has an overlay or not. Defaults to `true`.
@@ -197,9 +225,13 @@ slots:
:placeholder{class="h-48"}
::
-### Transition
+### Modal
-Use the `transition` prop to control whether the Modal is animated or not. Defaults to `true`.
+Use the `modal` prop to control whether the Modal blocks interaction with outside content. Defaults to `true`.
+
+::note
+When `modal` is set to `false`, the overlay is automatically disabled and outside content becomes interactive.
+::
::component-code
---
@@ -207,8 +239,8 @@ prettier: true
ignore:
- title
props:
- transition: false
- title: 'Modal without transition'
+ modal: false
+ title: 'Modal interactive'
slots:
default: |
@@ -225,19 +257,27 @@ slots:
:placeholder{class="h-48"}
::
-### Fullscreen
+### Dismissible
-Use the `fullscreen` prop to make the Modal fullscreen.
+Use the `dismissible` prop to control whether the Modal is dismissible when clicking outside of it or pressing escape. Defaults to `true`.
+
+::note
+A `close:prevent` event will be emitted when the user tries to close it.
+::
+
+::tip
+You can combine `modal: false` with `dismissible: false` to make the Modal's background interactive without closing it.
+::
::component-code
---
prettier: true
ignore:
- title
- - fullscreen
props:
- fullscreen: true
- title: 'Modal fullscreen'
+ dismissible: false
+ modal: true
+ title: 'Modal non-dismissible'
slots:
default: |
@@ -245,48 +285,65 @@ slots:
body: |
-
+
---
:u-button{label="Open" color="neutral" variant="subtle"}
#body
-:placeholder{class="h-full"}
+:placeholder{class="h-48"}
::
-## Examples
+### Scrollable :badge{label="Soon"}
-### Control open state
+Use the `scrollable` prop to make the Modal's content scrollable within the overlay.
-You can control the open state by using the `default-open` prop or the `v-model:open` directive.
+::warning
+As the overlay is needed for scrolling, `modal: false` is not compatible and `overlay: false` only removes the background.
+::
-::component-example
+::component-code
---
-name: 'modal-open-example'
+prettier: true
+ignore:
+ - title
+props:
+ scrollable: true
+ overlay: true
+ title: 'Modal scrollable'
+slots:
+ default: |
+
+
+
+ body: |
+
+
---
-::
-::note
-In this example, leveraging [`defineShortcuts`](/docs/composables/define-shortcuts), you can toggle the Modal by pressing :kbd{value="O"}.
+:u-button{label="Open" color="neutral" variant="subtle"}
+
+#body
+:placeholder{class="h-screen"}
::
-::tip
-This allows you to move the trigger outside of the Modal or remove it entirely.
+::caution
+There's a [known issue](https://reka-ui.com/docs/components/dialog#scrollable-overlay) where clicking on the scrollbar may unintentionally close the dialog on some operating systems.
::
-### Disable dismissal
+### Fullscreen
-Set the `dismissible` prop to `false` to prevent the Modal from being closed when clicking outside of it or pressing escape. A `close:prevent` event will be emitted when the user tries to close it.
+Use the `fullscreen` prop to make the Modal fullscreen.
::component-code
---
prettier: true
ignore:
- title
- - dismissible
+ - fullscreen
props:
- dismissible: false
- title: 'Modal non-dismissible'
+ fullscreen: true
+ title: 'Modal fullscreen'
slots:
default: |
@@ -294,13 +351,33 @@ slots:
body: |
-
+
---
:u-button{label="Open" color="neutral" variant="subtle"}
#body
-:placeholder{class="h-48"}
+:placeholder{class="h-full"}
+::
+
+## Examples
+
+### Control open state
+
+You can control the open state by using the `default-open` prop or the `v-model:open` directive.
+
+::component-example
+---
+name: 'modal-open-example'
+---
+::
+
+::note
+In this example, leveraging [`defineShortcuts`](/docs/composables/define-shortcuts), you can toggle the Modal by pressing :kbd{value="O"}.
+::
+
+::tip
+This allows you to move the trigger outside of the Modal or remove it entirely.
::
### Programmatic usage
diff --git a/docs/content/docs/2.components/popover.md b/docs/content/docs/2.components/popover.md
index 46aae47f60..dad05af2d9 100644
--- a/docs/content/docs/2.components/popover.md
+++ b/docs/content/docs/2.components/popover.md
@@ -165,32 +165,63 @@ slots:
:placeholder{class="size-48 m-4 inline-flex"}
::
-## Examples
-
-### Control open state
+### Modal
-You can control the open state by using the `default-open` prop or the `v-model:open` directive.
+Use the `modal` prop to control whether the Popover blocks interaction with outside content. Defaults to `false`.
-::component-example
+::component-code
---
-name: 'popover-open-example'
+prettier: true
+ignore:
+ - title
+props:
+ modal: true
+slots:
+ default: |
+
+
+
+ content: |
+
+
---
+
+:u-button{label="Open" color="neutral" variant="subtle"}
+
+#content
+:placeholder{class="size-48 m-4 inline-flex"}
::
+### Dismissible
+
+Use the `dismissible` prop to control whether the Popover is dismissible when clicking outside of it or pressing escape. Defaults to `true`.
+
::note
-In this example, leveraging [`defineShortcuts`](/docs/composables/define-shortcuts), you can toggle the Popover by pressing :kbd{value="O"}.
+A `close:prevent` event will be emitted when the user tries to close it.
::
-### Disable dismissal
+::component-example
+---
+name: 'popover-dismissible-example'
+---
+::
-Set the `dismissible` prop to `false` to prevent the Popover from being closed when clicking outside of it or pressing escape. A `close:prevent` event will be emitted when the user tries to close it.
+## Examples
+
+### Control open state
+
+You can control the open state by using the `default-open` prop or the `v-model:open` directive.
::component-example
---
-name: 'popover-dismissible-example'
+name: 'popover-open-example'
---
::
+::note
+In this example, leveraging [`defineShortcuts`](/docs/composables/define-shortcuts), you can toggle the Popover by pressing :kbd{value="O"}.
+::
+
### With command palette
You can use a [CommandPalette](/docs/components/command-palette) component inside the Popover's content.
diff --git a/docs/content/docs/2.components/slideover.md b/docs/content/docs/2.components/slideover.md
index ccb200a5d3..a6c45df4de 100644
--- a/docs/content/docs/2.components/slideover.md
+++ b/docs/content/docs/2.components/slideover.md
@@ -197,9 +197,9 @@ slots:
:placeholder{class="h-full min-h-48"}
::
-### Overlay
+### Transition
-Use the `overlay` prop to control whether the Slideover has an overlay or not. Defaults to `true`.
+Use the `transition` prop to control whether the Slideover is animated or not. Defaults to `true`.
::component-code
---
@@ -207,8 +207,8 @@ prettier: true
ignore:
- title
props:
- overlay: false
- title: 'Slideover without overlay'
+ transition: false
+ title: 'Slideover without transition'
slots:
default: |
@@ -225,9 +225,9 @@ slots:
:placeholder{class="h-full"}
::
-### Transition
+### Overlay
-Use the `transition` prop to control whether the Slideover is animated or not. Defaults to `true`.
+Use the `overlay` prop to control whether the Slideover has an overlay or not. Defaults to `true`.
::component-code
---
@@ -235,8 +235,8 @@ prettier: true
ignore:
- title
props:
- transition: false
- title: 'Slideover without transition'
+ overlay: false
+ title: 'Slideover without overlay'
slots:
default: |
@@ -253,38 +253,58 @@ slots:
:placeholder{class="h-full"}
::
-## Examples
+### Modal
-### Control open state
+Use the `modal` prop to control whether the Slideover blocks interaction with outside content. Defaults to `true`.
-You can control the open state by using the `default-open` prop or the `v-model:open` directive.
+::note
+When `modal` is set to `false`, the overlay is automatically disabled and outside content becomes interactive.
+::
-::component-example
+::component-code
---
-name: 'slideover-open-example'
+prettier: true
+ignore:
+ - title
+props:
+ modal: false
+ title: 'Slideover interactive'
+slots:
+ default: |
+
+
+
+ body: |
+
+
---
+
+:u-button{label="Open" color="neutral" variant="subtle"}
+
+#body
+:placeholder{class="h-full"}
::
+### Dismissible
+
+Use the `dismissible` prop to control whether the Slideover is dismissible when clicking outside of it or pressing escape. Defaults to `true`.
+
::note
-In this example, leveraging [`defineShortcuts`](/docs/composables/define-shortcuts), you can toggle the Slideover by pressing :kbd{value="O"}.
+A `close:prevent` event will be emitted when the user tries to close it.
::
::tip
-This allows you to move the trigger outside of the Slideover or remove it entirely.
+You can combine `modal: false` with `dismissible: false` to make the Slideover's background interactive without closing it.
::
-### Disable dismissal
-
-Set the `dismissible` prop to `false` to prevent the Slideover from being closed when clicking outside of it or pressing escape. A `close:prevent` event will be emitted when the user tries to close it.
-
::component-code
---
prettier: true
ignore:
- title
- - dismissible
props:
dismissible: false
+ modal: true
title: 'Slideover non-dismissible'
slots:
default: |
@@ -302,6 +322,26 @@ slots:
:placeholder{class="h-full"}
::
+## Examples
+
+### Control open state
+
+You can control the open state by using the `default-open` prop or the `v-model:open` directive.
+
+::component-example
+---
+name: 'slideover-open-example'
+---
+::
+
+::note
+In this example, leveraging [`defineShortcuts`](/docs/composables/define-shortcuts), you can toggle the Slideover by pressing :kbd{value="O"}.
+::
+
+::tip
+This allows you to move the trigger outside of the Slideover or remove it entirely.
+::
+
### Programmatic usage
You can use the [`useOverlay`](/docs/composables/use-overlay) composable to open a Slideover programmatically.
diff --git a/playgrounds/nuxt/app/pages/components/modal.vue b/playgrounds/nuxt/app/pages/components/modal.vue
index 2a03af782b..d243f0bf47 100644
--- a/playgrounds/nuxt/app/pages/components/modal.vue
+++ b/playgrounds/nuxt/app/pages/components/modal.vue
@@ -58,19 +58,32 @@ function openModal() {
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
diff --git a/src/runtime/components/Modal.vue b/src/runtime/components/Modal.vue
index b5c047319e..d9c36dd0eb 100644
--- a/src/runtime/components/Modal.vue
+++ b/src/runtime/components/Modal.vue
@@ -18,6 +18,11 @@ export interface ModalProps extends DialogRootProps {
* @defaultValue true
*/
overlay?: boolean
+ /**
+ * When `true`, enables scrollable overlay mode where content scrolls within the overlay.
+ * @defaultValue false
+ */
+ scrollable?: boolean
/**
* Animate the modal when opening or closing.
* @defaultValue true
@@ -76,7 +81,7 @@ export interface ModalSlots {
-
-
-
-
-
-
-
+
@@ -191,6 +207,24 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/theme/modal.ts b/src/theme/modal.ts
index cb57c157ff..4c3a2d8978 100644
--- a/src/theme/modal.ts
+++ b/src/theme/modal.ts
@@ -1,10 +1,10 @@
export default {
slots: {
- overlay: 'fixed inset-0 bg-elevated/75',
- content: 'fixed bg-default divide-y divide-default flex flex-col focus:outline-none',
+ overlay: 'fixed inset-0',
+ content: 'bg-default divide-y divide-default flex flex-col focus:outline-none',
header: 'flex items-center gap-1.5 p-4 sm:px-6 min-h-16',
wrapper: '',
- body: 'flex-1 overflow-y-auto p-4 sm:p-6',
+ body: 'flex-1 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',
@@ -22,8 +22,36 @@ export default {
content: 'inset-0'
},
false: {
- content: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[calc(100vw-2rem)] max-w-lg max-h-[calc(100dvh-2rem)] sm:max-h-[calc(100dvh-4rem)] rounded-lg shadow-lg ring ring-default overflow-hidden'
+ content: 'w-[calc(100vw-2rem)] max-w-lg rounded-lg shadow-lg ring ring-default'
}
+ },
+ overlay: {
+ true: {
+ overlay: 'bg-elevated/75'
+ }
+ },
+ scrollable: {
+ true: {
+ overlay: 'overflow-y-auto',
+ content: 'relative'
+ },
+ false: {
+ content: 'fixed',
+ body: 'overflow-y-auto'
+ }
+ }
+ },
+ compoundVariants: [{
+ scrollable: true,
+ fullscreen: false,
+ class: {
+ overlay: 'grid place-items-center p-4 sm:py-8'
+ }
+ }, {
+ scrollable: false,
+ fullscreen: false,
+ class: {
+ content: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 max-h-[calc(100dvh-2rem)] sm:max-h-[calc(100dvh-4rem)] overflow-hidden'
}
- }
+ }]
}
diff --git a/test/components/Modal.spec.ts b/test/components/Modal.spec.ts
index f2358ddba9..1a770fc62d 100644
--- a/test/components/Modal.spec.ts
+++ b/test/components/Modal.spec.ts
@@ -16,6 +16,9 @@ describe('Modal', () => {
['with fullscreen', { props: { ...props, fullscreen: true, title: 'Title', description: 'Description' } }],
['without overlay', { props: { ...props, overlay: false, title: 'Title', description: 'Description' } }],
['without transition', { props: { ...props, transition: false, title: 'Title', description: 'Description' } }],
+ ['with scrollable', { props: { ...props, scrollable: true, title: 'Title', description: 'Description' } }],
+ ['with scrollable and fullscreen', { props: { ...props, scrollable: true, fullscreen: true, title: 'Title', description: 'Description' } }],
+ ['with scrollable and without overlay', { props: { ...props, scrollable: true, overlay: false, title: 'Title', description: 'Description' } }],
['without close', { props: { ...props, close: false, title: 'Title', description: 'Description' } }],
['with closeIcon', { props: { ...props, closeIcon: 'i-lucide-trash' } }],
['with class', { props: { ...props, class: 'bg-elevated' } }],
diff --git a/test/components/__snapshots__/DashboardSearch-vue.spec.ts.snap b/test/components/__snapshots__/DashboardSearch-vue.spec.ts.snap
index 849cfd9247..42c10b55b2 100644
--- a/test/components/__snapshots__/DashboardSearch-vue.spec.ts.snap
+++ b/test/components/__snapshots__/DashboardSearch-vue.spec.ts.snap
@@ -2,10 +2,11 @@
exports[`DashboardSearch > renders with class correctly 1`] = `
"
+
-
-
dashboardSearch.title
dashboardSearch.description
+
+
dashboardSearch.title
dashboardSearch.description
@@ -38,10 +39,11 @@ exports[`DashboardSearch > renders with class correctly 1`] = `
exports[`DashboardSearch > renders with fullscreen correctly 1`] = `
"
+
-
-
dashboardSearch.title
dashboardSearch.description
+
+
dashboardSearch.title
dashboardSearch.description
@@ -74,10 +76,11 @@ exports[`DashboardSearch > renders with fullscreen correctly 1`] = `
exports[`DashboardSearch > renders with groups correctly 1`] = `
"
+
-
-
dashboardSearch.title
dashboardSearch.description
+
+
dashboardSearch.title
dashboardSearch.description
@@ -110,10 +113,11 @@ exports[`DashboardSearch > renders with groups correctly 1`] = `
exports[`DashboardSearch > renders with icon correctly 1`] = `
"