Repository: melt-ui/melt-ui
Pull request #1178 (2024-04-20 09:25:13)

Added prop to createToaster() to pause all timers on hover
Clean up version of #998 with [suggested changes](https://github.com/melt-ui/melt-ui/pull/998#issuecomment-1975127460).

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 948cc0ace107bf69095e36a514b57ac7ce26dc6a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/lolcabanon/melt-ui/new/feature/toaster-pause-all-on-hover?filename=.changeset/spotty-plants-check.md&value=---%0A%22%40melt-ui%2Fsvelte%22%3A%20patch%0A---%0A%0AAdded%20prop%20to%20createToaster()%20to%20pause%20all%20timers%20on%20hover%0A)


lolcabanon: @TGlide to me, this is ready for review! Let me know if there is anything more to add.

Patch:
diff --git a/.changeset/beige-actors-provide.md b/.changeset/beige-actors-provide.md
deleted file mode 100644
index dec04381..00000000
--- a/.changeset/beige-actors-provide.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@melt-ui/svelte': patch
----
-
-add `as const` return to more element builders (Date Field "segment", Menubar "menubar", Scroll Area)
diff --git a/.changeset/clean-tomatoes-lie.md b/.changeset/clean-tomatoes-lie.md
new file mode 100644
index 00000000..79b5031a
--- /dev/null
+++ b/.changeset/clean-tomatoes-lie.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": minor
+---
+
+Added prop to createToaster() to pause all timers on hover
diff --git a/.changeset/itchy-spoons-brush.md b/.changeset/itchy-spoons-brush.md
deleted file mode 100644
index 90fbef03..00000000
--- a/.changeset/itchy-spoons-brush.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@melt-ui/svelte": patch
----
-
-chore: internal popover refactors
diff --git a/.changeset/new-hairs-vanish.md b/.changeset/new-hairs-vanish.md
deleted file mode 100644
index c0a25729..00000000
--- a/.changeset/new-hairs-vanish.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@melt-ui/svelte': patch
----
-
-slider: add disable swapping option to avoid sorting of values array
diff --git a/.changeset/rude-carpets-rhyme.md b/.changeset/rude-carpets-rhyme.md
deleted file mode 100644
index a2adc6b6..00000000
--- a/.changeset/rude-carpets-rhyme.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@melt-ui/svelte": patch
----
-
-chore: refactor menu internals
diff --git a/.changeset/slow-chefs-deliver.md b/.changeset/slow-chefs-deliver.md
deleted file mode 100644
index c6f21980..00000000
--- a/.changeset/slow-chefs-deliver.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@melt-ui/svelte': minor
----
-
-Added new `preventTextSelectionOverflow` prop to builders to prevent text selection overflowing the floating element. (closes #1168)
diff --git a/.changeset/wicked-spies-approve.md b/.changeset/wicked-spies-approve.md
deleted file mode 100644
index 777955d1..00000000
--- a/.changeset/wicked-spies-approve.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@melt-ui/svelte": patch
----
-
-Refactor context menu internals
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca262bff..7036e4b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,79 @@
 # @melt-ui/svelte
 
+## 0.81.0
+
+### Minor Changes
+
+- Mount each floating element into their own portal by default now that stack handling has been improved ([#1103](https://github.com/melt-ui/melt-ui/pull/1103))
+
+- Support all of Floating UI's FlipOptions at `positioning.flip` ([#1203](https://github.com/melt-ui/melt-ui/pull/1203))
+
+### Patch Changes
+
+- fix svelte peer deps ([`de0169dd`](https://github.com/melt-ui/melt-ui/commit/de0169dd840a4ea51dd0a0611b59797665095a8c))
+
+- fix undefined on portal update ([`87bce8a0`](https://github.com/melt-ui/melt-ui/commit/87bce8a0ec9d085715b8985c16158ebd3991b71a))
+
+## 0.80.0
+
+### Minor Changes
+
+- feat(slider): add `onValueCommitted` callback ([#1212](https://github.com/melt-ui/melt-ui/pull/1212))
+
+- fix: add `data-state` to select & combobox (closes #1199) ([#1200](https://github.com/melt-ui/melt-ui/pull/1200))
+
+- chore: change peer deps to support svelte 5 ([#1207](https://github.com/melt-ui/melt-ui/pull/1207))
+
+## 0.79.1
+
+### Patch Changes
+
+- Fix: checkbox hiddenInput not calling change events ([`caae3c01`](https://github.com/melt-ui/melt-ui/commit/caae3c01d4bde5c6f8331fb0e63bf7dc0f573810))
+
+## 0.79.0
+
+### Minor Changes
+
+- Add support for sibling portals (closes #1077, #1153) ([#1080](https://github.com/melt-ui/melt-ui/pull/1080))
+
+### Patch Changes
+
+- Fixed a bug where a floating element would not close on outside interaction that opens a new floating element. (closes #1167) ([#1080](https://github.com/melt-ui/melt-ui/pull/1080))
+
+## 0.78.0
+
+### Minor Changes
+
+- Fixed escape keydown behavior by using stacks to correctly handle nested floating elements ([#1142](https://github.com/melt-ui/melt-ui/pull/1142))
+
+- Changed `closeOnEscape` builder prop to `escapeBehavior` to provide even further fine-grained control over escape behavior in builders (closes #1142) ([#1142](https://github.com/melt-ui/melt-ui/pull/1142))
+
+### Patch Changes
+
+- fix: Modal elements allow others to close again (closes #1177) ([`7ebc8202`](https://github.com/melt-ui/melt-ui/commit/7ebc820268fa6e80657a930ffb0c5141b620388c))
+
+- Fixed bug where tooltip would close while the pointer is still inside trigger (closes #886) ([#1158](https://github.com/melt-ui/melt-ui/pull/1158))
+
+- fix: pagination pages when count = 0 ([`883b1598`](https://github.com/melt-ui/melt-ui/commit/883b159899919eb0838130947c4c6a03a42d8a9b))
+
+## 0.77.0
+
+### Minor Changes
+
+- Added new `preventTextSelectionOverflow` prop to builders to prevent text selection overflowing the floating element. (closes #1168) ([#1170](https://github.com/melt-ui/melt-ui/pull/1170))
+
+### Patch Changes
+
+- add `as const` return to more element builders (Date Field "segment", Menubar "menubar", Scroll Area) ([#1172](https://github.com/melt-ui/melt-ui/pull/1172))
+
+- chore: internal popover refactors ([#1055](https://github.com/melt-ui/melt-ui/pull/1055))
+
+- slider: add disable swapping option to avoid sorting of values array ([#1124](https://github.com/melt-ui/melt-ui/pull/1124))
+
+- chore: refactor menu internals ([#1056](https://github.com/melt-ui/melt-ui/pull/1056))
+
+- Refactor context menu internals ([#1054](https://github.com/melt-ui/melt-ui/pull/1054))
+
 ## 0.76.3
 
 ### Patch Changes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 324a58aa..42ad7e71 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -127,7 +127,7 @@ submitting the pull request.
 
 ## Contribute from a browser IDE
 
-Make changes and contribute to OctoBot in a single click with an **already setup and ready to code
+Make changes and contribute to Melt UI in a single click with an **already setup and ready to code
 developer environment** using Gitpod !
 
 [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/?autostart=true#https://github.com/melt-ui/melt-ui/tree/develop)
diff --git a/README.md b/README.md
index 5b650d3f..0789e01c 100644
--- a/README.md
+++ b/README.md
@@ -58,12 +58,22 @@ enhance the experience of building with Svelte and improve the ecosystem for eve
 
 Check out our [Contributing guide](./CONTRIBUTING.md) to learn more.
 
+## Sponsors
+
+<p align="center">
+  <a href="https://github.com/sponsors/tglide">
+    <img src='https://github.com/tglide/sponsors/blob/main/sponsors.svg?raw=true' alt="Logos from Sponsors" />
+  </a>
+</p>
+
+## Community
+
 Melt UI is an open-source project built by the community for the community. It wouldn't be possible
 if it wasn't for the work of some amazing people.
 
 [![Contributors](https://contrib.rocks/image?repo=melt-ui/melt-ui)](<[https://github.com/codemaniac-sahil/news-webapp-api](https://github.com/melt-ui/melt-ui)https://github.com/melt-ui/melt-ui/graphs/contributors>)
 
-## Community
+### Discord
 
 Got any questions? Want to talk to the maintainers?
 
diff --git a/package.json b/package.json
index 8367b75f..0eaaf289 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@melt-ui/svelte",
-	"version": "0.76.3",
+	"version": "0.81.0",
 	"license": "MIT",
 	"repository": "github:melt-ui/melt-ui",
 	"author": "Thomas G. Lopes",
@@ -58,7 +58,7 @@
 		"!dist/**/tests"
 	],
 	"peerDependencies": {
-		"svelte": ">=3 <5"
+		"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.118"
 	},
 	"dependencies": {
 		"@floating-ui/core": "^1.3.1",
diff --git a/src/docs/constants.ts b/src/docs/constants.ts
index 21ff6599..99b327a9 100644
--- a/src/docs/constants.ts
+++ b/src/docs/constants.ts
@@ -13,6 +13,17 @@ export const SEE = {
 	},
 };
 
+const createBulletsHTML = (bullets: string[]) => {
+	return bullets.map((bullet) => `<li>${bullet}</li>`).join('');
+};
+
+const ESCAPE_BEHAVIOR_BULLETS = (name: string) => [
+	`\`close\`: Closes the ${name} immediately.`,
+	`\`ignore\`: Prevents the ${name} from closing and also blocks the parent element from closing in response to the Escape key.`,
+	`\`defer-otherwise-close\`: Delegates the action to the parent element. If no parent is found, it closes the element.`,
+	`\`defer-otherwise-ignore\`: Delegates the action to the parent element. If no parent is found, nothing is done.`,
+];
+
 export const DESCRIPTIONS = {
 	FLOATING_CONFIG:
 		'A configuration object which determines how the floating element is positioned relative to the trigger.',
@@ -22,13 +33,15 @@ export const DESCRIPTIONS = {
 	ON_SELECT:
 		'A callback which is called when the item is selected. To prevent the default behavior, call `e.preventDefault()` in the callback.',
 	LOOP: 'Whether or not the focus should loop back to the first item when the last item is reached.',
-	CLOSE_ON_ESCAPE: (name = 'element') =>
-		`Whether or not to close the ${name} when the escape key is pressed.`,
+	ESCAPE_BEHAVIOR: (name = 'element') =>
+		`Defines how the ${name} reacts when the Escape key is pressed. ${createBulletsHTML(
+			ESCAPE_BEHAVIOR_BULLETS(name)
+		)}`,
 	CLOSE_ON_CLICK_OUTSIDE: (name = 'element') =>
 		`Whether or not to close the ${name} when the user clicks outside of it.`,
 	BUILDER: (name: string) => `The builder function used to create the ${name} component.`,
 	PORTAL: (name = 'floating element') =>
-		`The element or selector to render the ${name} into. Nested floating elements are automatically rendered into their parent if not specified. Pass in \`null\` to disable portalling.`,
+		`The element or selector to render the ${name} into. Nested floating elements are automatically rendered into their own portal if not specified. Pass in \`null\` to disable portalling. Pass in \`undefined\` to use parent portal.`,
 	FORCE_VISIBLE: (name = 'element') =>
 		`Whether or not to force the ${name} to always be visible. This is useful for custom transitions and animations using conditional blocks.`,
 	ON_CHANGE: (store: string) =>
@@ -83,11 +96,11 @@ export const PROPS = {
 		description: DESCRIPTIONS.PORTAL(args.name ?? 'floating element'),
 		default: args.default ?? 'body',
 	}),
-	CLOSE_ON_ESCAPE: (args: PropArgs = {}): Prop => ({
-		name: 'closeOnEscape',
-		type: 'boolean',
-		description: DESCRIPTIONS.CLOSE_ON_ESCAPE(args.name ?? 'floating element'),
-		default: args.default ?? 'true',
+	ESCAPE_BEHAVIOR: (args: PropArgs = {}): Prop => ({
+		name: 'escapeBehavior',
+		type: '"close" | "ignore" | "defer-otherwise-close" | "defer-otherwise-ignore"',
+		description: DESCRIPTIONS.ESCAPE_BEHAVIOR(args.name ?? 'floating element'),
+		default: args.default ?? "'close'",
 	}),
 	CLOSE_ON_OUTSIDE_CLICK: (args: PropArgs = {}): Prop => ({
 		name: 'closeOnOutsideClick',
diff --git a/src/docs/content/builders/calendar.md b/src/docs/content/builders/calendar.md
index 65b24048..01b70539 100644
--- a/src/docs/content/builders/calendar.md
+++ b/src/docs/content/builders/calendar.md
@@ -464,6 +464,26 @@ constructor.
 	<svelte:component this={previews.locale} />
 </Preview>
 
+The Calendar builder also supports the use of specific calendar systems, such as the Persian
+calendar. You can achieve this by providing the defaultValue or defaultPlaceholder props with the
+desired calendar system. Various calendar systems supported by
+[@internationalized/date](https://react-spectrum.adobe.com/internationalized/date/Calendar.html#implementations)
+are available for use.
+
+```svelte showLineNumbers {3,8,9}
+<script lang="ts">
+	import { createCalendar, melt } from '@melt-ui/svelte'
+	import { PersianCalendar } from '@internationalized/date'
+
+	const {
+		/* ... */
+	} = createCalendar({
+		locale: 'fa',
+		defaultValue: new CalendarDate(new PersianCalendar(), 1403, 2, 12)
+	})
+</script>
+```
+
 ### Prevent Deselection
 
 By default, users can deselect a selected date without selecting another, by selecting the date
diff --git a/src/docs/content/builders/dialog.md b/src/docs/content/builders/dialog.md
index a1d5e168..1dde6ba3 100644
--- a/src/docs/content/builders/dialog.md
+++ b/src/docs/content/builders/dialog.md
@@ -99,11 +99,11 @@ const {
 ### Disable Close on Escape
 
 By default, pressing the escape key will close the dialog. You can disable this behavior by setting
-the `closeOnEscape` builder prop to `false`.
+the `escapeBehavior` builder prop to `ignore`.
 
 ```ts {2}
 const dialog = createDialog({
-	closeOnEscape: false
+	escapeBehavior: 'ignore'
 })
 ```
 
diff --git a/src/docs/content/builders/switch.md b/src/docs/content/builders/switch.md
index 4e347a43..f844b3d6 100644
--- a/src/docs/content/builders/switch.md
+++ b/src/docs/content/builders/switch.md
@@ -11,7 +11,7 @@ description: A control that allows the user to toggle between checked and not ch
 
 ## Anatomy
 
-- **Root**: The root container for the accordion
+- **Root**: The root container for the switch
 - **Input**: The native HTML input that is visually hidden
 
 ## Usage
diff --git a/src/docs/content/controlled.md b/src/docs/content/controlled.md
index 926d1254..138ef85b 100644
--- a/src/docs/content/controlled.md
+++ b/src/docs/content/controlled.md
@@ -59,7 +59,6 @@ interaction.
 ```svelte {10}
 <script lang="ts">
 	import { createDialog } from '@melt-ui/svelte'
-	import { writable } from 'svelte/store'
 
 	const {
 		elements: { trigger, overlay, content, title, description, close },
diff --git a/src/docs/data/builders/checkbox.ts b/src/docs/data/builders/checkbox.ts
index 0b8c309d..7495095b 100644
--- a/src/docs/data/builders/checkbox.ts
+++ b/src/docs/data/builders/checkbox.ts
@@ -107,7 +107,7 @@ const input = elementSchema('input', {
 	description: 'The native input element.',
 	dataAttributes: [
 		{
-			name: 'data-melt-checkbox-input',
+			name: 'data-melt-checkbox-hidden-input',
 			value: ATTRS.MELT('checkbox input'),
 		},
 	],
diff --git a/src/docs/data/builders/combobox.ts b/src/docs/data/builders/combobox.ts
index 9428f956..7e057994 100644
--- a/src/docs/data/builders/combobox.ts
+++ b/src/docs/data/builders/combobox.ts
@@ -18,7 +18,7 @@ const OPTION_PROPS = [
 	},
 	PROPS.LOOP,
 	PROPS.CLOSE_ON_OUTSIDE_CLICK,
-	PROPS.CLOSE_ON_ESCAPE,
+	PROPS.ESCAPE_BEHAVIOR,
 	PROPS.PREVENT_SCROLL,
 	PROPS.PREVENT_TEXT_SELECTION_OVERFLOW,
 	PROPS.PORTAL,
@@ -183,8 +183,8 @@ const input = elementSchema('input', {
 	events: comboboxEvents['input'],
 });
 
-const item = elementSchema('item', {
-	description: 'The menu item element',
+const option = elementSchema('option', {
+	description: 'The individual combobox item',
 	props: [
 		{
 			name: 'value',
@@ -201,25 +201,25 @@ const item = elementSchema('item', {
 			name: 'disabled',
 			type: 'boolean',
 			default: 'false',
-			description: 'Whether or not the `item` is disabled.',
+			description: 'Whether or not the `option` is disabled.',
 		},
 	],
 	dataAttributes: [
 		{
 			name: 'data-disabled',
-			value: ATTRS.DISABLED('`item`'),
+			value: ATTRS.DISABLED('`option`'),
 		},
 		{
 			name: 'data-selected',
-			value: ATTRS.SELECTED('`item`'),
+			value: ATTRS.SELECTED('`option`'),
 		},
 		{
 			name: 'data-highlighted',
 			value: ATTRS.HIGHLIGHTED(),
 		},
 		{
-			name: 'data-melt-combobox-item',
-			value: ATTRS.MELT('item'),
+			name: 'data-melt-combobox-option',
+			value: ATTRS.MELT('option'),
 		},
 	],
 	events: comboboxEvents['item'],
@@ -317,7 +317,7 @@ const keyboard: KeyboardSchema = [
 	},
 ];
 
-const schemas = [builder, menu, input, item, label, group, groupLabel, arrow, hiddenInput];
+const schemas = [builder, menu, input, option, label, group, groupLabel, arrow, hiddenInput];
 
 const features = [
 	'Full keyboard navigation',
diff --git a/src/docs/data/builders/dialog.ts b/src/docs/data/builders/dialog.ts
index 3c866d90..43a9cb0c 100644
--- a/src/docs/data/builders/dialog.ts
+++ b/src/docs/data/builders/dialog.ts
@@ -16,7 +16,7 @@ const OPTION_PROPS = [
 		description: 'The `role` attribute of the dialog element.',
 	},
 	PROPS.PREVENT_SCROLL,
-	PROPS.CLOSE_ON_ESCAPE,
+	PROPS.ESCAPE_BEHAVIOR,
 	PROPS.CLOSE_ON_OUTSIDE_CLICK,
 	PROPS.PORTAL,
 	PROPS.FORCE_VISIBLE,
diff --git a/src/docs/data/builders/link-preview.ts b/src/docs/data/builders/link-preview.ts
index 3139d838..a2023b1d 100644
--- a/src/docs/data/builders/link-preview.ts
+++ b/src/docs/data/builders/link-preview.ts
@@ -24,7 +24,7 @@ const OPTION_PROPS = [
 	PROPS.POSITIONING({ default: "placement: 'bottom'" }),
 	PROPS.ARROW_SIZE,
 	PROPS.CLOSE_ON_OUTSIDE_CLICK,
-	PROPS.CLOSE_ON_ESCAPE,
+	PROPS.ESCAPE_BEHAVIOR,
 	PROPS.PREVENT_TEXT_SELECTION_OVERFLOW,
 	PROPS.FORCE_VISIBLE,
 	PROPS.PORTAL,
diff --git a/src/docs/data/builders/menu.ts b/src/docs/data/builders/menu.ts
index fe029402..2abc9dd5 100644
--- a/src/docs/data/builders/menu.ts
+++ b/src/docs/data/builders/menu.ts
@@ -17,7 +17,7 @@ export const menuBuilderProps = [
 	PROPS.ARROW_SIZE,
 	PROPS.DIR,
 	PROPS.PREVENT_SCROLL,
-	PROPS.CLOSE_ON_ESCAPE,
+	PROPS.ESCAPE_BEHAVIOR,
 	PROPS.PREVENT_TEXT_SELECTION_OVERFLOW,
 	PROPS.PORTAL,
 	PROPS.CLOSE_ON_OUTSIDE_CLICK,
@@ -39,7 +39,7 @@ export const menuBuilderOptions = [
 	PROPS.ARROW_SIZE,
 	PROPS.DIR,
 	PROPS.PREVENT_SCROLL,
-	PROPS.CLOSE_ON_ESCAPE,
+	PROPS.ESCAPE_BEHAVIOR,
 	PROPS.PREVENT_TEXT_SELECTION_OVERFLOW,
 	PROPS.PORTAL,
 	PROPS.CLOSE_ON_OUTSIDE_CLICK,
diff --git a/src/docs/data/builders/menubar.ts b/src/docs/data/builders/menubar.ts
index 32b2f87a..bbc0b5d0 100644
--- a/src/docs/data/builders/menubar.ts
+++ b/src/docs/data/builders/menubar.ts
@@ -7,7 +7,7 @@ import { builder as dropdownBuilder } from './dropdown-menu.js';
 import type { BuilderData } from './index.js';
 import { getMenuSchemas, getMenuTriggerDataAttrs } from './menu.js';
 
-const OPTION_PROPS = [PROPS.CLOSE_ON_ESCAPE, PROPS.LOOP, PROPS.PREVENT_SCROLL];
+const OPTION_PROPS = [PROPS.ESCAPE_BEHAVIOR, PROPS.LOOP, PROPS.PREVENT_SCROLL];
 const BUILDER_NAME = 'menubar';
 
 const builder = builderSchema(BUILDER_NAME, {
diff --git a/src/docs/data/builders/popover.ts b/src/docs/data/builders/popover.ts
index 35154fa7..fec468e7 100644
--- a/src/docs/data/builders/popover.ts
+++ b/src/docs/data/builders/popover.ts
@@ -22,7 +22,7 @@ const OPTION_PROPS = [
 		description: 'Whether to disable the focus trap.',
 	},
 	PROPS.ARROW_SIZE,
-	PROPS.CLOSE_ON_ESCAPE,
+	PROPS.ESCAPE_BEHAVIOR,
 	PROPS.CLOSE_ON_OUTSIDE_CLICK,
 	PROPS.PREVENT_SCROLL,
 	PROPS.PREVENT_TEXT_SELECTION_OVERFLOW,
diff --git a/src/docs/data/builders/select.ts b/src/docs/data/builders/select.ts
index 4fd1afc7..cdbc08c6 100644
--- a/src/docs/data/builders/select.ts
+++ b/src/docs/data/builders/select.ts
@@ -19,7 +19,7 @@ const OPTION_PROPS = [
 	PROPS.ARROW_SIZE,
 	PROPS.PREVENT_SCROLL,
 	PROPS.LOOP,
-	PROPS.CLOSE_ON_ESCAPE,
+	PROPS.ESCAPE_BEHAVIOR,
 	PROPS.CLOSE_ON_OUTSIDE_CLICK,
 	PROPS.PREVENT_TEXT_SELECTION_OVERFLOW,
 	PROPS.PORTAL,
diff --git a/src/docs/data/builders/slider.ts b/src/docs/data/builders/slider.ts
index 78e0bee9..e9074875 100644
--- a/src/docs/data/builders/slider.ts
+++ b/src/docs/data/builders/slider.ts
@@ -72,6 +72,11 @@ const builder = builderSchema(BUILDER_NAME, {
 			description: 'A callback that is called when the value of the slider changes.',
 			see: SEE.CHANGE_FUNCTIONS,
 		},
+		{
+			name: 'onValueCommitted',
+			type: '(value: number[]) => void',
+			description: 'The callback invoked when the user has committed the value of the slider.',
+		},
 	],
 	elements: [
 		{
diff --git a/src/docs/data/builders/toast.ts b/src/docs/data/builders/toast.ts
index 04f445dd..0653a39a 100644
--- a/src/docs/data/builders/toast.ts
+++ b/src/docs/data/builders/toast.ts
@@ -20,6 +20,12 @@ const OPTION_PROPS = [
 		default: "'foreground'",
 		description: 'The sensitivity of the toast for accessibility purposes.',
 	},
+	{
+		name: 'hover',
+		type: ["'pause'", "'pause-all'", 'null'],
+		default: "'pause'",
+		description: 'The behavior when a toast is hovered.',
+	},
 ];
 
 const BUILDER_NAME = 'toast';
@@ -129,7 +135,7 @@ const keyboard: KeyboardSchema = [
 ];
 
 const schemas = [builder, content, title, description, close];
-const features = ['Automatically closes', 'Resets closing on hover'];
+const features = ['Automatically closes', 'Pause closing on hover (single, all, none)'];
 
 export const toastData: BuilderData = {
 	schemas,
diff --git a/src/docs/data/builders/tooltip.ts b/src/docs/data/builders/tooltip.ts
index 05243a89..63508b47 100644
--- a/src/docs/data/builders/tooltip.ts
+++ b/src/docs/data/builders/tooltip.ts
@@ -16,7 +16,7 @@ import type { BuilderData } from './index.js';
 const OPTION_PROPS = [
 	PROPS.POSITIONING({ default: "position: 'top'" }),
 	PROPS.ARROW_SIZE,
-	PROPS.CLOSE_ON_ESCAPE,
+	PROPS.ESCAPE_BEHAVIOR,
 	PROPS.FORCE_VISIBLE,
 	PROPS.PORTAL,
 	{
diff --git a/src/docs/data/long-types/floating-config.ts b/src/docs/data/long-types/floating-config.ts
index 029bf3b9..d7052823 100644
--- a/src/docs/data/long-types/floating-config.ts
+++ b/src/docs/data/long-types/floating-config.ts
@@ -1,3 +1,5 @@
+import type { FlipOptions } from '@floating-ui/core';
+
 type FloatingConfig = {
 	/**
 	 * The initial placement of the floating element.
@@ -56,7 +58,7 @@ type FloatingConfig = {
 	 *
 	 * @see https://floating-ui.com/docs/flip
 	 */
-	flip?: boolean;
+	flip?: boolean | FlipOptions;
 
 	/**
 	 * Whether the floating element can overlap the reference element.
diff --git a/src/docs/previews/popover/main/tailwind/Popover.svelte b/src/docs/previews/popover/main/tailwind/Popover.svelte
new file mode 100644
index 00000000..7d5191e9
--- /dev/null
+++ b/src/docs/previews/popover/main/tailwind/Popover.svelte
@@ -0,0 +1,97 @@
+<script lang="ts">
+	import { createPopover, createSync, melt } from '$lib/index.js';
+	import { fade } from 'svelte/transition';
+	import { Settings2, X } from '$icons/index.js';
+
+	export let open = false;
+
+	const {
+		elements: { trigger, content, arrow, close },
+		states,
+	} = createPopover({
+		forceVisible: true,
+	});
+
+	const sync = createSync(states);
+	$: sync.open(open, (v) => (open = v));
+</script>
+
+<button
+	type="button"
+	class="trigger"
+	use:melt={$trigger}
+	aria-label="Update dimensions"
+>
+	<Settings2 class="size-4" />
+	<span class="sr-only">Open Popover</span>
+</button>
+
+{#if open}
+	<div
+		use:melt={$content}
+		transition:fade={{ duration: 100 }}
+		class="force-dark content"
+	>
+		<div use:melt={$arrow} />
+		<div class="flex flex-col gap-2.5">
+			<p class="mb-2 font-medium text-neutral-900">Dimensions</p>
+			<fieldset class="flex items-center gap-5">
+				<label class="w-[75px] text-sm text-neutral-700" for="width"
+					>Width</label
+				>
+				<input type="number" id="width" class="input" placeholder="Width" />
+			</fieldset>
+			<fieldset class="flex items-center gap-5">
+				>
+				<label class="w-[75px] text-sm text-neutral-700" for="height"
+					>Height</label
+				>
+				<input type="number" id="height" class="input" placeholder="Height" />
+			</fieldset>
+			<fieldset class="flex items-center gap-5">
+				>
+				<label class="w-[75px] text-sm text-neutral-700" for="depth"
+					>Depth</label
+				>
+				<input type="number" id="depth" class="input" placeholder="Depth" />
+			</fieldset>
+			<fieldset class="flex items-center gap-5">
+				>
+				<label class="w-[75px] text-sm text-neutral-700" for="weight"
+					>Weight</label
+				>
+				<input type="number" id="weight" class="input" placeholder="Weight" />
+			</fieldset>
+		</div>
+		<button class="close" use:melt={$close}>
+			<X class="size-4" />
+		</button>
+	</div>
+{/if}
+
+<style lang="postcss">
+	.input {
+		@apply flex h-8 w-full rounded-md border border-magnum-800 bg-transparent px-2.5 text-sm;
+		@apply ring-offset-magnum-300 focus-visible:ring;
+		@apply focus-visible:ring-magnum-400 focus-visible:ring-offset-1;
+		@apply flex-1 items-center justify-center;
+		@apply px-2.5 text-sm leading-none text-magnum-700;
+	}
+
+	.trigger {
+		@apply inline-flex h-9 w-9 items-center justify-center rounded-full bg-white p-0;
+		@apply text-sm font-medium text-magnum-900 transition-colors hover:bg-white/90;
+		@apply focus-visible:ring focus-visible:ring-magnum-400 focus-visible:ring-offset-2;
+	}
+
+	.close {
+		@apply absolute right-1.5 top-1.5 flex h-7 w-7 items-center justify-center rounded-full;
+		@apply text-magnum-900 transition-colors hover:bg-magnum-500/10;
+		@apply focus-visible:ring focus-visible:ring-magnum-400 focus-visible:ring-offset-2;
+		@apply bg-white p-0 text-sm font-medium;
+	}
+
+	.content {
+		@apply z-10 w-60 rounded-[4px] bg-white p-5 shadow-sm;
+	}
+</style>
diff --git a/src/docs/previews/popover/main/tailwind/index.svelte b/src/docs/previews/popover/main/tailwind/index.svelte
index 5e0258b4..b476cab2 100644
--- a/src/docs/previews/popover/main/tailwind/index.svelte
+++ b/src/docs/previews/popover/main/tailwind/index.svelte
@@ -1,93 +1,5 @@
 <script lang="ts">
-	import { createPopover, melt } from '$lib/index.js';
-	import { fade } from 'svelte/transition';
-	import { Settings2, X } from '$icons/index.js';
-
-	const {
-		elements: { trigger, content, arrow, close },
-		states: { open },
-	} = createPopover({
-		forceVisible: true,
-	});
+	import Popover from './Popover.svelte';
 </script>
 
-<button
-	type="button"
-	class="trigger"
-	use:melt={$trigger}
-	aria-label="Update dimensions"
->
-	<Settings2 class="size-4" />
-	<span class="sr-only">Open Popover</span>
-</button>
-
-{#if $open}
-	<div
-		use:melt={$content}
-		transition:fade={{ duration: 100 }}
-		class="force-dark content"
-	>
-		<div use:melt={$arrow} />
-		<div class="flex flex-col gap-2.5">
-			<p>Dimensions</p>
-			<fieldset>
-				<label for="width">Width</label>
-				<input type="number" id="width" class="input" placeholder="Width" />
-			</fieldset>
-			<fieldset>
-				<label for="height">Height</label>
-				<input type="number" id="height" class="input" placeholder="Height" />
-			</fieldset>
-			<fieldset>
-				<label for="depth">Depth</label>
-				<input type="number" id="depth" class="input" placeholder="Depth" />
-			</fieldset>
-			<fieldset>
-				<label for="weight">Weight</label>
-				<input type="number" id="weight" class="input" placeholder="Weight" />
-			</fieldset>
-		</div>
-		<button class="close" use:melt={$close}>
-			<X class="size-4" />
-		</button>
-	</div>
-{/if}
-
-<style lang="postcss">
-	fieldset {
-		@apply flex items-center gap-5;
-	}
-
-	label {
-		@apply w-[75px] text-sm text-neutral-700;
-	}
-
-	p {
-		@apply mb-2 font-medium text-neutral-900;
-	}
-
-	.input {
-		@apply flex h-8 w-full rounded-md border border-magnum-800 bg-transparent px-2.5 text-sm;
-		@apply ring-offset-magnum-300 focus-visible:ring;
-		@apply focus-visible:ring-magnum-400 focus-visible:ring-offset-1;
-		@apply flex-1 items-center justify-center;
-		@apply px-2.5 text-sm leading-none text-magnum-700;
-	}
-
-	.trigger {
-		@apply inline-flex h-9 w-9 items-center justify-center rounded-full bg-white p-0;
-		@apply text-sm font-medium text-magnum-900 transition-colors hover:bg-white/90;
-		@apply focus-visible:ring focus-visible:ring-magnum-400 focus-visible:ring-offset-2;
-	}
-
-	.close {
-		@apply absolute right-1.5 top-1.5 flex h-7 w-7 items-center justify-center rounded-full;
-		@apply text-magnum-900 transition-colors hover:bg-magnum-500/10;
-		@apply focus-visible:ring focus-visible:ring-magnum-400 focus-visible:ring-offset-2;
-		@apply bg-white p-0 text-sm font-medium;
-	}
-
-	.content {
-		@apply z-10 w-60 rounded-[4px] bg-white p-5 shadow-sm;
-	}
-</style>
+<Popover />
diff --git a/src/docs/previews/toast/progress/tailwind/index.svelte b/src/docs/previews/toast/progress/tailwind/index.svelte
index e659ddda..58473fa8 100644
--- a/src/docs/previews/toast/progress/tailwind/index.svelte
+++ b/src/docs/previews/toast/progress/tailwind/index.svelte
@@ -7,16 +7,20 @@
 </script>
 
 <script lang="ts">
-	import { createToaster } from '$lib/index.js';
+	import { writable, type Writable } from 'svelte/store';
+	import { createToaster, type CreateToasterProps } from '$lib/index.js';
 	import { flip } from 'svelte/animate';
 	import Toast from './toast.svelte';
+	import SelectHover from './select-hover.svelte';
 
-	const {
+	const hover: Writable<CreateToasterProps['hover']> = writable('pause');
+
+	$: ({
 		elements,
 		helpers: { addToast },
 		states: { toasts },
 		actions: { portal },
-	} = createToaster<ToastData>();
+	} = createToaster<ToastData>({ hover: $hover }));
 
 	const toastData: ToastData[] = [
 		{
@@ -41,6 +45,8 @@
 	}
 </script>
 
+<SelectHover bind:value={$hover} />
+
 <button
 	class="inline-flex items-center justify-center rounded-xl bg-white px-4 py-3
 	font-medium leading-none text-magnum-700 shadow hover:opacity-75"
diff --git a/src/docs/previews/toast/progress/tailwind/select-hover.svelte b/src/docs/previews/toast/progress/tailwind/select-hover.svelte
new file mode 100644
index 00000000..104da266
--- /dev/null
+++ b/src/docs/previews/toast/progress/tailwind/select-hover.svelte
@@ -0,0 +1,82 @@
+<script lang="ts">
+	import { Check, ChevronDown } from '$icons/index.js';
+	import { createSelect, melt, type CreateToasterProps } from '$lib/index.js';
+	import { fade } from 'svelte/transition';
+
+	const options: { value: CreateToasterProps['hover']; label: string }[] = [
+		{ value: 'pause', label: 'pause' },
+		{ value: 'pause-all', label: 'pause-all' },
+		{ value: null, label: 'null' },
+	];
+
+	const {
+		elements: { trigger, menu, option, label },
+		states: { selected, selectedLabel, open },
+		helpers: { isSelected },
+	} = createSelect<CreateToasterProps['hover']>({
+		forceVisible: true,
+		positioning: {
+			placement: 'bottom',
+			fitViewport: true,
+			sameWidth: true,
+		},
+		defaultSelected: { value: 'pause', label: 'pause' },
+	});
+
+	export let value: CreateToasterProps['hover'];
+
+	$: value = $selected?.value;
+</script>
+
+<div class="absolute left-4 top-4">
+	<div class="flex flex-col gap-1">
+		<!-- svelte-ignore a11y-label-has-associated-control - $label contains the 'for' attribute -->
+		<label class="block text-white" use:melt={$label}> Hover behavior : </label>
+		<button
+			class="flex h-10 min-w-[220px] items-center justify-between rounded-lg bg-white px-3 py-2
+	text-magnum-700 shadow transition-opacity hover:opacity-90"
+			use:melt={$trigger}
+			aria-label="Hover behavior"
+		>
+			{$selectedLabel || 'Select hover behavior'}
+			<ChevronDown class="size-5" />
+		</button>
+		{#if $open}
+			<div
+				class="force-dark z-10 flex max-h-[300px] flex-col
+		overflow-y-auto rounded-lg bg-white p-1
+		shadow focus:!ring-0"
+				use:melt={$menu}
+				transition:fade={{ duration: 150 }}
+			>
+				{#each options as { value, label }}
+					<div
+						class="relative cursor-pointer rounded-lg py-1 pl-8 pr-4 text-neutral-800
+							hover:bg-magnum-100 focus:z-10
+							focus:text-magnum-700
+							data-[highlighted]:bg-magnum-200 data-[highlighted]:text-magnum-900
+							data-[disabled]:opacity-50"
+						use:melt={$option({ value, label })}
+					>
+						<div class="check {$isSelected(value) ? 'block' : 'hidden'}">
+							<Check class="size-4" />
+						</div>
+
+						{label}
+					</div>
+				{/each}
+			</div>
+		{/if}
+	</div>
+</div>
+
+<style lang="postcss">
+	.check {
+		position: absolute;
+		left: theme(spacing.2);
+		top: 50%;
+		z-index: theme(zIndex.20);
+		translate: 0 calc(-50% + 1px);
+		color: theme(colors.magnum.500);
+	}
+</style>
diff --git a/src/lib/builders/checkbox/create.ts b/src/lib/builders/checkbox/create.ts
index b139d4f1..2a394c76 100644
--- a/src/lib/builders/checkbox/create.ts
+++ b/src/lib/builders/checkbox/create.ts
@@ -10,9 +10,10 @@ import {
 	toWritableStores,
 } from '$lib/internal/helpers/index.js';
 import type { Defaults, MeltActionReturn } from '$lib/internal/types.js';
-import { derived, writable } from 'svelte/store';
+import { derived, readable, writable } from 'svelte/store';
 import type { CheckboxEvents } from './events.js';
 import type { CreateCheckboxProps } from './types.js';
+import { createHiddenInput } from '../hidden-input/create.js';
 
 const defaults = {
 	disabled: false,
@@ -68,28 +69,14 @@ export function createCheckbox(props?: CreateCheckboxProps) {
 		},
 	});
 
-	const input = makeElement('checkbox-input', {
-		stores: [checked, name, value, required, disabled],
-		returned: ([$checked, $name, $value, $required, $disabled]) => {
-			return {
-				type: 'checkbox',
-				'aria-hidden': true,
-				hidden: true,
-				tabindex: -1,
-				name: $name,
-				value: $value,
-				checked: $checked === 'indeterminate' ? false : $checked,
-				required: $required,
-				disabled: disabledAttr($disabled),
-				style: styleToString({
-					position: 'absolute',
-					opacity: 0,
-					'pointer-events': 'none',
-					margin: 0,
-					transform: 'translateX(-100%)',
-				}),
-			} as const;
-		},
+	const input = createHiddenInput({
+		value,
+		checked,
+		type: 'checkbox',
+		name: name,
+		disabled: disabled,
+		required,
+		prefix: 'checkbox',
 	});
 
 	const isIndeterminate = derived(checked, ($checked) => $checked === 'indeterminate');
diff --git a/src/lib/builders/combobox/create.ts b/src/lib/builders/combobox/create.ts
index 115e65e2..f86ed69c 100644
--- a/src/lib/builders/combobox/create.ts
+++ b/src/lib/builders/combobox/create.ts
@@ -1,4 +1,3 @@
-import { useEscapeKeydown } from '$lib/internal/actions/index.js';
 import {
 	addEventListener,
 	addMeltEventListener,
@@ -9,7 +8,6 @@ import {
 	isContentEditable,
 	isHTMLInputElement,
 	kbd,
-	noop,
 	omit,
 } from '$lib/internal/helpers/index.js';
 import type { MeltActionReturn } from '$lib/internal/types.js';
@@ -71,24 +69,12 @@ export function createCombobox<
 				})
 			);
 
-			let unsubEscapeKeydown = noop;
-
-			const escape = useEscapeKeydown(node, {
-				handler: () => {
-					listbox.helpers.closeMenu();
-				},
-			});
-			if (escape && escape.destroy) {
-				unsubEscapeKeydown = escape.destroy;
-			}
-
 			const { destroy } = listbox.elements.trigger(node);
 
 			return {
 				destroy() {
 					destroy?.();
 					unsubscribe();
-					unsubEscapeKeydown();
 				},
 			};
 		},
diff --git a/src/lib/builders/context-menu/create.ts b/src/lib/builders/context-menu/create.ts
index 56d68738..07fbab4b 100644
--- a/src/lib/builders/context-menu/create.ts
+++ b/src/lib/builders/context-menu/create.ts
@@ -46,9 +46,9 @@ const defaults = {
 		placement: 'bottom-start',
 	},
 	preventScroll: true,
-	closeOnEscape: true,
+	escapeBehavior: 'close',
 	closeOnOutsideClick: true,
-	portal: undefined,
+	portal: 'body',
 	loop: false,
 	dir: 'ltr',
 	defaultOpen: false,
@@ -72,7 +72,7 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 		closeOnOutsideClick,
 		portal,
 		forceVisible,
-		closeOnEscape,
+		escapeBehavior,
 		loop,
 		preventTextSelectionOverflow,
 	} = rootOptions;
@@ -149,15 +149,8 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 			let unsubPopper = noop;
 
 			const unsubDerived = effect(
-				[isVisible, rootActiveTrigger, positioning, closeOnOutsideClick, portal, closeOnEscape],
-				([
-					$isVisible,
-					$rootActiveTrigger,
-					$positioning,
-					$closeOnOutsideClick,
-					$portal,
-					$closeOnEscape,
-				]) => {
+				[isVisible, rootActiveTrigger, positioning, closeOnOutsideClick, portal],
+				([$isVisible, $rootActiveTrigger, $positioning, $closeOnOutsideClick, $portal]) => {
 					unsubPopper();
 					if (!$isVisible || !$rootActiveTrigger) return;
 					tick().then(() => {
@@ -177,7 +170,7 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 									shouldCloseOnInteractOutside: handleClickOutside,
 								},
 								portal: getPortalDestination(node, $portal),
-								escapeKeydown: $closeOnEscape ? undefined : null,
+								escapeKeydown: { behaviorType: escapeBehavior },
 								preventTextSelectionOverflow: { enabled: preventTextSelectionOverflow },
 							},
 						}).destroy;
diff --git a/src/lib/builders/date-picker/create.ts b/src/lib/builders/date-picker/create.ts
index 2bb4e2aa..15facacb 100644
--- a/src/lib/builders/date-picker/create.ts
+++ b/src/lib/builders/date-picker/create.ts
@@ -25,7 +25,7 @@ const defaults = {
 	positioning: {
 		placement: 'bottom',
 	},
-	closeOnEscape: true,
+	escapeBehavior: 'close',
 	closeOnOutsideClick: true,
 	onOutsideClick: undefined,
 	preventScroll: false,
@@ -77,7 +77,7 @@ export function createDatePicker(props?: CreateDatePickerProps) {
 		defaultOpen: withDefaults.defaultOpen,
 		open: withDefaults.open,
 		disableFocusTrap: withDefaults.disableFocusTrap,
-		closeOnEscape: withDefaults.closeOnEscape,
+		escapeBehavior: withDefaults.escapeBehavior,
 		preventScroll: withDefaults.preventScroll,
 		onOpenChange: withDefaults.onOpenChange,
 		closeOnOutsideClick: withDefaults.closeOnOutsideClick,
diff --git a/src/lib/builders/date-range-picker/create.ts b/src/lib/builders/date-range-picker/create.ts
index 66664ca4..71dedc4c 100644
--- a/src/lib/builders/date-range-picker/create.ts
+++ b/src/lib/builders/date-range-picker/create.ts
@@ -25,7 +25,7 @@ const defaults = {
 	positioning: {
 		placement: 'bottom',
 	},
-	closeOnEscape: true,
+	escapeBehavior: 'close',
 	closeOnOutsideClick: true,
 	preventScroll: false,
 	forceVisible: false,
@@ -61,7 +61,7 @@ export function createDateRangePicker(props?: CreateDateRangePickerProps) {
 		defaultOpen: withDefaults.defaultOpen,
 		open: withDefaults.open,
 		disableFocusTrap: withDefaults.disableFocusTrap,
-		closeOnEscape: withDefaults.closeOnEscape,
+		escapeBehavior: withDefaults.escapeBehavior,
 		preventScroll: withDefaults.preventScroll,
 		onOpenChange: withDefaults.onOpenChange,
 		closeOnOutsideClick: withDefaults.closeOnOutsideClick,
diff --git a/src/lib/builders/dialog/create.ts b/src/lib/builders/dialog/create.ts
index 8fe4d9ea..6850a06c 100644
--- a/src/lib/builders/dialog/create.ts
+++ b/src/lib/builders/dialog/create.ts
@@ -39,11 +39,11 @@ const { name } = createElHelpers<DialogParts>('dialog');
 
 const defaults = {
 	preventScroll: true,
-	closeOnEscape: true,
+	escapeBehavior: 'close',
 	closeOnOutsideClick: true,
 	role: 'dialog',
 	defaultOpen: false,
-	portal: undefined,
+	portal: 'body',
 	forceVisible: false,
 	openFocus: undefined,
 	closeFocus: undefined,
@@ -60,7 +60,7 @@ export function createDialog(props?: CreateDialogProps) {
 
 	const {
 		preventScroll,
-		closeOnEscape,
+		escapeBehavior,
 		closeOnOutsideClick,
 		role,
 		portal,
@@ -135,26 +135,6 @@ export function createDialog(props?: CreateDialogProps) {
 				'data-state': $open ? 'open' : 'closed',
 			} as const;
 		},
-		action: (node: HTMLElement) => {
-			let unsubEscapeKeydown = noop;
-
-			if (closeOnEscape.get()) {
-				const escapeKeydown = useEscapeKeydown(node, {
-					handler: () => {
-						handleClose();
-					},
-				});
-				if (escapeKeydown && escapeKeydown.destroy) {
-					unsubEscapeKeydown = escapeKeydown.destroy;
-				}
-			}
-
-			return {
-				destroy() {
-					unsubEscapeKeydown();
-				},
-			};
-		},
 	});
 
 	const content = makeElement(name('content'), {
@@ -174,13 +154,13 @@ export function createDialog(props?: CreateDialogProps) {
 		},
 
 		action: (node: HTMLElement) => {
-			let unsubModal = noop;
 			let unsubEscape = noop;
+			let unsubModal = noop;
 			let unsubFocusTrap = noop;
 
 			const unsubDerived = effect(
-				[isVisible, closeOnOutsideClick, closeOnEscape],
-				([$isVisible, $closeOnOutsideClick, $closeOnEscape]) => {
+				[isVisible, closeOnOutsideClick],
+				([$isVisible, $closeOnOutsideClick]) => {
 					unsubModal();
 					unsubEscape();
 					unsubFocusTrap();
@@ -198,7 +178,7 @@ export function createDialog(props?: CreateDialogProps) {
 
 					unsubEscape = useEscapeKeydown(node, {
 						handler: handleClose,
-						enabled: $closeOnEscape,
+						behaviorType: escapeBehavior,
 					}).destroy;
 
 					unsubFocusTrap = useFocusTrap(node, { fallbackFocus: node }).destroy;
diff --git a/src/lib/builders/dialog/types.ts b/src/lib/builders/dialog/types.ts
index 734e7263..99d5d7b1 100644
--- a/src/lib/builders/dialog/types.ts
+++ b/src/lib/builders/dialog/types.ts
@@ -2,6 +2,8 @@ import type { ChangeFn, FocusProp, IdObj } from '$lib/internal/helpers/index.js'
 import type { BuilderReturn } from '$lib/internal/types.js';
 import type { Writable } from 'svelte/store';
 import type { DialogIdParts, createDialog } from './create.js';
+import type { PortalConfig } from '$lib/internal/actions/portal.js';
+import type { EscapeBehaviorType } from '$lib/internal/actions/index.js';
 export type { DialogComponentEvents } from './events.js';
 export type CreateDialogProps = {
 	/**
@@ -13,11 +15,15 @@ export type CreateDialogProps = {
 	preventScroll?: boolean;
 
 	/**
-	 * If true, the dialog will close when the user presses the escape key.
+	 * Escape behavior type.
+	 * `close`: Closes the element immediately.
+	 * `defer-otherwise-close`: Delegates the action to the parent element. If no parent is found, it closes the element.
+	 * `defer-otherwise-ignore`: Delegates the action to the parent element. If no parent is found, nothing is done.
+	 * `ignore`: Prevents the element from closing and also blocks the parent element from closing in response to the Escape key.
 	 *
-	 * @default true
+	 * @defaultValue `close`
 	 */
-	closeOnEscape?: boolean;
+	escapeBehavior?: EscapeBehaviorType;
 
 	/**
 	 * If true, the dialog will close when the user clicks outside of it.
@@ -65,7 +71,7 @@ export type CreateDialogProps = {
 	 *
 	 * @default 'body'
 	 */
-	portal?: HTMLElement | string | null;
+	portal?: PortalConfig | null;
 
 	/**
 	 * If true, the dialog will be visible regardless of the open state.
diff --git a/src/lib/builders/dropdown-menu/create.ts b/src/lib/builders/dropdown-menu/create.ts
index 536bab35..39428146 100644
--- a/src/lib/builders/dropdown-menu/create.ts
+++ b/src/lib/builders/dropdown-menu/create.ts
@@ -11,9 +11,9 @@ const defaults = {
 		placement: 'bottom',
 	},
 	preventScroll: true,
-	closeOnEscape: true,
+	escapeBehavior: 'close',
 	closeOnOutsideClick: true,
-	portal: undefined,
+	portal: 'body',
 	loop: false,
 	dir: 'ltr',
 	defaultOpen: false,
diff --git a/src/lib/builders/hidden-input/create.ts b/src/lib/builders/hidden-input/create.ts
index 2c9d73bc..136793ee 100644
--- a/src/lib/builders/hidden-input/create.ts
+++ b/src/lib/builders/hidden-input/create.ts
@@ -6,12 +6,16 @@ import { toReadableStores } from '$lib/internal/helpers/store/toReadableStores.j
 import { omit } from '$lib/internal/helpers/object.js';
 import type { Readable } from 'svelte/motion';
 import { removeUndefined } from '$lib/internal/helpers/object.js';
+import { withGet } from '$lib/internal/helpers/withGet.js';
+import { executeCallbacks } from '$lib/internal/helpers/callbacks.js';
 
 const defaults = {
 	prefix: '',
 	disabled: readable(false),
 	required: readable(false),
 	name: readable(undefined),
+	type: readable(undefined),
+	checked: undefined,
 } satisfies Partial<CreateHiddenInputProps>;
 
 export function createHiddenInput(props: CreateHiddenInputProps) {
@@ -20,12 +24,21 @@ export function createHiddenInput(props: CreateHiddenInputProps) {
 		...removeUndefined(props),
 	} satisfies CreateHiddenInputProps;
 	const { name: elName } = createElHelpers(withDefaults.prefix);
-	const { value, name, disabled, required } = toReadableStores(omit(withDefaults, 'prefix'));
+	const { value, name, disabled, required, type, checked } = toReadableStores(
+		omit(withDefaults, 'prefix')
+	);
 	const nameStore = name as Readable<string | undefined>; // TODO: Remove this cast when types are fixed
 
+	const actualChecked = withGet.derived([checked, type], ([$checked, $type]) => {
+		if ($type === 'checkbox') {
+			return ($checked === 'indeterminate' ? false : $checked) as boolean;
+		}
+		return undefined;
+	});
+
 	const hiddenInput = makeElement(elName('hidden-input'), {
-		stores: [value, nameStore, disabled, required],
-		returned: ([$value, $name, $disabled, $required]) => {
+		stores: [value, nameStore, disabled, required, type, actualChecked],
+		returned: ([$value, $name, $disabled, $required, $type, $checked]) => {
 			return {
 				name: $name,
 				value: $value?.toString(),
@@ -34,6 +47,8 @@ export function createHiddenInput(props: CreateHiddenInputProps) {
 				disabled: $disabled,
 				required: $required,
 				tabIndex: -1,
+				type: $type,
+				checked: $checked,
 				style: styleToString({
 					position: 'absolute',
 					opacity: 0,
@@ -45,10 +60,21 @@ export function createHiddenInput(props: CreateHiddenInputProps) {
 		},
 		action: (node: HTMLInputElement) => {
 			// When value changes, emit a change event
-			const unsub = value.subscribe((newValue) => {
-				node.value = newValue;
-				node.dispatchEvent(new Event('change', { bubbles: true }));
-			});
+			const unsub = executeCallbacks(
+				value.subscribe((newValue) => {
+					if (type.get() === 'checkbox') {
+						return;
+					}
+					node.value = newValue;
+					node.dispatchEvent(new Event('change', { bubbles: true }));
+				}),
+				actualChecked.subscribe(() => {
+					if (type.get() !== 'checkbox') {
+						return;
+					}
+					node.dispatchEvent(new Event('change', { bubbles: true }));
+				})
+			);
 
 			return {
 				destroy: unsub,
diff --git a/src/lib/builders/hidden-input/types.ts b/src/lib/builders/hidden-input/types.ts
index 818c1836..946c9e2b 100644
--- a/src/lib/builders/hidden-input/types.ts
+++ b/src/lib/builders/hidden-input/types.ts
@@ -1,9 +1,12 @@
-import type { Readable } from 'svelte/store';
+import type { MaybeReadable } from '$lib/internal/types.js';
+import type { HTMLInputAttributes } from 'svelte/elements';
 
 export type CreateHiddenInputProps = {
-	value: Readable<string>;
-	disabled?: Readable<boolean>;
-	name?: Readable<string | undefined> | undefined;
-	required?: Readable<boolean>;
+	value: MaybeReadable<string>;
+	disabled?: MaybeReadable<boolean>;
+	name?: MaybeReadable<string | undefined> | undefined;
+	required?: MaybeReadable<boolean>;
 	prefix?: string;
+	type?: MaybeReadable<HTMLInputAttributes['type']>;
+	checked?: MaybeReadable<boolean | undefined | 'indeterminate'>;
 };
diff --git a/src/lib/builders/link-preview/create.ts b/src/lib/builders/link-preview/create.ts
index a7756c34..c6721151 100644
--- a/src/lib/builders/link-preview/create.ts
+++ b/src/lib/builders/link-preview/create.ts
@@ -42,8 +42,8 @@ const defaults = {
 	arrowSize: 8,
 	closeOnOutsideClick: true,
 	forceVisible: false,
-	portal: undefined,
-	closeOnEscape: true,
+	portal: 'body',
+	escapeBehavior: 'close',
 	onOutsideClick: undefined,
 	preventTextSelectionOverflow: true,
 } satisfies CreateLinkPreviewProps;
@@ -74,7 +74,7 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 		closeOnOutsideClick,
 		forceVisible,
 		portal,
-		closeOnEscape,
+		escapeBehavior,
 		onOutsideClick,
 		preventTextSelectionOverflow,
 	} = options;
@@ -177,15 +177,8 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 			let unsubPopper = noop;
 
 			const unsubDerived = effect(
-				[isVisible, activeTrigger, positioning, closeOnOutsideClick, portal, closeOnEscape],
-				([
-					$isVisible,
-					$activeTrigger,
-					$positioning,
-					$closeOnOutsideClick,
-					$portal,
-					$closeOnEscape,
-				]) => {
+				[isVisible, activeTrigger, positioning, closeOnOutsideClick, portal],
+				([$isVisible, $activeTrigger, $positioning, $closeOnOutsideClick, $portal]) => {
 					unsubPopper();
 					if (!$isVisible || !$activeTrigger) return;
 
@@ -215,7 +208,7 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 								},
 								portal: getPortalDestination(node, $portal),
 								focusTrap: null,
-								escapeKeydown: $closeOnEscape ? undefined : null,
+								escapeKeydown: { behaviorType: escapeBehavior },
 								preventTextSelectionOverflow: { enabled: preventTextSelectionOverflow },
 							},
 						}).destroy;
diff --git a/src/lib/builders/link-preview/types.ts b/src/lib/builders/link-preview/types.ts
index 323879a7..17b04ead 100644
--- a/src/lib/builders/link-preview/types.ts
+++ b/src/lib/builders/link-preview/types.ts
@@ -1,4 +1,9 @@
-import type { FloatingConfig, InteractOutsideEvent } from '$lib/internal/actions/index.js';
+import type {
+	EscapeBehaviorType,
+	FloatingConfig,
+	InteractOutsideEvent,
+	PortalConfig,
+} from '$lib/internal/actions/index.js';
 import type { ChangeFn, IdObj } from '$lib/internal/helpers/index.js';
 import type { BuilderReturn } from '$lib/internal/types.js';
 import type { Writable } from 'svelte/store';
@@ -66,12 +71,15 @@ export type CreateLinkPreviewProps = {
 	onOutsideClick?: (event: InteractOutsideEvent) => void;
 
 	/**
-	 * Whether or not to close the linkpreview when the escape key is pressed
-	 * while it is open.
+	 * Escape behavior type.
+	 * `close`: Closes the element immediately.
+	 * `defer-otherwise-close`: Delegates the action to the parent element. If no parent is found, it closes the element.
+	 * `defer-otherwise-ignore`: Delegates the action to the parent element. If no parent is found, nothing is done.
+	 * `ignore`: Prevents the element from closing and also blocks the parent element from closing in response to the Escape key.
 	 *
-	 * @default true
+	 * @defaultValue `close`
 	 */
-	closeOnEscape?: boolean;
+	escapeBehavior?: EscapeBehaviorType;
 
 	/**
 	 * Whether should prevent text selection overflowing the element when the element is the top layer.
@@ -102,7 +110,7 @@ export type CreateLinkPreviewProps = {
 	 *
 	 * @default 'body'
 	 */
-	portal?: HTMLElement | string | null;
+	portal?: PortalConfig | null;
 
 	/**
 	 * Optionally override the default ids we assign to the elements
diff --git a/src/lib/builders/listbox/create.ts b/src/lib/builders/listbox/create.ts
index c56f532d..ecdb8cb1 100644
--- a/src/lib/builders/listbox/create.ts
+++ b/src/lib/builders/listbox/create.ts
@@ -1,4 +1,4 @@
-import { useEscapeKeydown, usePopper } from '$lib/internal/actions/index.js';
+import { usePopper } from '$lib/internal/actions/index.js';
 import {
 	FIRST_LAST_KEYS,
 	addHighlight,
@@ -66,9 +66,9 @@ const defaults = {
 	defaultOpen: false,
 	closeOnOutsideClick: true,
 	preventScroll: true,
-	closeOnEscape: true,
+	escapeBehavior: 'close',
 	forceVisible: false,
-	portal: undefined,
+	portal: 'body',
 	builder: 'listbox',
 	disabled: false,
 	required: false,
@@ -134,7 +134,7 @@ export function createListbox<
 		scrollAlignment,
 		loop,
 		closeOnOutsideClick,
-		closeOnEscape,
+		escapeBehavior,
 		preventScroll,
 		portal,
 		forceVisible,
@@ -277,6 +277,7 @@ export function createListbox<
 				'aria-controls': $menuId,
 				'aria-expanded': $open,
 				'aria-labelledby': $labelId,
+				'data-state': $open ? 'open' : 'closed',
 				// autocomplete: 'off',
 				id: $triggerId,
 				role: 'combobox',
@@ -433,23 +434,10 @@ export function createListbox<
 				})
 			);
 
-			let unsubEscapeKeydown = noop;
-
-			const escape = useEscapeKeydown(node, {
-				handler: closeMenu,
-				enabled: derived([open, closeOnEscape], ([$open, $closeOnEscape]) => {
-					return $open && $closeOnEscape;
-				}),
-			});
-			if (escape && escape.destroy) {
-				unsubEscapeKeydown = escape.destroy;
-			}
-
 			return {
 				destroy() {
 					activeTrigger.set(null);
 					unsubscribe();
-					unsubEscapeKeydown();
 				},
 			};
 		},
@@ -506,7 +494,7 @@ export function createListbox<
 										},
 									},
 
-									escapeKeydown: null,
+									escapeKeydown: { handler: closeMenu, behaviorType: escapeBehavior },
 									portal: getPortalDestination(node, $portal),
 									preventTextSelectionOverflow: { enabled: preventTextSelectionOverflow },
 								},
diff --git a/src/lib/builders/listbox/types.ts b/src/lib/builders/listbox/types.ts
index 274a14b9..a1d06a7a 100644
--- a/src/lib/builders/listbox/types.ts
+++ b/src/lib/builders/listbox/types.ts
@@ -1,4 +1,9 @@
-import type { FloatingConfig, InteractOutsideEvent } from '$lib/internal/actions/index.js';
+import type {
+	EscapeBehaviorType,
+	FloatingConfig,
+	InteractOutsideEvent,
+	PortalConfig,
+} from '$lib/internal/actions/index.js';
 import type { ChangeFn, IdObj } from '$lib/internal/helpers/index.js';
 import type { BuilderReturn, WhenTrue } from '$lib/internal/types.js';
 import type { Writable } from 'svelte/store';
@@ -104,12 +109,15 @@ export type CreateListboxProps<
 	closeOnOutsideClick?: boolean;
 
 	/**
-	 * Whether or not to close the listbox menu when the user presses
-	 * the escape key.
+	 * Escape behavior type.
+	 * `close`: Closes the element immediately.
+	 * `defer-otherwise-close`: Delegates the action to the parent element. If no parent is found, it closes the element.
+	 * `defer-otherwise-ignore`: Delegates the action to the parent element. If no parent is found, nothing is done.
+	 * `ignore`: Prevents the element from closing and also blocks the parent element from closing in response to the Escape key.
 	 *
-	 * @default true
+	 * @defaultValue `close`
 	 */
-	closeOnEscape?: boolean;
+	escapeBehavior?: EscapeBehaviorType;
 
 	/**
 	 * A custom event handler for the "outside click" event, which
@@ -139,7 +147,7 @@ export type CreateListboxProps<
 	 *
 	 * @default 'body'
 	 */
-	portal?: HTMLElement | string | null;
+	portal?: PortalConfig | null;
 
 	/**
 	 * Whether the menu content should be displayed even if it is not open.
diff --git a/src/lib/builders/menu/create.ts b/src/lib/builders/menu/create.ts
index 2557537a..999479cd 100644
--- a/src/lib/builders/menu/create.ts
+++ b/src/lib/builders/menu/create.ts
@@ -1,5 +1,5 @@
 import { createSeparator } from '$lib/builders/index.js';
-import { useEscapeKeydown, usePopper, usePortal } from '$lib/internal/actions/index.js';
+import { usePopper, usePortal } from '$lib/internal/actions/index.js';
 import {
 	FIRST_LAST_KEYS,
 	SELECTION_KEYS,
@@ -71,9 +71,9 @@ const defaults = {
 		placement: 'bottom',
 	},
 	preventScroll: true,
-	closeOnEscape: true,
+	escapeBehavior: 'close',
 	closeOnOutsideClick: true,
-	portal: undefined,
+	portal: 'body',
 	loop: false,
 	dir: 'ltr',
 	defaultOpen: false,
@@ -90,7 +90,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		preventScroll,
 		arrowSize,
 		positioning,
-		closeOnEscape,
+		escapeBehavior,
 		closeOnOutsideClick,
 		portal,
 		forceVisible,
@@ -182,15 +182,8 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 			let unsubPopper = noop;
 
 			const unsubDerived = effect(
-				[isVisible, rootActiveTrigger, positioning, closeOnOutsideClick, portal, closeOnEscape],
-				([
-					$isVisible,
-					$rootActiveTrigger,
-					$positioning,
-					$closeOnOutsideClick,
-					$portal,
-					$closeOnEscape,
-				]) => {
+				[isVisible, rootActiveTrigger, positioning, closeOnOutsideClick, portal],
+				([$isVisible, $rootActiveTrigger, $positioning, $closeOnOutsideClick, $portal]) => {
 					unsubPopper();
 					if (!$isVisible || !$rootActiveTrigger) return;
 					tick().then(() => {
@@ -218,7 +211,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 									onClose: () => rootOpen.set(false),
 								},
 								portal: getPortalDestination(node, $portal),
-								escapeKeydown: $closeOnEscape ? undefined : null,
+								escapeKeydown: { behaviorType: escapeBehavior },
 								preventTextSelectionOverflow: { enabled: preventTextSelectionOverflow },
 							},
 						}).destroy;
@@ -354,17 +347,6 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 			} as const;
 		},
 		action: (node: HTMLElement) => {
-			let unsubEscapeKeydown = noop;
-
-			if (closeOnEscape.get()) {
-				const escapeKeydown = useEscapeKeydown(node, {
-					handler: () => rootOpen.set(false),
-				});
-				if (escapeKeydown && escapeKeydown.destroy) {
-					unsubEscapeKeydown = escapeKeydown.destroy;
-				}
-			}
-
 			const unsubPortal = effect([portal], ([$portal]) => {
 				if ($portal === null) return noop;
 				const portalDestination = getPortalDestination(node, $portal);
@@ -374,7 +356,6 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 
 			return {
 				destroy() {
-					unsubEscapeKeydown();
 					unsubPortal();
 				},
 			};
@@ -1126,14 +1107,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 			);
 		};
 
-		const keydownListener = (e: KeyboardEvent) => {
-			if (e.key === kbd.ESCAPE && closeOnEscape.get()) {
-				rootOpen.set(false);
-				return;
-			}
-		};
 		unsubs.push(addEventListener(document, 'keydown', handleKeyDown, { capture: true }));
-		unsubs.push(addEventListener(document, 'keydown', keydownListener));
 
 		return () => {
 			unsubs.forEach((unsub) => unsub());
@@ -1191,25 +1165,6 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		};
 	});
 
-	effect(rootOpen, ($rootOpen) => {
-		if (!isBrowser) return;
-
-		const handlePointer = () => isUsingKeyboard.set(false);
-		const handleKeyDown = (e: KeyboardEvent) => {
-			isUsingKeyboard.set(true);
-			if (e.key === kbd.ESCAPE && $rootOpen && closeOnEscape.get()) {
-				rootOpen.set(false);
-				return;
-			}
-		};
-
-		return executeCallbacks(
-			addEventListener(document, 'pointerdown', handlePointer, { capture: true, once: true }),
-			addEventListener(document, 'pointermove', handlePointer, { capture: true, once: true }),
-			addEventListener(document, 'keydown', handleKeyDown, { capture: true })
-		);
-	});
-
 	function handleOpen(triggerEl: HTMLElement) {
 		rootOpen.update((prev) => {
 			const isOpen = !prev;
diff --git a/src/lib/builders/menu/types.ts b/src/lib/builders/menu/types.ts
index 4dfdb5fc..73500eeb 100644
--- a/src/lib/builders/menu/types.ts
+++ b/src/lib/builders/menu/types.ts
@@ -1,4 +1,9 @@
-import type { FloatingConfig, InteractOutsideEvent } from '$lib/internal/actions/index.js';
+import type {
+	EscapeBehaviorType,
+	FloatingConfig,
+	InteractOutsideEvent,
+	PortalConfig,
+} from '$lib/internal/actions/index.js';
 import type { TextDirection } from '$lib/internal/types.js';
 import type { ChangeFn, FocusProp, IdObj } from '$lib/internal/helpers/index.js';
 import type { Writable } from 'svelte/store';
@@ -34,11 +39,15 @@ export type _CreateMenuProps = {
 	preventScroll?: boolean;
 
 	/**
-	 * Whether or not to close the menu when the escape key is pressed.
+	 * Escape behavior type.
+	 * `close`: Closes the element immediately.
+	 * `defer-otherwise-close`: Delegates the action to the parent element. If no parent is found, it closes the element.
+	 * `defer-otherwise-ignore`: Delegates the action to the parent element. If no parent is found, nothing is done.
+	 * `ignore`: Prevents the element from closing and also blocks the parent element from closing in response to the Escape key.
 	 *
-	 * @default true
+	 * @defaultValue `close`
 	 */
-	closeOnEscape?: boolean;
+	escapeBehavior?: EscapeBehaviorType;
 
 	/**
 	 * Whether or not to close the menu when an internal item is clicked.
@@ -52,7 +61,7 @@ export type _CreateMenuProps = {
 	 *
 	 * @default 'body'
 	 */
-	portal?: HTMLElement | string | null;
+	portal?: PortalConfig | null;
 
 	/**
 	 * Whether or not to close the menu when a click occurs outside of it.
@@ -189,7 +198,7 @@ export type _MenuBuilderOptions = {
 		preventScroll: WithGet<Writable<boolean | undefined>>;
 		loop: WithGet<Writable<boolean | undefined>>;
 		dir: WithGet<Writable<TextDirection>>;
-		closeOnEscape: WithGet<Writable<boolean>>;
+		escapeBehavior: WithGet<Writable<EscapeBehaviorType>>;
 		closeOnOutsideClick: WithGet<Writable<boolean>>;
 		preventTextSelectionOverflow: WithGet<Writable<boolean>>;
 		portal: WithGet<Writable<string | HTMLElement | undefined | null>>;
diff --git a/src/lib/builders/menubar/create.ts b/src/lib/builders/menubar/create.ts
index 7aa0e332..4e7f7fba 100644
--- a/src/lib/builders/menubar/create.ts
+++ b/src/lib/builders/menubar/create.ts
@@ -2,7 +2,6 @@ import { usePopper } from '$lib/internal/actions/index.js';
 import {
 	FIRST_LAST_KEYS,
 	SELECTION_KEYS,
-	addEventListener,
 	addHighlight,
 	addMeltEventListener,
 	makeElement,
@@ -49,7 +48,7 @@ const { name } = createElHelpers<_MenuParts | 'menu'>('menubar');
 
 const defaults = {
 	loop: true,
-	closeOnEscape: true,
+	escapeBehavior: 'close',
 	preventScroll: false,
 } satisfies CreateMenubarProps;
 
@@ -60,7 +59,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 	const withDefaults = { ...defaults, ...props } satisfies CreateMenubarProps;
 
 	const options = toWritableStores(omit(withDefaults, 'ids'));
-	const { loop, closeOnEscape, preventScroll } = options;
+	const { loop, escapeBehavior, preventScroll } = options;
 	const activeMenu = withGet(writable<string>(''));
 
 	const nextFocusable = withGet(writable<HTMLElement | null>(null));
@@ -99,9 +98,9 @@ export function createMenubar(props?: CreateMenubarProps) {
 		arrowSize: 8,
 		dir: 'ltr',
 		loop: false,
-		closeOnEscape: true,
+		escapeBehavior: 'close',
 		closeOnOutsideClick: true,
-		portal: undefined,
+		portal: 'body',
 		forceVisible: false,
 		defaultOpen: false,
 		typeahead: true,
@@ -207,6 +206,10 @@ export function createMenubar(props?: CreateMenubarProps) {
 											activeMenu.set('');
 										},
 									},
+									escapeKeydown: {
+										behaviorType: escapeBehavior,
+										handler: () => activeMenu.set(''),
+									},
 									preventTextSelectionOverflow: { enabled: preventTextSelectionOverflow },
 								},
 							}).destroy;
@@ -585,12 +588,6 @@ export function createMenubar(props?: CreateMenubarProps) {
 				if (MENUBAR_NAV_KEYS.includes(e.key)) {
 					handleMenubarNavigation(e);
 				}
-			}),
-			addEventListener(document, 'keydown', (e) => {
-				if (closeOnEscape.get() && e.key === kbd.ESCAPE) {
-					window.clearTimeout(closeTimer.get());
-					activeMenu.set('');
-				}
 			})
 		);
 
diff --git a/src/lib/builders/menubar/types.ts b/src/lib/builders/menubar/types.ts
index 4e12e494..a579e87b 100644
--- a/src/lib/builders/menubar/types.ts
+++ b/src/lib/builders/menubar/types.ts
@@ -1,3 +1,4 @@
+import type { EscapeBehaviorType } from '$lib/internal/actions/index.js';
 import type { IdObj } from '$lib/internal/helpers/id.js';
 import type { BuilderReturn } from '$lib/internal/types.js';
 import type { _Menu } from '../menu/index.js';
@@ -15,11 +16,15 @@ export type CreateMenubarProps = {
 	loop?: boolean;
 
 	/**
-	 * Whether to close the active menu when the escape key is pressed.
+	 * Escape behavior type.
+	 * `close`: Closes the element immediately.
+	 * `defer-otherwise-close`: Delegates the action to the parent element. If no parent is found, it closes the element.
+	 * `defer-otherwise-ignore`: Delegates the action to the parent element. If no parent is found, nothing is done.
+	 * `ignore`: Prevents the element from closing and also blocks the parent element from closing in response to the Escape key.
 	 *
-	 * @default true
+	 * @defaultValue `close`
 	 */
-	closeOnEscape?: boolean;
+	escapeBehavior?: EscapeBehaviorType;
 
 	/**
 	 * Whether to prevent scrolling the body when any menu within
diff --git a/src/lib/builders/pagination/helpers.ts b/src/lib/builders/pagination/helpers.ts
index 7720453e..a105dd4c 100644
--- a/src/lib/builders/pagination/helpers.ts
+++ b/src/lib/builders/pagination/helpers.ts
@@ -6,7 +6,7 @@ export function getPageItems({
 	siblingCount = 1,
 }: GetPageItemsArgs): Array<PageItem> {
 	const pageItems: Array<PageItem> = [];
-	const pagesToShow = new Set([1, totalPages]);
+	const pagesToShow = new Set([1, Math.max(totalPages, 1)]);
 	const firstItemWithSiblings = 3 + siblingCount;
 	const lastItemWithSiblings = totalPages - 2 - siblingCount;
 
diff --git a/src/lib/builders/popover/create.ts b/src/lib/builders/popover/create.ts
index 64ba76f1..3093dd14 100644
--- a/src/lib/builders/popover/create.ts
+++ b/src/lib/builders/popover/create.ts
@@ -19,14 +19,10 @@ import {
 	portalAttr,
 	generateIds,
 	withGet,
+	sleep,
 } from '$lib/internal/helpers/index.js';
 
-import {
-	useEscapeKeydown,
-	usePopper,
-	usePortal,
-	type InteractOutsideEvent,
-} from '$lib/internal/actions/index.js';
+import { usePopper, usePortal, type InteractOutsideEvent } from '$lib/internal/actions/index.js';
 import type { Defaults, MeltActionReturn } from '$lib/internal/types.js';
 import { writable } from 'svelte/store';
 import type { PopoverEvents } from './events.js';
@@ -40,11 +36,11 @@ const defaults = {
 	arrowSize: 8,
 	defaultOpen: false,
 	disableFocusTrap: false,
-	closeOnEscape: true,
+	escapeBehavior: 'close',
 	preventScroll: false,
 	onOpenChange: undefined,
 	closeOnOutsideClick: true,
-	portal: undefined,
+	portal: 'body',
 	forceVisible: false,
 	openFocus: undefined,
 	closeFocus: undefined,
@@ -67,7 +63,7 @@ export function createPopover(args?: CreatePopoverProps) {
 		arrowSize,
 		disableFocusTrap,
 		preventScroll,
-		closeOnEscape,
+		escapeBehavior,
 		closeOnOutsideClick,
 		portal,
 		forceVisible,
@@ -106,21 +102,12 @@ export function createPopover(args?: CreatePopoverProps) {
 			let unsubPopper = noop;
 
 			const unsubDerived = effect(
-				[
-					isVisible,
-					activeTrigger,
-					positioning,
-					disableFocusTrap,
-					closeOnEscape,
-					closeOnOutsideClick,
-					portal,
-				],
+				[isVisible, activeTrigger, positioning, disableFocusTrap, closeOnOutsideClick, portal],
 				([
 					$isVisible,
 					$activeTrigger,
 					$positioning,
 					$disableFocusTrap,
-					$closeOnEscape,
 					$closeOnOutsideClick,
 					$portal,
 				]) => {
@@ -140,7 +127,7 @@ export function createPopover(args?: CreatePopoverProps) {
 									onClose: handleClose,
 									closeOnInteractOutside: $closeOnOutsideClick,
 								},
-								escapeKeydown: $closeOnEscape ? { handler: handleClose } : null,
+								escapeKeydown: { behaviorType: escapeBehavior },
 								portal: getPortalDestination(node, $portal),
 								preventTextSelectionOverflow: { enabled: preventTextSelectionOverflow },
 							},
@@ -158,7 +145,7 @@ export function createPopover(args?: CreatePopoverProps) {
 		},
 	});
 
-	function toggleOpen() {
+	async function toggleOpen() {
 		open.update((prev) => !prev);
 	}
 	function shouldCloseOnInteractOutside(e: InteractOutsideEvent) {
@@ -219,21 +206,9 @@ export function createPopover(args?: CreatePopoverProps) {
 			} as const;
 		},
 		action: (node: HTMLElement) => {
-			let unsubEscapeKeydown = noop;
 			let unsubDerived = noop;
 			let unsubPortal = noop;
 
-			if (closeOnEscape.get()) {
-				const escapeKeydown = useEscapeKeydown(node, {
-					handler: () => {
-						handleClose();
-					},
-				});
-				if (escapeKeydown && escapeKeydown.destroy) {
-					unsubEscapeKeydown = escapeKeydown.destroy;
-				}
-			}
-
 			unsubDerived = effect([portal], ([$portal]) => {
 				unsubPortal();
 				if ($portal === null) return;
@@ -244,7 +219,6 @@ export function createPopover(args?: CreatePopoverProps) {
 
 			return {
 				destroy() {
-					unsubEscapeKeydown();
 					unsubDerived();
 					unsubPortal();
 				},
diff --git a/src/lib/builders/popover/types.ts b/src/lib/builders/popover/types.ts
index 5e6427ac..106b12d7 100644
--- a/src/lib/builders/popover/types.ts
+++ b/src/lib/builders/popover/types.ts
@@ -1,4 +1,9 @@
-import type { FloatingConfig, InteractOutsideEvent } from '$lib/internal/actions/index.js';
+import type {
+	EscapeBehaviorType,
+	FloatingConfig,
+	InteractOutsideEvent,
+	PortalConfig,
+} from '$lib/internal/actions/index.js';
 import type { ChangeFn, FocusProp, IdObj } from '$lib/internal/helpers/index.js';
 import type { BuilderReturn } from '$lib/internal/types.js';
 import type { Writable } from 'svelte/store';
@@ -47,11 +52,15 @@ export type CreatePopoverProps = {
 	disableFocusTrap?: boolean;
 
 	/**
-	 * Whether or not to close the popover when the escape key is pressed.
+	 * Escape behavior type.
+	 * `close`: Closes the element immediately.
+	 * `defer-otherwise-close`: Delegates the action to the parent element. If no parent is found, it closes the element.
+	 * `defer-otherwise-ignore`: Delegates the action to the parent element. If no parent is found, nothing is done.
+	 * `ignore`: Prevents the element from closing and also blocks the parent element from closing in response to the Escape key.
 	 *
-	 * @default true
+	 * @defaultValue `close`
 	 */
-	closeOnEscape?: boolean;
+	escapeBehavior?: EscapeBehaviorType;
 
 	/**
 	 * Whether or not to close the popover when the escape key is pressed.
@@ -87,7 +96,7 @@ export type CreatePopoverProps = {
 	 *
 	 * @default 'body'
 	 */
-	portal?: HTMLElement | string | null;
+	portal?: PortalConfig | null;
 
 	/**
 	 * Whether the menu content should be displayed even if it is not open.
diff --git a/src/lib/builders/slider/create.ts b/src/lib/builders/slider/create.ts
index 124915bf..459a9407 100644
--- a/src/lib/builders/slider/create.ts
+++ b/src/lib/builders/slider/create.ts
@@ -42,7 +42,9 @@ const { name } = createElHelpers('slider');
 export const createSlider = (props?: CreateSliderProps) => {
 	const withDefaults = { ...defaults, ...props } satisfies CreateSliderProps;
 
-	const options = toWritableStores(omit(withDefaults, 'value', 'onValueChange', 'defaultValue'));
+	const options = toWritableStores(
+		omit(withDefaults, 'value', 'onValueChange', 'onValueCommitted', 'defaultValue')
+	);
 	const { min, max, step, orientation, dir, disabled, autoSort } = options;
 
 	const valueWritable = withDefaults.value ?? writable(withDefaults.defaultValue);
@@ -323,6 +325,10 @@ export const createSlider = (props?: CreateSliderProps) => {
 						break;
 					}
 				}
+
+				if (withDefaults?.onValueCommitted) {
+					withDefaults.onValueCommitted(value.get());
+				}
 			});
 
 			return {
@@ -516,6 +522,10 @@ export const createSlider = (props?: CreateSliderProps) => {
 
 			const pointerUp = () => {
 				isActive.set(false);
+
+				if (withDefaults?.onValueCommitted) {
+					withDefaults.onValueCommitted(value.get());
+				}
 			};
 
 			const unsub = executeCallbacks(
diff --git a/src/lib/builders/slider/types.ts b/src/lib/builders/slider/types.ts
index 678367c1..df2451b8 100644
--- a/src/lib/builders/slider/types.ts
+++ b/src/lib/builders/slider/types.ts
@@ -27,6 +27,11 @@ export type CreateSliderProps = {
 	 */
 	onValueChange?: ChangeFn<number[]>;
 
+	/**
+	 * The callback invoked when the user has committed the value of the slider.
+	 */
+	onValueCommitted?: (value: number[]) => void;
+
 	/**
 	 * The minimum value of the slider.
 	 *
diff --git a/src/lib/builders/tags-input/create.ts b/src/lib/builders/tags-input/create.ts
index 5e5127f0..ec7b0ba4 100644
--- a/src/lib/builders/tags-input/create.ts
+++ b/src/lib/builders/tags-input/create.ts
@@ -14,6 +14,7 @@ import {
 	overridable,
 	styleToString,
 	toWritableStores,
+	noop,
 } from '$lib/internal/helpers/index.js';
 import { withGet } from '$lib/internal/helpers/withGet.js';
 import type { Defaults, MeltActionReturn } from '$lib/internal/types.js';
@@ -23,6 +24,7 @@ import { generateIds } from '../../internal/helpers/id.js';
 import type { TagsInputEvents } from './events.js';
 import { focusInput, highlightText, setSelectedFromEl } from './helpers.js';
 import type { CreateTagsInputProps, Tag, TagProps } from './types.js';
+import { useInteractOutside } from '$lib/internal/actions/index.js';
 
 const defaults = {
 	placeholder: '',
@@ -226,6 +228,11 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 		return true;
 	}
 
+	// Used to determine if tag is being edited
+	const isEditing = derived([editable, editing], ([$editable, $editing]) => {
+		return (tag: Tag) => $editable && $editing?.id === tag.id;
+	});
+
 	const root = makeElement(name(''), {
 		stores: [disabled],
 		returned: ([$disabled]) => {
@@ -617,11 +624,10 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 	});
 
 	const edit = makeElement(name('edit'), {
-		stores: [editing, editable],
-		returned: ([$editing, $editable]) => {
+		stores: isEditing,
+		returned: ($isEditing) => {
 			return (tag: Tag) => {
-				const editable = $editable;
-				const editing = editable ? $editing?.id === tag.id : undefined;
+				const editing = $isEditing(tag);
 
 				return {
 					'aria-hidden': !editing,
@@ -652,56 +658,66 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 				};
 			};
 
-			const unsub = executeCallbacks(
-				addMeltEventListener(node, 'blur', () => {
-					if (node.hasAttribute('hidden')) return;
+			let unsubInteractOutside = noop;
+			let unsubEvents = noop;
 
-					// Stop editing, reset the value to the original and clear an invalid state
-					editing.set(null);
-					node.textContent = getElProps().value;
-					getElementByMeltId(meltIds.root)?.removeAttribute('data-invalid-edit');
-					node.removeAttribute('data-invalid-edit');
-				}),
-				addMeltEventListener(node, 'keydown', async (e) => {
-					if (node.hasAttribute('hidden')) return;
+			const unsubDerived = effect(isEditing, ($isEditing) => {
+				unsubInteractOutside();
+				unsubEvents();
+				if (!$isEditing(getElProps())) return;
 
-					if (e.key === kbd.ENTER) {
-						// Capture the edit value, validate and then update
-						e.preventDefault();
+				unsubInteractOutside = useInteractOutside(node).destroy;
+				unsubEvents = executeCallbacks(
+					addMeltEventListener(node, 'blur', () => {
+						// Stop editing, reset the value to the original and clear an invalid state
+						editing.set(null);
+						node.textContent = getElProps().value;
+						getElementByMeltId(meltIds.root)?.removeAttribute('data-invalid-edit');
+						node.removeAttribute('data-invalid-edit');
+					}),
+					addMeltEventListener(node, 'keydown', async (e) => {
+						if (e.key === kbd.ENTER) {
+							// Capture the edit value, validate and then update
+							e.preventDefault();
 
-						// Do nothing when the value is empty
-						const value = node.textContent;
-						if (!value) return;
+							// Do nothing when the value is empty
+							const value = node.textContent;
+							if (!value) return;
 
-						const t = { id: getElProps().id, value };
+							const t = { id: getElProps().id, value };
 
-						if (isInputValid(value) && (await updateTag(t, true))) {
-							node.textContent = t.value;
+							if (isInputValid(value) && (await updateTag(t, true))) {
+								node.textContent = t.value;
+								editValue.set('');
+								focusInput(meltIds.input);
+							} else {
+								getElementByMeltId(meltIds.root)?.setAttribute('data-invalid-edit', '');
+								node.setAttribute('data-invalid-edit', '');
+							}
+						} else if (e.key === kbd.ESCAPE) {
+							// Reset the value, clear the edit value store, set this tag as
+							// selected and focus on input
+							e.preventDefault();
+							e.stopImmediatePropagation();
+							node.textContent = getElProps().value;
 							editValue.set('');
+							setSelectedFromEl(node, selected);
 							focusInput(meltIds.input);
-						} else {
-							getElementByMeltId(meltIds.root)?.setAttribute('data-invalid-edit', '');
-							node.setAttribute('data-invalid-edit', '');
 						}
-					} else if (e.key === kbd.ESCAPE) {
-						// Reset the value, clear the edit value store, set this tag as
-						// selected and focus on input
-						e.preventDefault();
-						node.textContent = getElProps().value;
-						editValue.set('');
-						setSelectedFromEl(node, selected);
-						focusInput(meltIds.input);
-					}
-				}),
-				addMeltEventListener(node, 'input', () => {
-					if (node.hasAttribute('hidden')) return;
+					}),
+					addMeltEventListener(node, 'input', () => {
+						// Update the edit value store
+						editValue.set(node.textContent || '');
+					})
+				);
+			});
 
-					// Update the edit value store
-					editValue.set(node.textContent || '');
-				})
-			);
 			return {
-				destroy: unsub,
+				destroy() {
+					unsubInteractOutside();
+					unsubEvents();
+					unsubDerived();
+				},
 			};
 		},
 	});
@@ -757,6 +773,7 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 		},
 		helpers: {
 			isSelected,
+			isEditing,
 			isInputValid,
 			addTag,
 			updateTag,
diff --git a/src/lib/builders/toast/create.ts b/src/lib/builders/toast/create.ts
index 2576e73a..640ce466 100644
--- a/src/lib/builders/toast/create.ts
+++ b/src/lib/builders/toast/create.ts
@@ -21,13 +21,14 @@ const { name } = createElHelpers<ToastParts>('toast');
 const defaults = {
 	closeDelay: 5000,
 	type: 'foreground',
+	hover: 'pause',
 } satisfies CreateToasterProps;
 
 export function createToaster<T = object>(props?: CreateToasterProps) {
 	const withDefaults = { ...defaults, ...props } satisfies CreateToasterProps;
 
 	const options = toWritableStores(withDefaults);
-	const { closeDelay, type } = options;
+	const { closeDelay, type, hover } = options;
 
 	const toastsMap = writable(new Map<string, Toast<T>>());
 
@@ -98,6 +99,23 @@ export function createToaster<T = object>(props?: CreateToasterProps) {
 		});
 	};
 
+	const pauseToastTimer = (currentToast: Toast<T>) => {
+		if (currentToast.timeout !== null) {
+			window.clearTimeout(currentToast.timeout);
+		}
+		currentToast.pausedAt = performance.now();
+	};
+	const restartToastTimer = (currentToast: Toast<T>) => {
+		const pausedAt = currentToast.pausedAt ?? currentToast.createdAt;
+		const elapsed = pausedAt - currentToast.createdAt - currentToast.pauseDuration;
+		const remaining = currentToast.closeDelay - elapsed;
+		currentToast.timeout = window.setTimeout(() => {
+			removeToast(currentToast.id);
+		}, remaining);
+		currentToast.pauseDuration += performance.now() - pausedAt;
+		currentToast.pausedAt = undefined;
+	};
+
 	const content = makeElement(name('content'), {
 		stores: toastsMap,
 		returned: ($toasts) => {
@@ -123,31 +141,42 @@ export function createToaster<T = object>(props?: CreateToasterProps) {
 				addMeltEventListener(node, 'pointerenter', (e) => {
 					if (isTouch(e)) return;
 					toastsMap.update((currentMap) => {
-						const currentToast = currentMap.get(node.id);
-						if (!currentToast || currentToast.closeDelay === 0) return currentMap;
-
-						if (currentToast.timeout !== null) {
-							window.clearTimeout(currentToast.timeout);
+						switch (hover.get()) {
+							case 'pause': {
+								const currentToast = currentMap.get(node.id);
+								if (!currentToast || currentToast.closeDelay === 0) return currentMap;
+								pauseToastTimer(currentToast);
+								break;
+							}
+							case 'pause-all':
+								for (const [, currentToast] of currentMap) {
+									if (!currentToast || currentToast.closeDelay === 0) continue;
+									pauseToastTimer(currentToast);
+								}
+								break;
 						}
-						currentToast.pausedAt = performance.now();
 						return new Map(currentMap);
 					});
 				}),
 				addMeltEventListener(node, 'pointerleave', (e) => {
 					if (isTouch(e)) return;
 					toastsMap.update((currentMap) => {
-						const currentToast = currentMap.get(node.id);
-						if (!currentToast || currentToast.closeDelay === 0) return currentMap;
-
-						const pausedAt = currentToast.pausedAt ?? currentToast.createdAt;
-						const elapsed = pausedAt - currentToast.createdAt - currentToast.pauseDuration;
-						const remaining = currentToast.closeDelay - elapsed;
-						currentToast.timeout = window.setTimeout(() => {
-							removeToast(node.id);
-						}, remaining);
-
-						currentToast.pauseDuration += performance.now() - pausedAt;
-						currentToast.pausedAt = undefined;
+						switch (hover.get()) {
+							case 'pause': {
+								const currentToast = currentMap.get(node.id);
+								if (!currentToast || currentToast.closeDelay === 0) return currentMap;
+								restartToastTimer(currentToast);
+								break;
+							}
+
+							case 'pause-all':
+								for (const [, currentToast] of currentMap) {
+									if (!currentToast || currentToast.closeDelay === 0) continue;
+									restartToastTimer(currentToast);
+								}
+								break;
+						}
+
 						return new Map(currentMap);
 					});
 				}),
diff --git a/src/lib/builders/toast/types.ts b/src/lib/builders/toast/types.ts
index 82af1ec8..fd56622b 100644
--- a/src/lib/builders/toast/types.ts
+++ b/src/lib/builders/toast/types.ts
@@ -8,9 +8,10 @@ export type CreateToasterProps = {
 	// If set to 0, the toast will not be automatically closed.
 	closeDelay?: number;
 	type?: 'foreground' | 'background';
+	hover?: 'pause' | 'pause-all' | null;
 };
 
-export type AddToastProps<T = object> = CreateToasterProps & {
+export type AddToastProps<T = object> = Omit<CreateToasterProps, 'hover'> & {
 	data: T;
 };
 
diff --git a/src/lib/builders/tooltip/create.ts b/src/lib/builders/tooltip/create.ts
index 48c8f016..a07a336f 100644
--- a/src/lib/builders/tooltip/create.ts
+++ b/src/lib/builders/tooltip/create.ts
@@ -10,7 +10,6 @@ import {
 	isDocument,
 	isElement,
 	isTouch,
-	kbd,
 	makeHullFromElements,
 	noop,
 	omit,
@@ -21,7 +20,12 @@ import {
 	isPointerInGraceArea,
 } from '$lib/internal/helpers/index.js';
 
-import { useFloating, usePortal } from '$lib/internal/actions/index.js';
+import {
+	useEscapeKeydown,
+	useFloating,
+	useInteractOutside,
+	usePortal,
+} from '$lib/internal/actions/index.js';
 import type { MeltActionReturn } from '$lib/internal/types.js';
 import { derived, writable, type Writable } from 'svelte/store';
 import { generateIds } from '../../internal/helpers/id.js';
@@ -39,8 +43,8 @@ const defaults = {
 	openDelay: 1000,
 	closeDelay: 0,
 	forceVisible: false,
-	portal: undefined,
-	closeOnEscape: true,
+	portal: 'body',
+	escapeBehavior: 'close',
 	disableHoverableContent: false,
 	group: undefined,
 } satisfies CreateTooltipProps;
@@ -66,7 +70,7 @@ export function createTooltip(props?: CreateTooltipProps) {
 		closeDelay,
 		forceVisible,
 		portal,
-		closeOnEscape,
+		escapeBehavior,
 		disableHoverableContent,
 		group,
 	} = options;
@@ -80,6 +84,8 @@ export function createTooltip(props?: CreateTooltipProps) {
 	const ids = toWritableStores({ ...generateIds(tooltipIdParts), ...withDefaults.ids });
 
 	let clickedTrigger = false;
+	let isPointerInsideTrigger = false;
+	let isPointerInsideContent = false;
 
 	const getEl = (part: keyof typeof ids) => {
 		if (!isBrowser) return null;
@@ -144,17 +150,6 @@ export function createTooltip(props?: CreateTooltipProps) {
 			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<TooltipEvents['trigger']> => {
-			const keydownHandler = (e: KeyboardEvent) => {
-				if (closeOnEscape.get() && e.key === kbd.ESCAPE) {
-					if (openTimeout) {
-						window.clearTimeout(openTimeout);
-						openTimeout = null;
-					}
-
-					open.set(false);
-				}
-			};
-
 			const unsub = executeCallbacks(
 				addMeltEventListener(node, 'pointerdown', () => {
 					const $closeOnPointerDown = closeOnPointerDown.get();
@@ -167,10 +162,12 @@ export function createTooltip(props?: CreateTooltipProps) {
 					}
 				}),
 				addMeltEventListener(node, 'pointerenter', (e) => {
+					isPointerInsideTrigger = true;
 					if (isTouch(e)) return;
 					openTooltip('pointer');
 				}),
 				addMeltEventListener(node, 'pointerleave', (e) => {
+					isPointerInsideTrigger = false;
 					if (isTouch(e)) return;
 					if (openTimeout) {
 						window.clearTimeout(openTimeout);
@@ -181,13 +178,14 @@ export function createTooltip(props?: CreateTooltipProps) {
 					if (clickedTrigger) return;
 					openTooltip('focus');
 				}),
-				addMeltEventListener(node, 'blur', () => closeTooltip(true)),
-				addMeltEventListener(node, 'keydown', keydownHandler),
-				addEventListener(document, 'keydown', keydownHandler)
+				addMeltEventListener(node, 'blur', () => closeTooltip(true))
 			);
 
 			return {
-				destroy: unsub,
+				destroy() {
+					unsub();
+					isPointerInsideTrigger = false;
+				},
 			};
 		},
 	});
@@ -208,21 +206,42 @@ export function createTooltip(props?: CreateTooltipProps) {
 		action: (node: HTMLElement): MeltActionReturn<TooltipEvents['content']> => {
 			let unsubFloating = noop;
 			let unsubPortal = noop;
+			let unsubInteractOutside = noop;
+			let unsubEscapeKeydown = noop;
 
 			const unsubDerived = effect(
 				[isVisible, positioning, portal],
 				([$isVisible, $positioning, $portal]) => {
 					unsubPortal();
 					unsubFloating();
+					unsubInteractOutside();
+					unsubEscapeKeydown();
 					const triggerEl = getEl('trigger');
 					if (!$isVisible || !triggerEl) return;
-
 					tick().then(() => {
 						unsubPortal();
 						unsubFloating();
+						unsubInteractOutside();
+						unsubEscapeKeydown();
 						const portalDest = getPortalDestination(node, $portal);
-						if (portalDest) unsubPortal = usePortal(node, portalDest).destroy;
+						if (portalDest !== null) {
+							unsubPortal = usePortal(node, portalDest).destroy;
+						}
 						unsubFloating = useFloating(triggerEl, node, $positioning).destroy;
+						unsubInteractOutside = useInteractOutside(node).destroy;
+
+						const onEscapeKeyDown = () => {
+							if (openTimeout) {
+								window.clearTimeout(openTimeout);
+								openTimeout = null;
+							}
+							open.set(false);
+						};
+
+						unsubEscapeKeydown = useEscapeKeydown(node, {
+							behaviorType: escapeBehavior,
+							handler: onEscapeKeyDown,
+						}).destroy;
 					});
 				}
 			);
@@ -242,17 +261,25 @@ export function createTooltip(props?: CreateTooltipProps) {
 			}
 
 			const unsubEvents = executeCallbacks(
-				addMeltEventListener(node, 'pointerenter', () => openTooltip('pointer')),
+				addMeltEventListener(node, 'pointerenter', () => {
+					isPointerInsideContent = true;
+					openTooltip('pointer');
+				}),
+				addMeltEventListener(node, 'pointerleave', () => {
+					isPointerInsideContent = false;
+				}),
 				addMeltEventListener(node, 'pointerdown', () => openTooltip('pointer')),
 				addEventListener(window, 'scroll', handleScroll, { capture: true })
 			);
 
 			return {
 				destroy() {
+					isPointerInsideContent = false;
 					unsubEvents();
 					unsubPortal();
 					unsubFloating();
 					unsubDerived();
+					unsubInteractOutside();
 				},
 			};
 		},
@@ -306,7 +333,15 @@ export function createTooltip(props?: CreateTooltipProps) {
 					? [triggerEl]
 					: [triggerEl, contentEl];
 				const polygon = makeHullFromElements(polygonElements);
-				isMouseInTooltipArea = isPointerInGraceArea(e, polygon);
+
+				/**
+				 * This takes into account the potential discrepancy between the
+				 * pointer's coordinates (`clientX` and `clientY`) and the
+				 * exact boundaries of the trigger element's rectangle due to
+				 * sub-pixel rendering and rounding errors.
+				 */
+				isMouseInTooltipArea =
+					isPointerInsideTrigger || isPointerInsideContent || isPointerInGraceArea(e, polygon);
 
 				if ($openReason !== 'pointer') return;
 
diff --git a/src/lib/builders/tooltip/types.ts b/src/lib/builders/tooltip/types.ts
index fb137f44..45d40aa4 100644
--- a/src/lib/builders/tooltip/types.ts
+++ b/src/lib/builders/tooltip/types.ts
@@ -1,4 +1,8 @@
-import type { FloatingConfig } from '$lib/internal/actions/index.js';
+import type {
+	EscapeBehaviorType,
+	FloatingConfig,
+	PortalConfig,
+} from '$lib/internal/actions/index.js';
 import type { ChangeFn, IdObj } from '$lib/internal/helpers/index.js';
 import type { BuilderReturn } from '$lib/internal/types.js';
 import type { Writable } from 'svelte/store';
@@ -14,7 +18,17 @@ export type CreateTooltipProps = {
 	openDelay?: number;
 	closeDelay?: number;
 	forceVisible?: boolean;
-	closeOnEscape?: boolean;
+	/**
+	 * Escape behavior type.
+	 * `close`: Closes the element immediately.
+	 * `defer-otherwise-close`: Delegates the action to the parent element. If no parent is found, it closes the element.
+	 * `defer-otherwise-ignore`: Delegates the action to the parent element. If no parent is found, nothing is done.
+	 * `ignore`: Prevents the element from closing and also blocks the parent element from closing in response to the Escape key.
+	 *
+	 * @defaultValue `close`
+	 */
+	escapeBehavior?: EscapeBehaviorType;
+
 	disableHoverableContent?: boolean;
 	/**
 	 * If set to `true`, whenever you open this tooltip, all other tooltips
@@ -27,7 +41,7 @@ export type CreateTooltipProps = {
 	 *
 	 * @default 'body'
 	 */
-	portal?: HTMLElement | string | null;
+	portal?: PortalConfig | null;
 
 	/**
 	 * Optionally override the default ids we assign to the elements
diff --git a/src/lib/internal/actions/click-outside/action.ts b/src/lib/internal/actions/click-outside/action.ts
deleted file mode 100644
index 537112b4..00000000
--- a/src/lib/internal/actions/click-outside/action.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-// Modified from Grail UI v0.9.6 (2023-06-10)
-// Source: https://github.com/grail-ui/grail-ui
-// https://github.com/grail-ui/grail-ui/tree/master/packages/grail-ui/src/clickOutside/clickOutside.ts
-
-import { readable } from 'svelte/store';
-import { addEventListener } from '$lib/internal/helpers/event.js';
-import { get } from 'svelte/store';
-import { isFunction } from '$lib/internal/helpers/is.js';
-import type { ClickOutsideConfig } from './types.js';
-
-/**
- * Creates a readable store that tracks the latest PointerEvent that occurred on the document.
- *
- * @returns A function to unsubscribe from the event listener and stop tracking pointer events.
- */
-const documentClickStore = readable<PointerEvent | undefined>(undefined, (set): (() => void) => {
-	/**
-	 * Event handler for pointerdown events on the document.
-	 * Updates the store's value with the latest PointerEvent and then resets it to undefined.
-	 */
-	function clicked(event: PointerEvent | undefined) {
-		set(event);
-
-		// New subscriptions will not trigger immediately
-		set(undefined);
-	}
-
-	// Adds a pointerdown event listener to the document, calling the clicked function when triggered.
-	const unsubscribe = addEventListener(document, 'pointerup', clicked, {
-		passive: false,
-		capture: true,
-	});
-
-	// Returns a function to unsubscribe from the event listener and stop tracking pointer events.
-	return unsubscribe;
-});
-
-export const useClickOutside = (node: HTMLElement, config: ClickOutsideConfig = {}) => {
-	let options = { enabled: true, ...config };
-
-	// Returns true if the click outside handler is enabled
-	function isEnabled(): boolean {
-		return typeof options.enabled === 'boolean' ? options.enabled : get(options.enabled);
-	}
-
-	// Handle document clicks
-	const unsubscribe = documentClickStore.subscribe((e) => {
-		// If the click outside handler is disabled, or if the event is null or the node itself, return early
-		if (!isEnabled() || !e || e.target === node) {
-			return;
-		}
-
-		const composedPath = e.composedPath();
-
-		// If the target is in the node, return early
-		if (composedPath.includes(node)) return;
-
-		// If an ignore function is passed, check if it returns true
-		if (options.ignore) {
-			if (isFunction(options.ignore)) {
-				if (options.ignore(e)) return;
-			}
-			// If an ignore array is passed, check if any elements in the array match the target
-			else if (Array.isArray(options.ignore)) {
-				if (
-					options.ignore.length > 0 &&
-					options.ignore.some((ignoreEl) => {
-						return ignoreEl && (e.target === ignoreEl || composedPath.includes(ignoreEl));
-					})
-				)
-					return;
-			}
-		}
-
-		// If none of the above conditions are met, call the handler
-		options.handler?.(e);
-	});
-
-	return {
-		update(params: Partial<ClickOutsideConfig>) {
-			options = { ...options, ...params };
-		},
-		destroy() {
-			unsubscribe();
-		},
-	};
-};
diff --git a/src/lib/internal/actions/click-outside/index.ts b/src/lib/internal/actions/click-outside/index.ts
deleted file mode 100644
index 01388907..00000000
--- a/src/lib/internal/actions/click-outside/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './action.js';
-export * from './types.js';
diff --git a/src/lib/internal/actions/click-outside/types.ts b/src/lib/internal/actions/click-outside/types.ts
deleted file mode 100644
index 93d19b2d..00000000
--- a/src/lib/internal/actions/click-outside/types.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-// Modified from Grail UI v0.9.6 (2023-06-10)
-// Source: https://github.com/grail-ui/grail-ui
-// https://github.com/grail-ui/grail-ui/tree/master/packages/grail-ui/src/clickOutside/clickOutside.types.ts
-
-import type { Readable } from 'svelte/store';
-
-export type ClickOutsideConfig = {
-	/**
-	 * Whether the listener is active.
-	 *
-	 * @defaultValue `true`
-	 */
-	enabled?: boolean | Readable<boolean>;
-
-	/**
-	 * Callback when user clicks outside a given element.
-	 */
-	handler?: (evt: PointerEvent) => void;
-
-	/**
-	 * A predicate function or a list of elements that should not trigger the event.
-	 */
-	ignore?: ((e: PointerEvent) => boolean) | Element[];
-};
diff --git a/src/lib/internal/actions/escape-keydown/action.ts b/src/lib/internal/actions/escape-keydown/action.ts
index 05bd15e3..e266670e 100644
--- a/src/lib/internal/actions/escape-keydown/action.ts
+++ b/src/lib/internal/actions/escape-keydown/action.ts
@@ -1,72 +1,49 @@
 import { addEventListener } from '$lib/internal/helpers/event.js';
 import { isFunction, isHTMLElement, isReadable } from '$lib/internal/helpers/is.js';
-import { get, readable, type Readable } from 'svelte/store';
-import { effect, executeCallbacks, kbd, noop } from '../../helpers/index.js';
-import type { EscapeKeydownConfig } from './types.js';
+import { readable, type Readable } from 'svelte/store';
+import { effect, executeCallbacks, kbd, noop, withGet, type WithGet } from '../../helpers/index.js';
+import type { EscapeBehaviorType, EscapeKeydownConfig } from './types.js';
 import type { Action } from 'svelte/action';
 
-/**
- * Creates a readable store that tracks the latest Escape Keydown that occurred on the document.
- *
- * @returns A function to unsubscribe from the event listener and stop tracking keydown events.
- */
-const documentEscapeKeyStore = readable<KeyboardEvent | undefined>(
-	undefined,
-	(set): (() => void) => {
-		/**
-		 * Event handler for keydown events on the document.
-		 * Updates the store's value with the latest Escape Keydown event and then resets it to undefined.
-		 */
-		function keydown(event: KeyboardEvent | undefined) {
-			if (event && event.key === kbd.ESCAPE) {
-				set(event);
-			}
-
-			// New subscriptions will not trigger immediately
-			set(undefined);
-		}
-
-		// Adds a keydown event listener to the document, calling the keydown function when triggered.
-		const unsubscribe = addEventListener(document, 'keydown', keydown, {
-			passive: false,
-		});
-
-		// Returns a function to unsubscribe from the event listener and stop tracking keydown events.
-		return unsubscribe;
-	}
-);
+const layers = new Map<HTMLElement, WithGet<Readable<EscapeBehaviorType>>>();
 
 export const useEscapeKeydown = ((node, config = {}) => {
 	let unsub = noop;
+
 	function update(config: EscapeKeydownConfig = {}) {
 		unsub();
+		const options = { behaviorType: 'close', ...config } satisfies EscapeKeydownConfig;
+		const behaviorType = isReadable(options.behaviorType)
+			? options.behaviorType
+			: withGet(readable(options.behaviorType));
 
-		const options = { enabled: true, ...config };
-		const enabled = (
-			isReadable(options.enabled) ? options.enabled : readable(options.enabled)
-		) as Readable<boolean>;
-
-		unsub = executeCallbacks(
-			// Handle escape keydowns
-			documentEscapeKeyStore.subscribe((e) => {
-				if (!e || !get(enabled)) return;
-				const target = e.target;
+		layers.set(node, behaviorType);
 
-				if (!isHTMLElement(target) || target.closest('[data-escapee]') !== node) {
-					return;
-				}
+		const onKeyDown = (e: KeyboardEvent) => {
+			if (e.key !== kbd.ESCAPE || !isResponsibleEscapeLayer(node)) return;
+			const target = e.target;
+			if (!isHTMLElement(target)) return;
 
-				e.preventDefault();
-				if (shouldIgnoreEvent(e, options.ignore)) return;
+			e.preventDefault();
+			if (shouldIgnoreEvent(e, options.ignore)) return;
+			if (shouldInvokeResponsibleLayerHandler(behaviorType.get())) {
 				options.handler?.(e);
-			}),
-			effect(enabled, ($enabled) => {
-				if ($enabled) {
+			}
+		};
+
+		unsub = executeCallbacks(
+			addEventListener(document, 'keydown', onKeyDown, { passive: false }),
+			effect(behaviorType, ($behaviorType) => {
+				if (
+					$behaviorType === 'close' ||
+					($behaviorType === 'defer-otherwise-close' && [...layers.keys()][0] === node)
+				) {
 					node.dataset.escapee = '';
 				} else {
 					delete node.dataset.escapee;
 				}
-			})
+			}),
+			behaviorType.destroy || noop
 		);
 	}
 
@@ -75,12 +52,30 @@ export const useEscapeKeydown = ((node, config = {}) => {
 	return {
 		update,
 		destroy() {
-			node.removeAttribute('data-escapee');
+			layers.delete(node);
+			delete node.dataset.escapee;
 			unsub();
 		},
 	};
 }) satisfies Action<HTMLElement, EscapeKeydownConfig>;
 
+const isResponsibleEscapeLayer = (node: HTMLElement): boolean => {
+	const layersArr = [...layers];
+	/**
+	 * We first check if we can find a top layer with `close` or `ignore`.
+	 * If that top layer was found and matches the provided node, then the node is
+	 * responsible for the escape. Otherwise, we know that all layers defer so
+	 * the first layer is the responsible one.
+	 */
+	const topMostLayer = layersArr.findLast(([_, behaviorType]) => {
+		const $behaviorType = behaviorType.get();
+		return $behaviorType === 'close' || $behaviorType === 'ignore';
+	});
+	if (topMostLayer) return topMostLayer[0] === node;
+	const [firstLayerNode] = layersArr[0];
+	return firstLayerNode === node;
+};
+
 const shouldIgnoreEvent = (e: KeyboardEvent, ignore: EscapeKeydownConfig['ignore']): boolean => {
 	if (!ignore) return false;
 	if (isFunction(ignore) && ignore(e)) return true;
@@ -89,3 +84,7 @@ const shouldIgnoreEvent = (e: KeyboardEvent, ignore: EscapeKeydownConfig['ignore
 	}
 	return false;
 };
+
+const shouldInvokeResponsibleLayerHandler = (behaviorType: EscapeBehaviorType) => {
+	return behaviorType === 'close' || behaviorType === 'defer-otherwise-close';
+};
diff --git a/src/lib/internal/actions/escape-keydown/types.ts b/src/lib/internal/actions/escape-keydown/types.ts
index 5348c5d5..23d02a37 100644
--- a/src/lib/internal/actions/escape-keydown/types.ts
+++ b/src/lib/internal/actions/escape-keydown/types.ts
@@ -1,12 +1,23 @@
+import type { WithGet } from '$lib/internal/helpers/withGet.js';
 import type { Readable } from 'svelte/store';
 
+export type EscapeBehaviorType =
+	| 'close'
+	| 'defer-otherwise-close'
+	| 'defer-otherwise-ignore'
+	| 'ignore';
+
 export type EscapeKeydownConfig = {
 	/**
-	 * Whether the listener is active.
+	 * Escape behavior type.
+	 * `close`: Closes the element immediately.
+	 * `defer-otherwise-close`: Delegates the action to the parent element. If no parent is found, it closes the element.
+	 * `defer-otherwise-ignore`: Delegates the action to the parent element. If no parent is found, nothing is done.
+	 * `ignore`: Prevents the element from closing and also blocks the parent element from closing in response to the Escape key.
 	 *
-	 * @defaultValue `true`
+	 * @defaultValue `close`
 	 */
-	enabled?: boolean | Readable<boolean>;
+	behaviorType?: EscapeBehaviorType | WithGet<Readable<EscapeBehaviorType>>;
 
 	/**
 	 * Callback when user presses the escape key element.
diff --git a/src/lib/internal/actions/floating/action.ts b/src/lib/internal/actions/floating/action.ts
index 4847a13e..bda14424 100644
--- a/src/lib/internal/actions/floating/action.ts
+++ b/src/lib/internal/actions/floating/action.ts
@@ -13,8 +13,8 @@ import {
 	autoUpdate,
 } from '@floating-ui/dom';
 import type { FloatingConfig } from './types.js';
-import { isHTMLElement, noop } from '$lib/internal/helpers/index.js';
-import type { Placement, VirtualElement } from '@floating-ui/core';
+import { isHTMLElement, isObject, noop } from '$lib/internal/helpers/index.js';
+import type { Placement, VirtualElement, FlipOptions } from '@floating-ui/core';
 
 const defaultConfig = {
 	strategy: 'absolute',
@@ -58,6 +58,7 @@ export function useFloating(
 			flip({
 				boundary: options.boundary,
 				padding: options.overflowPadding,
+				...(isObject(options.flip) && options.flip),
 			})
 		);
 	}
diff --git a/src/lib/internal/actions/floating/types.ts b/src/lib/internal/actions/floating/types.ts
index 0fa7346d..c7d02585 100644
--- a/src/lib/internal/actions/floating/types.ts
+++ b/src/lib/internal/actions/floating/types.ts
@@ -2,6 +2,7 @@
 // Source: https://github.com/grail-ui/grail-ui
 // https://github.com/grail-ui/grail-ui/tree/master/packages/grail-ui/src/floating/floating.types.ts
 
+import type { FlipOptions } from '@floating-ui/core';
 import type { Boundary } from '@floating-ui/dom';
 
 /**
@@ -67,7 +68,7 @@ export type FloatingConfig = {
 	 *
 	 * @see https://floating-ui.com/docs/flip
 	 */
-	flip?: boolean;
+	flip?: boolean | FlipOptions;
 
 	/**
 	 * Whether the floating element can overlap the reference element.
diff --git a/src/lib/internal/actions/index.ts b/src/lib/internal/actions/index.ts
index 66a7009c..c8ff54fd 100644
--- a/src/lib/internal/actions/index.ts
+++ b/src/lib/internal/actions/index.ts
@@ -1,4 +1,3 @@
-export * from './click-outside/index.js';
 export * from './escape-keydown/index.js';
 export * from './floating/index.js';
 export * from './focus-trap/index.js';
diff --git a/src/lib/internal/actions/interact-outside/action.ts b/src/lib/internal/actions/interact-outside/action.ts
index 99404174..179f32c2 100644
--- a/src/lib/internal/actions/interact-outside/action.ts
+++ b/src/lib/internal/actions/interact-outside/action.ts
@@ -14,12 +14,16 @@ import type {
 } from './types.js';
 import type { Action } from 'svelte/action';
 
-export const useInteractOutside = ((node, config) => {
+const layers = new Set<HTMLElement>();
+
+export const useInteractOutside = ((node, config = {}) => {
 	let unsubEvents = noop;
 	let unsubPointerDown = noop;
 	let unsubPointerUp = noop;
 	let unsubResetInterceptedEvents = noop;
 
+	layers.add(node);
+
 	const documentObj = getOwnerDocument(node);
 
 	let isPointerDown = false;
@@ -92,13 +96,14 @@ export const useInteractOutside = ((node, config) => {
 		resetInterceptedEvents();
 		const { onInteractOutside, onInteractOutsideStart, enabled } = { enabled: true, ...config };
 		if (!enabled) return;
+		let wasTopLayerInPointerDownCapture = false;
 
 		/**
 		 * Debouncing `onPointerDown` ensures that other events can be flagged as not intercepted,
 		 * allowing a comprehensive check for intercepted events thereafter.
 		 */
 		const onPointerDownDebounced = debounce((e: InteractOutsideEvent) => {
-			if (isAnyEventIntercepted()) return;
+			if (!wasTopLayerInPointerDownCapture || isAnyEventIntercepted()) return;
 			if (onInteractOutside && isValidEvent(e, node)) onInteractOutsideStart?.(e);
 			const target = e.target;
 			if (isElement(target) && isOrContainsTarget(node, target)) {
@@ -113,8 +118,13 @@ export const useInteractOutside = ((node, config) => {
 		 * allowing a comprehensive check for intercepted events thereafter.
 		 */
 		const onPointerUpDebounced = debounce((e: InteractOutsideEvent) => {
-			if (isAnyEventIntercepted()) return;
-			if (shouldTriggerInteractOutside(e)) onInteractOutside?.(e);
+			if (
+				wasTopLayerInPointerDownCapture &&
+				!isAnyEventIntercepted() &&
+				shouldTriggerInteractOutside(e)
+			) {
+				onInteractOutside?.(e);
+			}
 			resetPointerState();
 		}, 10);
 		unsubPointerUp = onPointerUpDebounced.destroy;
@@ -127,11 +137,15 @@ export const useInteractOutside = ((node, config) => {
 		const resetInterceptedEventsDebounced = debounce(resetInterceptedEvents, 20);
 		unsubResetInterceptedEvents = resetInterceptedEventsDebounced.destroy;
 
+		const markTopLayerInPointerDown = () => {
+			wasTopLayerInPointerDownCapture = isHighestLayer(node);
+		};
+
 		unsubEvents = executeCallbacks(
 			/** Capture Events For Interaction Start */
-			setupCapturePhaseHandlerAndMarkAsIntercepted('pointerdown'),
-			setupCapturePhaseHandlerAndMarkAsIntercepted('mousedown'),
-			setupCapturePhaseHandlerAndMarkAsIntercepted('touchstart'),
+			setupCapturePhaseHandlerAndMarkAsIntercepted('pointerdown', markTopLayerInPointerDown),
+			setupCapturePhaseHandlerAndMarkAsIntercepted('mousedown', markTopLayerInPointerDown),
+			setupCapturePhaseHandlerAndMarkAsIntercepted('touchstart', markTopLayerInPointerDown),
 			/**
 			 * Intercepted events are reset only at the end of an interaction, allowing
 			 * interception at the start while still capturing the entire interaction.
@@ -181,6 +195,7 @@ export const useInteractOutside = ((node, config) => {
 			unsubPointerDown();
 			unsubPointerUp();
 			unsubResetInterceptedEvents();
+			layers.delete(node);
 		},
 	};
 }) satisfies Action<HTMLElement, InteractOutsideConfig>;
@@ -198,3 +213,7 @@ function isValidEvent(e: InteractOutsideEvent, node: HTMLElement): boolean {
 
 	return node && !isOrContainsTarget(node, target);
 }
+
+function isHighestLayer(node: HTMLElement): boolean {
+	return Array.from(layers).at(-1) === node;
+}
diff --git a/src/lib/internal/actions/modal/action.ts b/src/lib/internal/actions/modal/action.ts
index 709c4972..bba21383 100644
--- a/src/lib/internal/actions/modal/action.ts
+++ b/src/lib/internal/actions/modal/action.ts
@@ -1,53 +1,33 @@
-import { isElement, last, noop } from '$lib/internal/helpers/index.js';
+import { isElement, noop } from '$lib/internal/helpers/index.js';
 import type { InteractOutsideEvent } from '../interact-outside/types.js';
 import { useInteractOutside } from '../index.js';
 import type { ModalConfig } from './types.js';
 import type { Action } from 'svelte/action';
 
-const visibleModals: Element[] = [];
 export const useModal = ((node, config) => {
 	let unsubInteractOutside = noop;
-	visibleModals.push(node);
-
-	function removeNodeFromVisibleModals() {
-		const index = visibleModals.indexOf(node);
-		if (index >= 0) {
-			visibleModals.splice(index, 1);
-		}
-	}
-
-	function isLastModal() {
-		return last(visibleModals) === node;
-	}
 
 	function update(config: ModalConfig) {
 		unsubInteractOutside();
 		const { onClose, shouldCloseOnInteractOutside, closeOnInteractOutside } = config;
 
 		function closeModal() {
-			// we only want to call onClose if this is the topmost modal
-			if (isLastModal() && onClose) {
-				onClose();
-				removeNodeFromVisibleModals();
-			}
+			onClose?.();
 		}
 		function onInteractOutsideStart(e: InteractOutsideEvent) {
 			const target = e.target;
 			if (!isElement(target)) return;
-			if (target && isLastModal()) {
-				e.stopImmediatePropagation();
-			}
+			e.stopImmediatePropagation();
 		}
 		function onInteractOutside(e: InteractOutsideEvent) {
-			if (shouldCloseOnInteractOutside?.(e) && isLastModal()) {
-				e.stopImmediatePropagation();
-				closeModal();
-			}
+			if (!shouldCloseOnInteractOutside?.(e)) return;
+			e.stopImmediatePropagation();
+			closeModal();
 		}
-
 		unsubInteractOutside = useInteractOutside(node, {
 			onInteractOutsideStart,
 			onInteractOutside: closeOnInteractOutside ? onInteractOutside : undefined,
+			enabled: closeOnInteractOutside,
 		}).destroy;
 	}
 
@@ -56,7 +36,6 @@ export const useModal = ((node, config) => {
 	return {
 		update,
 		destroy() {
-			removeNodeFromVisibleModals();
 			unsubInteractOutside();
 		},
 	};
diff --git a/src/lib/internal/actions/popper/action.ts b/src/lib/internal/actions/popper/action.ts
index d657769f..cac6ffe9 100644
--- a/src/lib/internal/actions/popper/action.ts
+++ b/src/lib/internal/actions/popper/action.ts
@@ -76,7 +76,6 @@ export const usePopper = ((popperElement, args) => {
 	if (opts.escapeKeydown !== null) {
 		callbacks.push(
 			useEscapeKeydown(popperElement, {
-				enabled: open,
 				handler: () => {
 					open.set(false);
 				},
diff --git a/src/lib/internal/actions/portal.ts b/src/lib/internal/actions/portal.ts
index f2581099..a762abb6 100644
--- a/src/lib/internal/actions/portal.ts
+++ b/src/lib/internal/actions/portal.ts
@@ -13,8 +13,9 @@ export const usePortal = ((el, target = 'body') => {
 		};
 	}
 
-	async function update(newTarget: HTMLElement | string | undefined) {
+	async function update(newTarget: PortalConfig = 'body') {
 		target = newTarget;
+
 		if (typeof target === 'string') {
 			targetEl = document.querySelector(target);
 			if (targetEl === null) {
diff --git a/src/lib/internal/helpers/store/toReadableStores.ts b/src/lib/internal/helpers/store/toReadableStores.ts
index 6e142847..cfd5f0f0 100644
--- a/src/lib/internal/helpers/store/toReadableStores.ts
+++ b/src/lib/internal/helpers/store/toReadableStores.ts
@@ -1,11 +1,16 @@
 import { readable, type Readable } from 'svelte/store';
 import { isReadable } from '../is.js';
 import { withGet, type WithGet } from '../withGet.js';
+import type { MaybeReadable } from '$lib/internal/types.js';
 
 type TODO = any;
 
 export type ToReadableStores<T extends Record<string, unknown>> = {
-	[K in keyof T]: T[K] extends Readable<TODO> ? WithGet<T[K]> : WithGet<Readable<T[K]>>;
+	[K in keyof T]: T[K] extends Readable<TODO>
+		? WithGet<T[K]>
+		: T[K] extends MaybeReadable<infer U>
+		? WithGet<Readable<U>>
+		: WithGet<Readable<T[K]>>;
 };
 
 /**
@@ -29,5 +34,5 @@ export function toReadableStores<T extends Record<string, unknown>>(
 		}
 	});
 
-	return result;
+	return result as any;
 }
diff --git a/src/lib/internal/types.ts b/src/lib/internal/types.ts
index ca90773c..1bf58768 100644
--- a/src/lib/internal/types.ts
+++ b/src/lib/internal/types.ts
@@ -1,4 +1,5 @@
 import type { ActionReturn } from 'svelte/action';
+import type { Readable } from 'svelte/store';
 
 // Check if type are equal or just extends
 export type IfEquals<T, U, Y = unknown, N = never> = (<G>() => G extends T ? 1 : 2) extends <
@@ -125,3 +126,5 @@ export type RenameProperties<T, NewNames extends Partial<Record<keyof T, string>
 }>;
 
 export type NonEmptyArray<T> = [T, ...T[]];
+
+export type MaybeReadable<T> = T | Readable<T>;
diff --git a/src/routes/(landing-ui)/popover.svelte b/src/routes/(landing-ui)/popover.svelte
index a2a791d4..cae33270 100644
--- a/src/routes/(landing-ui)/popover.svelte
+++ b/src/routes/(landing-ui)/popover.svelte
@@ -16,7 +16,7 @@
 		forceVisible: true,
 		defaultOpen: true,
 		closeOnOutsideClick: false,
-		closeOnEscape: false,
+		escapeBehavior: 'ignore',
 		disableFocusTrap: true,
 	});
 


Test patch:
diff --git a/src/lib/internal/helpers/tests/escape-keydown.spec.ts b/src/lib/internal/helpers/tests/escape-keydown.spec.ts
new file mode 100644
index 00000000..962a05cd
--- /dev/null
+++ b/src/lib/internal/helpers/tests/escape-keydown.spec.ts
@@ -0,0 +1,208 @@
+import { kbd } from '$lib/internal/helpers/keyboard.js';
+import { vi } from 'vitest';
+import { useEscapeKeydown } from '../../actions/escape-keydown/action.js';
+import { noop } from '$lib/internal/helpers/callbacks.js';
+import type {
+	EscapeBehaviorType,
+	EscapeKeydownConfig,
+} from '../../actions/escape-keydown/types.js';
+import { withGet } from '$lib/internal/helpers/withGet.js';
+
+const dispatchKeydownEvent = (key: string) => {
+	const event = new KeyboardEvent('keydown', { key, bubbles: true });
+	document.documentElement.dispatchEvent(event);
+};
+
+const dispatchEscape = () => dispatchKeydownEvent(kbd.ESCAPE);
+
+const mountLayer = (config?: EscapeKeydownConfig) => {
+	const node = document.createElement('div');
+	const handler = vi.fn(noop);
+	const action = useEscapeKeydown(node, { ...config, handler });
+	return { action, handler };
+};
+
+const singleEscapeAssertions = [
+	{
+		behavior: 'close',
+		shouldClose: true,
+	},
+	{
+		behavior: 'ignore',
+		shouldClose: false,
+	},
+	{
+		behavior: 'defer-otherwise-close',
+		shouldClose: true,
+	},
+	{
+		behavior: 'defer-otherwise-ignore',
+		shouldClose: false,
+	},
+] satisfies { behavior: EscapeBehaviorType; shouldClose: boolean }[];
+
+const nestedEscapeAssertions = [
+	{
+		behaviors: ['close', 'close'],
+		expectedInvocations: [false, true],
+	},
+	{
+		behaviors: ['close', 'defer-otherwise-close'],
+		expectedInvocations: [true, false],
+	},
+	{
+		behaviors: ['close', 'defer-otherwise-ignore'],
+		expectedInvocations: [true, false],
+	},
+	{
+		behaviors: ['close', 'ignore'],
+		expectedInvocations: [false, false],
+	},
+	{
+		behaviors: ['defer-otherwise-close', 'defer-otherwise-close'],
+		expectedInvocations: [true, false],
+	},
+	{
+		behaviors: ['defer-otherwise-ignore', 'defer-otherwise-ignore'],
+		expectedInvocations: [false, false],
+	},
+] satisfies { behaviors: EscapeBehaviorType[]; expectedInvocations: boolean[] }[];
+
+describe('escape keydown', () => {
+	describe('single layers', () => {
+		for (const { behavior, shouldClose } of singleEscapeAssertions) {
+			it(`provided '${behavior}', on escape ${shouldClose ? 'should' : 'should not'} close`, () => {
+				const { handler, action } = mountLayer({ behaviorType: behavior });
+				expect(handler).not.toHaveBeenCalled();
+				dispatchEscape();
+				expect(handler).toHaveBeenCalledTimes(shouldClose ? 1 : 0);
+				action.destroy();
+			});
+		}
+	});
+
+	describe('nested layers', () => {
+		for (const { behaviors, expectedInvocations } of nestedEscapeAssertions) {
+			it(`provided behaviors '${behaviors}', expected invocations '${expectedInvocations}'`, () => {
+				const layers = behaviors.map((behaviorType) => mountLayer({ behaviorType }));
+				layers.forEach(({ handler }) => expect(handler).not.toHaveBeenCalled());
+
+				dispatchEscape();
+				layers.forEach(({ handler }, idx) => {
+					const shouldInvoke = expectedInvocations[idx];
+					expect(handler).toHaveBeenCalledTimes(shouldInvoke ? 1 : 0);
+				});
+				layers.forEach(({ action }) => action.destroy());
+			});
+		}
+	});
+
+	it('does not call handler after unmounting', () => {
+		const { handler, action } = mountLayer({ behaviorType: 'close' });
+		expect(handler).toHaveBeenCalledTimes(0);
+		action.destroy();
+		dispatchEscape();
+		expect(handler).toHaveBeenCalledTimes(0);
+	});
+
+	it('does not call handler when other key is pressed', () => {
+		const { handler, action } = mountLayer({ behaviorType: 'close' });
+		dispatchKeydownEvent(kbd.SPACE);
+		expect(handler).toHaveBeenCalledTimes(0);
+		action.destroy();
+	});
+
+	it('correctly unmounts event listener and calls handler of top-most layer', () => {
+		const { handler: handler1, action: action1 } = mountLayer({ behaviorType: 'close' });
+		const { handler: handler2, action: action2 } = mountLayer({ behaviorType: 'close' });
+		dispatchEscape();
+		expect(handler1).toHaveBeenCalledTimes(0);
+		expect(handler2).toHaveBeenCalledTimes(1);
+
+		action2.destroy();
+		dispatchEscape();
+		expect(handler1).toHaveBeenCalledTimes(1);
+		expect(handler2).toHaveBeenCalledTimes(1);
+		action1.destroy();
+	});
+
+	it('does not change position of layer in the stack when options update', () => {
+		const { handler: handler1, action: action1 } = mountLayer({ behaviorType: 'close' });
+		const { handler: handler2, action: action2 } = mountLayer({ behaviorType: 'close' });
+		dispatchEscape();
+		expect(handler1).toHaveBeenCalledTimes(0);
+		expect(handler2).toHaveBeenCalledTimes(1);
+
+		action1.update({ behaviorType: 'ignore', handler: handler1 });
+		dispatchEscape();
+		expect(handler1).toHaveBeenCalledTimes(0);
+		expect(handler2).toHaveBeenCalledTimes(2);
+		action2.destroy();
+		action1.destroy();
+	});
+
+	it('does not change position of layer in the stack when updating behaviorType store', () => {
+		const w1 = withGet.writable<EscapeBehaviorType>('close');
+		const { handler: handler1, action: action1 } = mountLayer({ behaviorType: w1 });
+		const { handler: handler2, action: action2 } = mountLayer({ behaviorType: 'close' });
+		dispatchEscape();
+		expect(handler1).toHaveBeenCalledTimes(0);
+		expect(handler2).toHaveBeenCalledTimes(1);
+
+		w1.set('ignore');
+		dispatchEscape();
+		expect(handler1).toHaveBeenCalledTimes(0);
+		expect(handler2).toHaveBeenCalledTimes(2);
+		action2.destroy();
+		action1.destroy();
+	});
+
+	it('respects updated behaviorType', () => {
+		const { handler: handler1, action: action1 } = mountLayer({ behaviorType: 'close' });
+		const { handler: handler2, action: action2 } = mountLayer({
+			behaviorType: 'defer-otherwise-close',
+		});
+		dispatchEscape();
+		expect(handler1).toHaveBeenCalledTimes(1);
+		expect(handler2).toHaveBeenCalledTimes(0);
+
+		action2.update({ behaviorType: 'close', handler: handler2 });
+		dispatchEscape();
+		expect(handler1).toHaveBeenCalledTimes(1);
+		expect(handler2).toHaveBeenCalledTimes(1);
+		action2.destroy();
+		action1.destroy();
+	});
+
+	it('respects updated behaviorType store', () => {
+		const w2 = withGet.writable<EscapeBehaviorType>('defer-otherwise-close');
+		const { handler: handler1, action: action1 } = mountLayer({ behaviorType: 'close' });
+		const { handler: handler2, action: action2 } = mountLayer({ behaviorType: w2 });
+		dispatchEscape();
+		expect(handler1).toHaveBeenCalledTimes(1);
+		expect(handler2).toHaveBeenCalledTimes(0);
+
+		w2.set('close');
+		dispatchEscape();
+		expect(handler1).toHaveBeenCalledTimes(1);
+		expect(handler2).toHaveBeenCalledTimes(1);
+		action2.destroy();
+		action1.destroy();
+	});
+
+	it('respects updating behaviorType from string value to store', () => {
+		const { handler: handler1, action: action1 } = mountLayer({ behaviorType: 'close' });
+		const { handler: handler2, action: action2 } = mountLayer({ behaviorType: 'close' });
+		dispatchEscape();
+		expect(handler1).toHaveBeenCalledTimes(0);
+		expect(handler2).toHaveBeenCalledTimes(1);
+
+		const w2 = withGet.writable<EscapeBehaviorType>('ignore');
+		action2.update({ behaviorType: w2, handler: handler2 });
+		dispatchEscape();
+		expect(handler1).toHaveBeenCalledTimes(0);
+		expect(handler2).toHaveBeenCalledTimes(1);
+		action2.destroy();
+		action1.destroy();
+	});
+});
diff --git a/src/tests/combobox/Combobox.spec.ts b/src/tests/combobox/Combobox.spec.ts
index 725c6f21..5e721044 100644
--- a/src/tests/combobox/Combobox.spec.ts
+++ b/src/tests/combobox/Combobox.spec.ts
@@ -182,9 +182,9 @@ describe('Combobox', () => {
 		expect(menu).not.toBeVisible();
 	});
 
-	test('Respects the `closeOnEscape` prop', async () => {
+	test('Respects the `escapeBehavior` prop', async () => {
 		const user = userEvent.setup();
-		const { getByTestId } = render(ComboboxTest, { closeOnEscape: false });
+		const { getByTestId } = render(ComboboxTest, { escapeBehavior: 'ignore' });
 
 		const input = getByTestId('input');
 		const menu = getByTestId('menu');
@@ -211,7 +211,7 @@ describe('Combobox', () => {
 
 	test('should not prevent focusing on another input on outside interaction of combobox', async () => {
 		const user = userEvent.setup();
-		const { getByTestId } = render(ComboboxTest, { closeOnEscape: false });
+		const { getByTestId } = render(ComboboxTest);
 
 		const input = getByTestId('input');
 		const menu = getByTestId('menu');
@@ -422,10 +422,10 @@ describe('Combobox (forceVisible)', () => {
 		await waitFor(() => expect(getMenu()).toBeNull());
 	});
 
-	test('Respects the `closeOnEscape` prop', async () => {
+	test('Respects the `escapeBehavior` prop', async () => {
 		const user = userEvent.setup();
 		const { getByTestId, queryByTestId } = render(ComboboxForceVisibleTest, {
-			closeOnEscape: false,
+			escapeBehavior: 'ignore',
 		});
 
 		const input = getByTestId('input');
diff --git a/src/tests/combobox/ComboboxForceVisibleTest.svelte b/src/tests/combobox/ComboboxForceVisibleTest.svelte
index d0673b04..83942b0b 100644
--- a/src/tests/combobox/ComboboxForceVisibleTest.svelte
+++ b/src/tests/combobox/ComboboxForceVisibleTest.svelte
@@ -18,7 +18,7 @@
 	export let defaultValue: string | undefined = undefined;
 	export let ids: CreateComboboxProps<unknown>['ids'] = undefined;
 	export let onOutsideClick: CreateComboboxProps<unknown>['onOutsideClick'] = undefined;
-	export let closeOnEscape: CreateComboboxProps<unknown>['closeOnEscape'] = undefined;
+	export let escapeBehavior: CreateComboboxProps<unknown>['escapeBehavior'] = undefined;
 
 	const {
 		elements: { menu, input, option, label },
@@ -34,7 +34,7 @@
 				: undefined,
 			ids,
 			onOutsideClick,
-			closeOnEscape,
+			escapeBehavior,
 			forceVisible: true,
 		})
 	);
diff --git a/src/tests/combobox/ComboboxTest.svelte b/src/tests/combobox/ComboboxTest.svelte
index bcacbc13..44b30ba1 100644
--- a/src/tests/combobox/ComboboxTest.svelte
+++ b/src/tests/combobox/ComboboxTest.svelte
@@ -18,7 +18,7 @@
 	export let defaultValue: string | undefined = undefined;
 	export let ids: CreateComboboxProps<unknown>['ids'] = undefined;
 	export let onOutsideClick: CreateComboboxProps<unknown>['onOutsideClick'] = undefined;
-	export let closeOnEscape: CreateComboboxProps<unknown>['closeOnEscape'] = undefined;
+	export let escapeBehavior: CreateComboboxProps<unknown>['escapeBehavior'] = undefined;
 
 	const {
 		elements: { menu, input, option, label },
@@ -34,7 +34,7 @@
 				: undefined,
 			ids,
 			onOutsideClick,
-			closeOnEscape,
+			escapeBehavior,
 		})
 	);
 
diff --git a/src/tests/context-menu/ContextMenu.spec.ts b/src/tests/context-menu/ContextMenu.spec.ts
index c9bf99e7..3b33dac1 100644
--- a/src/tests/context-menu/ContextMenu.spec.ts
+++ b/src/tests/context-menu/ContextMenu.spec.ts
@@ -158,9 +158,9 @@ describe('Context Menu', () => {
 		await waitFor(() => expect(queryByTestId('menu')).toBeNull());
 	});
 
-	test('respects `closeOnEscape` prop', async () => {
+	test('respects `escapeBehavior` prop', async () => {
 		const { user, getByTestId } = await open({
-			closeOnEscape: false,
+			escapeBehavior: 'ignore',
 		});
 		await user.keyboard(kbd.ESCAPE);
 		expect(getByTestId('menu')).toBeVisible();
diff --git a/src/tests/context-menu/ContextMenuTest.svelte b/src/tests/context-menu/ContextMenuTest.svelte
index ec481d4d..aa850c5d 100644
--- a/src/tests/context-menu/ContextMenuTest.svelte
+++ b/src/tests/context-menu/ContextMenuTest.svelte
@@ -12,7 +12,7 @@
 
 	export let loop = false;
 	export let closeFocus: CreateContextMenuProps['closeFocus'] = undefined;
-	export let closeOnEscape: CreateContextMenuProps['closeOnEscape'] = true;
+	export let escapeBehavior: CreateContextMenuProps['escapeBehavior'] = 'close';
 	export let closeOnOutsideClick: CreateContextMenuProps['closeOnOutsideClick'] = true;
 	export let submenuIds: CreateContextMenuProps['ids'] = undefined;
 
@@ -23,7 +23,7 @@
 	} = createContextMenu({
 		loop,
 		closeFocus,
-		closeOnEscape,
+		escapeBehavior,
 		closeOnOutsideClick,
 		...$$restProps,
 		forceVisible: true,
diff --git a/src/tests/dialog/Dialog.spec.ts b/src/tests/dialog/Dialog.spec.ts
index c039b0d5..5069dbff 100644
--- a/src/tests/dialog/Dialog.spec.ts
+++ b/src/tests/dialog/Dialog.spec.ts
@@ -175,9 +175,9 @@ describe('Dialog', () => {
 		await waitFor(() => expect(content).not.toBeVisible());
 	});
 
-	it('Respects the `closeOnEscape` prop', async () => {
+	it('Respects the `escapeBehavior` prop', async () => {
 		const { user, content } = await open({
-			closeOnEscape: false,
+			escapeBehavior: 'ignore',
 		});
 
 		await user.keyboard(kbd.ESCAPE);
@@ -225,8 +225,8 @@ describe('Dialog', () => {
 		await waitFor(() => expect(content).not.toBeVisible());
 	});
 
-	it("Doesn't deactivate focus trap on escape provided `closeOnEscape` false", async () => {
-		const { user, content } = await open({ closeOnEscape: false });
+	it("Doesn't deactivate focus trap on escape provided `escapeBehavior` false", async () => {
+		const { user, content } = await open({ escapeBehavior: 'ignore' });
 		await user.keyboard(kbd.ESCAPE);
 		expect(content).toBeVisible();
 		await assertActiveFocusTrap(user, content);
diff --git a/src/tests/dialog/DialogNested.spec.ts b/src/tests/dialog/DialogNested.spec.ts
index b4a39e77..73f87274 100644
--- a/src/tests/dialog/DialogNested.spec.ts
+++ b/src/tests/dialog/DialogNested.spec.ts
@@ -107,8 +107,8 @@ describe('Nested Dialogs', () => {
 		await waitFor(() => expect(getByTestId('content')).not.toBeNull());
 		// Testing focus-trap is a bit flaky. So the focusable element is
 		// always content here.
-		await waitFor(() => expect(document.activeElement).toBe(getByTestId('content')));
+		expect(document.activeElement).toBe(getByTestId('content'));
 		await user.tab();
-		await waitFor(() => expect(document.activeElement).toBe(getByTestId('content')));
+		expect(document.activeElement).toBe(getByTestId('content'));
 	});
 });
diff --git a/src/tests/dropdown-menu/DropdownMenu.spec.ts b/src/tests/dropdown-menu/DropdownMenu.spec.ts
index cddde6f5..dd5250eb 100644
--- a/src/tests/dropdown-menu/DropdownMenu.spec.ts
+++ b/src/tests/dropdown-menu/DropdownMenu.spec.ts
@@ -203,9 +203,9 @@ describe('Dropdown Menu (Default)', () => {
 		expect(closeFocus).toHaveFocus();
 	});
 
-	test('respects `closeOnEscape` prop', async () => {
+	test('respects `escapeBehavior` prop', async () => {
 		const { user, trigger, getByTestId } = setup({
-			closeOnEscape: false,
+			escapeBehavior: 'ignore',
 		});
 		const menu = getByTestId('menu');
 		await user.click(trigger);
diff --git a/src/tests/dropdown-menu/DropdownMenuTest.svelte b/src/tests/dropdown-menu/DropdownMenuTest.svelte
index ac04e2f9..20a526f0 100644
--- a/src/tests/dropdown-menu/DropdownMenuTest.svelte
+++ b/src/tests/dropdown-menu/DropdownMenuTest.svelte
@@ -11,7 +11,7 @@
 
 	export let loop = false;
 	export let closeFocus: CreateDropdownMenuProps['closeFocus'] = undefined;
-	export let closeOnEscape: CreateDropdownMenuProps['closeOnEscape'] = true;
+	export let escapeBehavior: CreateDropdownMenuProps['escapeBehavior'] = 'close';
 	export let closeOnOutsideClick: CreateDropdownMenuProps['closeOnOutsideClick'] = true;
 	export let submenuIds: CreateDropdownMenuProps['ids'] = undefined;
 
@@ -22,7 +22,7 @@
 		removeUndefined({
 			loop,
 			closeFocus,
-			closeOnEscape,
+			escapeBehavior,
 			closeOnOutsideClick,
 			...$$restProps,
 		})
diff --git a/src/tests/escape-keydown/ComboboxTest.svelte b/src/tests/escape-keydown/ComboboxTest.svelte
new file mode 100644
index 00000000..0a465d6a
--- /dev/null
+++ b/src/tests/escape-keydown/ComboboxTest.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { createCombobox, melt, type CreateComboboxProps } from '$lib/index.js';
+
+	type $$Props = CreateComboboxProps & { setRootEscapeBehaviorIgnore: () => void };
+	export let setRootEscapeBehaviorIgnore: () => void;
+
+	const {
+		elements: { input, menu },
+		states: { open },
+	} = createCombobox({ ...$$restProps, forceVisible: true });
+</script>
+
+<input use:melt={$input} data-testid="combobox-trigger" />
+{#if $open}
+	<div use:melt={$menu} data-testid="combobox-content">
+		<button
+			data-testid="combobox-set-parent-escape-behavior-ignore"
+			on:click={setRootEscapeBehaviorIgnore}
+		>
+			set root escapeBehavior: ignore
+		</button>
+	</div>
+{/if}
diff --git a/src/tests/escape-keydown/DialogTest.svelte b/src/tests/escape-keydown/DialogTest.svelte
new file mode 100644
index 00000000..5fd01c69
--- /dev/null
+++ b/src/tests/escape-keydown/DialogTest.svelte
@@ -0,0 +1,27 @@
+<script lang="ts">
+	import { createDialog, melt, type CreateDialogProps } from '$lib/index.js';
+
+	type $$Props = CreateDialogProps & { setRootEscapeBehaviorIgnore: () => void };
+	export let setRootEscapeBehaviorIgnore: () => void;
+
+	export let escapeBehavior: CreateDialogProps['escapeBehavior'] = 'close';
+
+	const {
+		elements: { trigger, content, portalled },
+		states: { open },
+	} = createDialog({ escapeBehavior, forceVisible: true });
+</script>
+
+<button use:melt={$trigger} data-testid="dialog-trigger">Open</button>
+{#if $open}
+	<div use:melt={$portalled}>
+		<div use:melt={$content} data-testid="dialog-content">
+			<button
+				data-testid="dialog-set-parent-escape-behavior-ignore"
+				on:click={setRootEscapeBehaviorIgnore}
+			>
+				set root escapeBehavior: ignore
+			</button>
+		</div>
+	</div>
+{/if}
diff --git a/src/tests/escape-keydown/EscapeKeydown.spec.ts b/src/tests/escape-keydown/EscapeKeydown.spec.ts
new file mode 100644
index 00000000..c06aaca1
--- /dev/null
+++ b/src/tests/escape-keydown/EscapeKeydown.spec.ts
@@ -0,0 +1,137 @@
+import { describe, it } from 'vitest';
+import { render, waitFor } from '@testing-library/svelte';
+import { userEvent } from '@testing-library/user-event';
+import EscapeKeydownRoot from './EscapeKeydownRoot.svelte';
+import type { CreateDialogProps } from '$lib/index.js';
+import { testKbd } from '../utils.js';
+
+async function setup(props: CreateDialogProps) {
+	const user = userEvent.setup();
+	const returned = render(EscapeKeydownRoot, props);
+	const rootTrigger = returned.getByTestId('root-dialog-trigger');
+	const getRootContent = () => returned.queryByTestId('root-dialog-content');
+	expect(getRootContent()).toBeNull();
+	await user.click(rootTrigger);
+	expect(getRootContent()).toBeVisible();
+	return { user, ...returned, rootTrigger, getRootContent };
+}
+
+const components = [
+	'dialog',
+	'combobox',
+	'link-preview',
+	'menubar',
+	'menu',
+	'popover',
+	'select',
+	'tooltip',
+] as const;
+
+const componentsToHover = new Set<(typeof components)[number]>(['tooltip', 'link-preview']);
+
+describe('Nested Escape Keydown Behaviors', () => {
+	describe.each(components)('dialog + %s', (componentName) => {
+		it('`escapeBehavior: close` on child should only close child', async () => {
+			const { user, getByTestId, queryByTestId, getRootContent } = await setup({
+				escapeBehavior: 'close',
+			});
+			const trigger = getByTestId(`${componentName}-trigger`);
+			const getContent = () => queryByTestId(`${componentName}-content`);
+
+			if (componentsToHover.has(componentName)) {
+				await user.hover(trigger);
+			} else {
+				await user.click(trigger);
+			}
+
+			expect(getContent()).toBeVisible();
+			await user.keyboard(testKbd.ESCAPE);
+			expect(getContent()).toBeNull();
+			expect(getRootContent()).toBeVisible();
+		});
+
+		it('`escapeBehavior: ignore` on child should neither close child nor parent', async () => {
+			const { user, getByTestId, queryByTestId, getRootContent } = await setup({
+				escapeBehavior: 'ignore',
+			});
+			const trigger = getByTestId(`${componentName}-trigger`);
+			const getContent = () => queryByTestId(`${componentName}-content`);
+
+			if (componentsToHover.has(componentName)) {
+				await user.hover(trigger);
+			} else {
+				await user.click(trigger);
+			}
+
+			expect(getContent()).toBeVisible();
+			await user.keyboard(testKbd.ESCAPE);
+			expect(getContent()).toBeVisible();
+			expect(getRootContent()).toBeVisible();
+		});
+
+		it('`escapeBehavior: defer-otherwise-close` on child should close both parent and child', async () => {
+			const { user, getByTestId, queryByTestId, getRootContent } = await setup({
+				escapeBehavior: 'defer-otherwise-close',
+			});
+			const trigger = getByTestId(`${componentName}-trigger`);
+			const getContent = () => queryByTestId(`${componentName}-content`);
+
+			if (componentsToHover.has(componentName)) {
+				await user.hover(trigger);
+			} else {
+				await user.click(trigger);
+			}
+
+			expect(getContent()).toBeVisible();
+			await user.keyboard(testKbd.ESCAPE);
+			await waitFor(() => expect(getContent()).toBeNull());
+			expect(getRootContent()).toBeNull();
+		});
+
+		it('`escapeBehavior: defer-otherwise-ignore` on child should close both parent and child', async () => {
+			const { user, getByTestId, queryByTestId, getRootContent } = await setup({
+				escapeBehavior: 'defer-otherwise-ignore',
+			});
+			const trigger = getByTestId(`${componentName}-trigger`);
+			const getContent = () => queryByTestId(`${componentName}-content`);
+
+			if (componentsToHover.has(componentName)) {
+				await user.hover(trigger);
+			} else {
+				await user.click(trigger);
+			}
+
+			expect(getContent()).toBeVisible();
+			await user.keyboard(testKbd.ESCAPE);
+			await waitFor(() => expect(getContent()).toBeNull());
+			expect(getRootContent()).toBeNull();
+		});
+
+		it('updating parent `escapeBehavior` should not intercept escape key press', async () => {
+			const { user, getByTestId, queryByTestId, getRootContent } = await setup({
+				escapeBehavior: 'close',
+			});
+			const trigger = getByTestId(`${componentName}-trigger`);
+			const getContent = () => queryByTestId(`${componentName}-content`);
+
+			if (componentsToHover.has(componentName)) {
+				await user.hover(trigger);
+			} else {
+				await user.click(trigger);
+			}
+
+			expect(getContent()).toBeVisible();
+
+			/**
+			 * If we update parent `escapeBehaivor` to `ignore` and escape is pressed,
+			 * parent element should stay open and child element should close.
+			 * If the parent element's position in the stack was reset, on escape no element would be closed
+			 * because the parent would be at the "top" of the stack and it's set to ignore escape key presses.
+			 */
+			await user.click(getByTestId(`${componentName}-set-parent-escape-behavior-ignore`));
+			await user.keyboard(testKbd.ESCAPE);
+			expect(getContent()).toBeNull();
+			expect(getRootContent()).toBeVisible();
+		});
+	});
+});
diff --git a/src/tests/escape-keydown/EscapeKeydownRoot.svelte b/src/tests/escape-keydown/EscapeKeydownRoot.svelte
new file mode 100644
index 00000000..931de31a
--- /dev/null
+++ b/src/tests/escape-keydown/EscapeKeydownRoot.svelte
@@ -0,0 +1,38 @@
+<script lang="ts">
+	import { createDialog, melt, type CreateDialogProps } from '$lib/index.js';
+	import Dialog from './DialogTest.svelte';
+	import Combobox from './ComboboxTest.svelte';
+	import LinkPreview from './LinkPreviewTest.svelte';
+	import Menubar from './MenubarTest.svelte';
+	import Menu from './MenuTest.svelte';
+	import Popover from './PopoverTest.svelte';
+	import Select from './SelectTest.svelte';
+	import Tooltip from './TooltipTest.svelte';
+
+	type $$Props = CreateDialogProps;
+	export let escapeBehavior: CreateDialogProps['escapeBehavior'] = 'close';
+
+	const {
+		elements: { trigger, content, portalled },
+		states: { open },
+		options: { escapeBehavior: rootEscapeBehavior },
+	} = createDialog({ forceVisible: true });
+
+	const setRootEscapeBehaviorIgnore = () => rootEscapeBehavior.set('ignore');
+</script>
+
+<button use:melt={$trigger} data-testid="root-dialog-trigger">Open</button>
+{#if $open}
+	<div use:melt={$portalled}>
+		<div use:melt={$content} data-testid="root-dialog-content">
+			<Dialog {escapeBehavior} {setRootEscapeBehaviorIgnore} />
+			<Combobox {escapeBehavior} {setRootEscapeBehaviorIgnore} />
+			<LinkPreview {escapeBehavior} {setRootEscapeBehaviorIgnore} />
+			<Menubar {escapeBehavior} {setRootEscapeBehaviorIgnore} />
+			<Menu {escapeBehavior} {setRootEscapeBehaviorIgnore} />
+			<Popover {escapeBehavior} {setRootEscapeBehaviorIgnore} />
+			<Select {escapeBehavior} {setRootEscapeBehaviorIgnore} />
+			<Tooltip {escapeBehavior} {setRootEscapeBehaviorIgnore} />
+		</div>
+	</div>
+{/if}
diff --git a/src/tests/escape-keydown/LinkPreviewTest.svelte b/src/tests/escape-keydown/LinkPreviewTest.svelte
new file mode 100644
index 00000000..1834cc72
--- /dev/null
+++ b/src/tests/escape-keydown/LinkPreviewTest.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { createLinkPreview, melt, type CreateLinkPreviewProps } from '$lib/index.js';
+
+	type $$Props = CreateLinkPreviewProps & { setRootEscapeBehaviorIgnore: () => void };
+	export let setRootEscapeBehaviorIgnore: () => void;
+
+	const {
+		elements: { trigger, content },
+		states: { open },
+	} = createLinkPreview({ ...$$restProps, forceVisible: true, openDelay: 0, closeDelay: 0 });
+</script>
+
+<button use:melt={$trigger} data-testid="link-preview-trigger">trigger</button>
+{#if $open}
+	<div use:melt={$content} data-testid="link-preview-content">
+		<button
+			data-testid="link-preview-set-parent-escape-behavior-ignore"
+			on:click={setRootEscapeBehaviorIgnore}
+		>
+			set root escapeBehavior: ignore
+		</button>
+	</div>
+{/if}
diff --git a/src/tests/escape-keydown/MenuTest.svelte b/src/tests/escape-keydown/MenuTest.svelte
new file mode 100644
index 00000000..c57441fa
--- /dev/null
+++ b/src/tests/escape-keydown/MenuTest.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { createDropdownMenu, melt, type CreateDropdownMenuProps } from '$lib/index.js';
+
+	type $$Props = CreateDropdownMenuProps & { setRootEscapeBehaviorIgnore: () => void };
+	export let setRootEscapeBehaviorIgnore: () => void;
+
+	const {
+		elements: { trigger, menu },
+		states: { open },
+	} = createDropdownMenu({ ...$$restProps, forceVisible: true });
+</script>
+
+<button use:melt={$trigger} data-testid="menu-trigger">trigger</button>
+{#if $open}
+	<div use:melt={$menu} data-testid="menu-content">
+		<button
+			data-testid="menu-set-parent-escape-behavior-ignore"
+			on:click={setRootEscapeBehaviorIgnore}
+		>
+			set root escapeBehavior: ignore
+		</button>
+	</div>
+{/if}
diff --git a/src/tests/escape-keydown/MenubarTest.svelte b/src/tests/escape-keydown/MenubarTest.svelte
new file mode 100644
index 00000000..9fc82fa0
--- /dev/null
+++ b/src/tests/escape-keydown/MenubarTest.svelte
@@ -0,0 +1,31 @@
+<script lang="ts">
+	import { createMenubar, melt, type CreateMenubarMenuProps } from '$lib/index.js';
+
+	type $$Props = CreateMenubarMenuProps & { setRootEscapeBehaviorIgnore: () => void };
+	export let escapeBehavior: CreateMenubarMenuProps['escapeBehavior'] = 'close';
+	export let setRootEscapeBehaviorIgnore: () => void;
+
+	const {
+		elements: { menubar },
+		builders: { createMenu },
+	} = createMenubar({ escapeBehavior });
+
+	const {
+		elements: { trigger, menu },
+		states: { open },
+	} = createMenu({ ...$$restProps, escapeBehavior, forceVisible: true });
+</script>
+
+<div use:melt={$menubar}>
+	<button use:melt={$trigger} data-testid="menubar-trigger">trigger</button>
+	{#if $open}
+		<div use:melt={$menu} data-testid="menubar-content">
+			<button
+				data-testid="menubar-set-parent-escape-behavior-ignore"
+				on:click={setRootEscapeBehaviorIgnore}
+			>
+				set root escapeBehavior: ignore
+			</button>
+		</div>
+	{/if}
+</div>
diff --git a/src/tests/escape-keydown/PopoverTest.svelte b/src/tests/escape-keydown/PopoverTest.svelte
new file mode 100644
index 00000000..e71d6535
--- /dev/null
+++ b/src/tests/escape-keydown/PopoverTest.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { createPopover, melt, type CreatePopoverProps } from '$lib/index.js';
+
+	type $$Props = CreatePopoverProps & { setRootEscapeBehaviorIgnore: () => void };
+	export let setRootEscapeBehaviorIgnore: () => void;
+
+	const {
+		elements: { trigger, content },
+		states: { open },
+	} = createPopover({ ...$$restProps, forceVisible: true });
+</script>
+
+<button use:melt={$trigger} data-testid="popover-trigger">trigger</button>
+{#if $open}
+	<div use:melt={$content} data-testid="popover-content">
+		<button
+			data-testid="popover-set-parent-escape-behavior-ignore"
+			on:click={setRootEscapeBehaviorIgnore}
+		>
+			set root escapeBehavior: ignore
+		</button>
+	</div>
+{/if}
diff --git a/src/tests/escape-keydown/SelectTest.svelte b/src/tests/escape-keydown/SelectTest.svelte
new file mode 100644
index 00000000..7e82060e
--- /dev/null
+++ b/src/tests/escape-keydown/SelectTest.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { createSelect, melt, type CreateSelectProps } from '$lib/index.js';
+
+	type $$Props = CreateSelectProps & { setRootEscapeBehaviorIgnore: () => void };
+	export let setRootEscapeBehaviorIgnore: () => void;
+
+	const {
+		elements: { trigger, menu },
+		states: { open },
+	} = createSelect({ ...$$restProps, forceVisible: true });
+</script>
+
+<button use:melt={$trigger} data-testid="select-trigger">trigger</button>
+{#if $open}
+	<div use:melt={$menu} data-testid="select-content">
+		<button
+			data-testid="select-set-parent-escape-behavior-ignore"
+			on:click={setRootEscapeBehaviorIgnore}
+		>
+			set root escapeBehavior: ignore
+		</button>
+	</div>
+{/if}
diff --git a/src/tests/escape-keydown/TooltipTest.svelte b/src/tests/escape-keydown/TooltipTest.svelte
new file mode 100644
index 00000000..8c4558ba
--- /dev/null
+++ b/src/tests/escape-keydown/TooltipTest.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { createTooltip, melt, type CreateTooltipProps } from '$lib/index.js';
+
+	type $$Props = CreateTooltipProps & { setRootEscapeBehaviorIgnore: () => void };
+	export let setRootEscapeBehaviorIgnore: () => void;
+
+	const {
+		elements: { trigger, content },
+		states: { open },
+	} = createTooltip({ ...$$restProps, forceVisible: true, openDelay: 0, closeDelay: 0 });
+</script>
+
+<button use:melt={$trigger} data-testid="tooltip-trigger">trigger</button>
+{#if $open}
+	<div use:melt={$content} data-testid="tooltip-content">
+		<button
+			data-testid="tooltip-set-parent-escape-behavior-ignore"
+			on:click={setRootEscapeBehaviorIgnore}
+		>
+			set root escapeBehavior: ignore
+		</button>
+	</div>
+{/if}
diff --git a/src/tests/link-preview/LinkPreview.spec.ts b/src/tests/link-preview/LinkPreview.spec.ts
index 7d2de121..57b04074 100644
--- a/src/tests/link-preview/LinkPreview.spec.ts
+++ b/src/tests/link-preview/LinkPreview.spec.ts
@@ -45,9 +45,9 @@ describe('LinkPreview (Default)', () => {
 		await user.keyboard(kbd.ESCAPE);
 		await waitFor(() => expect(content).not.toBeVisible());
 	});
-	test('Respects the `closeOnEscape` prop', async () => {
+	test('Respects the `escapeBehavior` prop', async () => {
 		const { getByTestId, trigger, user } = setup({
-			closeOnEscape: false,
+			escapeBehavior: 'ignore',
 		});
 		const content = getByTestId('content');
 
diff --git a/src/tests/popover/Popover.spec.ts b/src/tests/popover/Popover.spec.ts
index 545462f3..b85d7883 100644
--- a/src/tests/popover/Popover.spec.ts
+++ b/src/tests/popover/Popover.spec.ts
@@ -109,8 +109,8 @@ describe('Popover (Default)', () => {
 		expect(content.id).toBe(ids.content);
 	});
 
-	it("Doesn't deactivate focus trap on escape provided `closeOnEscape` false", async () => {
-		const { getByTestId, user, content } = await open({ closeOnEscape: false });
+	it("Doesn't deactivate focus trap on escape provided `escapeBehavior` false", async () => {
+		const { getByTestId, user, content } = await open({ escapeBehavior: 'ignore' });
 		await user.keyboard(kbd.ESCAPE);
 		expect(content).toBeVisible();
 		expect(getByTestId('content')).toHaveFocus();
@@ -152,4 +152,13 @@ describe('Popover (Default)', () => {
 		expect(content).toBeVisible();
 		await assertActiveFocusTrap(user, content);
 	});
+
+	it('closes popover when opening a sibling popover', async () => {
+		const { getByTestId, user, content } = await open();
+		await user.click(getByTestId('click-interceptor'));
+		expect(content).toBeVisible();
+		await user.click(getByTestId('trigger-2'));
+		await waitFor(() => expect(content).not.toBeVisible());
+		expect(getByTestId('content-2')).toBeVisible();
+	});
 });
diff --git a/src/tests/popover/PopoverTest.svelte b/src/tests/popover/PopoverTest.svelte
index c5d9a97f..28e51e25 100644
--- a/src/tests/popover/PopoverTest.svelte
+++ b/src/tests/popover/PopoverTest.svelte
@@ -15,6 +15,10 @@
 		closeFocus,
 		...$$restProps,
 	});
+
+	const {
+		elements: { trigger: triggerB, content: contentB },
+	} = createPopover();
 </script>
 
 <button data-testid="closeFocus" id="closeFocus"> focus me on close </button>
@@ -66,6 +70,15 @@
 <div data-testid="outside" />
 <button on:click|stopPropagation data-testid="click-interceptor">click interceptor</button>
 
+<button type="button" class="trigger" use:melt={$triggerB} data-testid="trigger-2">
+	<span>Open Popover</span>
+</button>
+<div use:melt={$contentB} class="content" data-testid="content-2">
+	<div class="flex flex-col gap-2.5">
+		<p>Dimensions</p>
+	</div>
+</div>
+
 <style lang="postcss">
 	fieldset {
 		@apply flex items-center gap-5;
diff --git a/src/tests/portal/Dialog.svelte b/src/tests/portal/Dialog.svelte
index 4bf78443..0b276e4e 100644
--- a/src/tests/portal/Dialog.svelte
+++ b/src/tests/portal/Dialog.svelte
@@ -1,26 +1,32 @@
 <script lang="ts">
-	import { createDialog, melt } from '$lib/index.js';
-	import { fade } from 'svelte/transition';
+	import { createDialog, melt, type CreateDialogProps } from '$lib/index.js';
 	import { initLevel } from './level.js';
 
+	export let portal: CreateDialogProps['portal'];
+	export let forceVisible: CreateDialogProps['forceVisible'];
+
 	const {
 		elements: { trigger, overlay, content, title, description, close, portalled },
-	} = createDialog({});
+		states: { open },
+	} = createDialog({ portal, forceVisible });
 
 	const level = initLevel();
 </script>
 
 <button use:melt={$trigger} data-testid="dialog-trigger-{level}">Open</button>
-<div use:melt={$portalled} data-testid="dialog-portalled-{level}">
-	<div use:melt={$overlay} class="overlay" data-testid="dialog-overlay-{level}" transition:fade />
-	<div use:melt={$content} class="content" data-testid="dialog-content-{level}">
-		<h2 use:melt={$title}>Title</h2>
-		<p use:melt={$description}>Description</p>
-
-		<button use:melt={$close} data-testid="dialog-closer-{level}">Close</button>
-		<slot />
+
+{#if $open || !forceVisible}
+	<div use:melt={$portalled} data-testid="dialog-portalled-{level}">
+		<div use:melt={$overlay} class="overlay" data-testid="dialog-overlay-{level}" />
+		<div use:melt={$content} class="content" data-testid="dialog-content-{level}">
+			<h2 use:melt={$title}>Title</h2>
+			<p use:melt={$description}>Description</p>
+
+			<button use:melt={$close} data-testid="dialog-closer-{level}">Close</button>
+			<slot />
+		</div>
 	</div>
-</div>
+{/if}
 <div data-testid="dialog-outside-{level}" />
 
 <style>
diff --git a/src/tests/portal/DropdownMenu.svelte b/src/tests/portal/DropdownMenu.svelte
new file mode 100644
index 00000000..10e56ac9
--- /dev/null
+++ b/src/tests/portal/DropdownMenu.svelte
@@ -0,0 +1,27 @@
+<script lang="ts">
+	import { createDropdownMenu, melt, type CreateDropdownMenuProps } from '$lib/index.js';
+	import { initLevel } from './level.js';
+
+	type $$Props = CreateDropdownMenuProps;
+
+	export let portal: CreateDropdownMenuProps['portal'] = undefined;
+	export let forceVisible: CreateDropdownMenuProps['forceVisible'] = false;
+
+	const {
+		elements: { trigger, menu, item },
+		states: { open },
+	} = createDropdownMenu({ forceVisible, portal, ...$$restProps });
+
+	const level = initLevel();
+</script>
+
+<button use:melt={$trigger} data-testid="dropdown-menu-trigger-{level}">Open Popover</button>
+
+{#if $open || !forceVisible}
+	<div class="menu" use:melt={$menu} data-testid="dropdown-menu-content-{level}">
+		<div class="item" use:melt={$item}>Item 1</div>
+		<div class="item" use:melt={$item}>Item 2</div>
+		<slot />
+	</div>
+{/if}
+<div data-testid="dropdown-menu-outside-{level}" />
diff --git a/src/tests/portal/Popover.svelte b/src/tests/portal/Popover.svelte
index feb9ac13..4923fba9 100644
--- a/src/tests/portal/Popover.svelte
+++ b/src/tests/portal/Popover.svelte
@@ -1,11 +1,15 @@
 <script lang="ts">
-	import { createPopover, melt } from '$lib/index.js';
+	import { createPopover, melt, type CreatePopoverProps } from '$lib/index.js';
 	import { Settings2 } from '$icons/index.js';
 	import { initLevel } from './level.js';
 
+	export let portal: CreatePopoverProps['portal'];
+	export let forceVisible: CreatePopoverProps['forceVisible'];
+
 	const {
 		elements: { trigger, content, arrow, close },
-	} = createPopover();
+		states: { open },
+	} = createPopover({ portal, forceVisible });
 
 	const level = initLevel();
 </script>
@@ -21,9 +25,11 @@
 	<span class="sr-only">Open Popover</span>
 </button>
 
-<div use:melt={$content} data-testid="popover-content-{level}">
-	<div use:melt={$arrow} data-testid="popover-arrow-{level}" />
-	<slot />
-	<button use:melt={$close} data-testid="popover-close-{level}"> Close </button>
-</div>
+{#if $open || !forceVisible}
+	<div use:melt={$content} data-testid="popover-content-{level}">
+		<div use:melt={$arrow} data-testid="popover-arrow-{level}" />
+		<slot />
+		<button use:melt={$close} data-testid="popover-close-{level}"> Close </button>
+	</div>
+{/if}
 <div data-testid="popover-outside-{level}" />
diff --git a/src/tests/portal/PopoverSelect.svelte b/src/tests/portal/PopoverSelect.svelte
new file mode 100644
index 00000000..28020e73
--- /dev/null
+++ b/src/tests/portal/PopoverSelect.svelte
@@ -0,0 +1,37 @@
+<script lang="ts">
+	import { createPopover, createSelect, melt, type CreateSelectProps } from '$lib/index.js';
+	import { Settings2 } from '$icons/index.js';
+	import type { PortalConfig } from '$lib/internal/actions/portal.js';
+
+	export let portal: PortalConfig;
+
+	const {
+		elements: { trigger, content, arrow, close },
+	} = createPopover({ portal });
+
+	const {
+		elements: { trigger: selectTrigger, menu, option },
+	} = createSelect({ portal });
+</script>
+
+<button
+	type="button"
+	class="trigger"
+	use:melt={$trigger}
+	aria-label="Update dimensions"
+	data-testid="popover-trigger"
+>
+	<Settings2 class="h-4 w-4" />
+	<span class="sr-only">Open Popover</span>
+</button>
+
+<div use:melt={$content} data-testid="popover-content">
+	<div use:melt={$arrow} data-testid="popover-arrow" />
+	<button use:melt={$selectTrigger} data-testid="select-trigger"> Open Select </button>
+	<div use:melt={$menu} data-testid="select-menu">
+		<div use:melt={$option({ value: 'option 1', label: 'option 1' })}>option 1</div>
+	</div>
+	<button use:melt={$close} data-testid="popover-close">Close</button>
+</div>
+<div class="mt-48 h-[500px] w-[500px] bg-black" />
+<div data-testid="outside" class="h-5 w-5" />
diff --git a/src/tests/portal/PopoverTagsInput.svelte b/src/tests/portal/PopoverTagsInput.svelte
new file mode 100644
index 00000000..3fd1cb7d
--- /dev/null
+++ b/src/tests/portal/PopoverTagsInput.svelte
@@ -0,0 +1,46 @@
+<script lang="ts">
+	import { createPopover, createTagsInput, melt, type CreatePopoverProps } from '$lib/index.js';
+	import { Settings2 } from '$icons/index.js';
+
+	export let portal: CreatePopoverProps['portal'];
+
+	const {
+		elements: { trigger, content, arrow, close },
+	} = createPopover({ portal });
+
+	export let defaultTags = ['Svelte', 'Typescript'];
+	const {
+		elements: { root, input, tag, deleteTrigger, edit },
+		states: { tags },
+	} = createTagsInput({ defaultTags });
+</script>
+
+<button
+	type="button"
+	class="trigger"
+	use:melt={$trigger}
+	aria-label="Update dimensions"
+	data-testid="popover-trigger"
+>
+	<Settings2 class="h-4 w-4" />
+	<span class="sr-only">Open Popover</span>
+</button>
+
+<div use:melt={$content} data-testid="popover-content">
+	<div use:melt={$arrow} data-testid="popover-arrow" />
+	<div use:melt={$root}>
+		{#each $tags as t, i}
+			<div data-testid="tag-{i}" use:melt={$tag(t)}>
+				<span>{t.value}</span>
+				<button data-testid="delete-tag-{i}" use:melt={$deleteTrigger(t)}> Delete </button>
+			</div>
+			<div data-testid="edit-tag-{i}" use:melt={$edit(t)} />
+		{/each}
+
+		<input use:melt={$input} type="text" placeholder="Enter tags..." />
+	</div>
+
+	<button use:melt={$close} data-testid="popover-close"> Close </button>
+</div>
+<div class="mt-48 h-[500px] w-[500px] bg-black" />
+<div data-testid="outside" class="h-5 w-5" />
diff --git a/src/tests/portal/PopoverTooltip.svelte b/src/tests/portal/PopoverTooltip.svelte
index c73ea7f3..4e9f8a0c 100644
--- a/src/tests/portal/PopoverTooltip.svelte
+++ b/src/tests/portal/PopoverTooltip.svelte
@@ -1,16 +1,22 @@
 <script lang="ts">
-	import { createPopover, createTooltip, melt } from '$lib/index.js';
+	import { createPopover, createTooltip, melt, type CreateTooltipProps } from '$lib/index.js';
 	import { Settings2 } from '$icons/index.js';
+	import type { PortalConfig } from '$lib/internal/actions/portal.js';
+
+	export let portal: PortalConfig;
+	export let tooltipEscapeBehavior: CreateTooltipProps['escapeBehavior'] = 'close';
 
 	const {
 		elements: { trigger, content, arrow, close },
-	} = createPopover();
+	} = createPopover({ portal });
 
 	const {
 		elements: { trigger: ttTrigger, content: ttContent },
 	} = createTooltip({
 		openDelay: 0,
 		closeDelay: 0,
+		portal,
+		escapeBehavior: tooltipEscapeBehavior,
 	});
 </script>
 
diff --git a/src/tests/portal/Portal.spec.ts b/src/tests/portal/Portal.spec.ts
index 6cb1228d..e68efc54 100644
--- a/src/tests/portal/Portal.spec.ts
+++ b/src/tests/portal/Portal.spec.ts
@@ -2,10 +2,21 @@ import { describe, it } from 'vitest';
 import { render, waitFor } from '@testing-library/svelte';
 import { userEvent } from '@testing-library/user-event';
 import PopoverTooltip from './PopoverTooltip.svelte';
+import PopoverTagsInput from './PopoverTagsInput.svelte';
+import PopoverSelect from './PopoverSelect.svelte';
+import { assertActiveFocusTrap, testKbd as kbd } from '../utils.js';
+import type { PortalConfig } from '$lib/internal/actions/portal.js';
+import type { EscapeBehaviorType } from '$lib/internal/actions/index.js';
 
-function setupPopoverTooltip() {
+function setupPopoverTooltip({
+	portalType,
+	tooltipEscapeBehavior,
+}: {
+	portalType: PortalConfig;
+	tooltipEscapeBehavior?: EscapeBehaviorType;
+}) {
 	const user = userEvent.setup();
-	const returned = render(PopoverTooltip);
+	const returned = render(PopoverTooltip, { portal: portalType, tooltipEscapeBehavior });
 	const popoverTrigger = returned.getByTestId('popover-trigger');
 	const popoverContent = returned.getByTestId('popover-content');
 	const tooltipTrigger = returned.getByTestId('tooltip-trigger');
@@ -15,53 +26,168 @@ function setupPopoverTooltip() {
 	return { user, ...returned, elements };
 }
 
-describe('Portal Behaviors - Popover & Tooltip', () => {
-	it('should not close the popover when the tooltip content is clicked', async () => {
-		const { elements, user } = setupPopoverTooltip();
-
-		expect(elements.popoverContent).not.toBeVisible();
-		await user.click(elements.popoverTrigger);
-		expect(elements.popoverContent).toBeVisible();
-		expect(elements.tooltipContent).not.toBeVisible();
-		await user.hover(elements.tooltipTrigger);
-		expect(elements.tooltipContent).toBeVisible();
-		await user.click(elements.tooltipContent);
-		expect(elements.tooltipContent).toBeVisible();
-		expect(elements.popoverContent).toBeVisible();
+function setupPopoverTagsInput({ portalType }: { portalType: PortalConfig }) {
+	const user = userEvent.setup();
+	const returned = render(PopoverTagsInput, { portal: portalType });
+	const popoverTrigger = returned.getByTestId('popover-trigger');
+	const popoverContent = returned.getByTestId('popover-content');
+	const getTag = (i: number) => returned.getByTestId(`tag-${i}`);
+	const getEditTag = (i: number) => returned.getByTestId(`edit-tag-${i}`);
+	const outside = returned.getByTestId('outside');
+	const elements = { popoverTrigger, popoverContent, outside, getTag, getEditTag };
+	return { user, ...returned, elements };
+}
+
+function setupPopoverSelect({ portalType }: { portalType: PortalConfig }) {
+	const user = userEvent.setup();
+	const returned = render(PopoverSelect, { portal: portalType });
+	const popoverTrigger = returned.getByTestId('popover-trigger');
+	const popoverContent = returned.getByTestId('popover-content');
+	const selectTrigger = returned.getByTestId('select-trigger');
+	const selectMenu = returned.getByTestId('select-menu');
+	const outside = returned.getByTestId('outside');
+	const elements = { popoverTrigger, popoverContent, outside, selectTrigger, selectMenu };
+	return { user, ...returned, elements };
+}
+
+type PortalTestOption = { label: string; portalType: PortalConfig };
+
+const portalTestOptions = [
+	{ label: 'Sibling portals', portalType: 'body' },
+	{ label: 'Single portal', portalType: undefined },
+] satisfies PortalTestOption[];
+
+describe.each(portalTestOptions)('Portal Behaviors - $label', ({ portalType }) => {
+	describe('Popover & Tooltip', () => {
+		it('should not close the popover when the tooltip content is clicked', async () => {
+			const { elements, user } = setupPopoverTooltip({ portalType });
+
+			expect(elements.popoverContent).not.toBeVisible();
+			await user.click(elements.popoverTrigger);
+			expect(elements.popoverContent).toBeVisible();
+			expect(elements.tooltipContent).not.toBeVisible();
+			await user.hover(elements.tooltipTrigger);
+			expect(elements.tooltipContent).toBeVisible();
+			await user.click(elements.tooltipContent);
+			expect(elements.tooltipContent).toBeVisible();
+			expect(elements.popoverContent).toBeVisible();
+		});
+
+		it('should close the tooltip content when the popover content is clicked and tooltip is open', async () => {
+			const { elements, user } = setupPopoverTooltip({ portalType });
+
+			expect(elements.popoverContent).not.toBeVisible();
+			await user.click(elements.popoverTrigger);
+			expect(elements.popoverContent).toBeVisible();
+			expect(elements.tooltipContent).not.toBeVisible();
+			await user.hover(elements.tooltipTrigger);
+			expect(elements.tooltipContent).toBeVisible();
+			await user.click(elements.tooltipContent);
+			expect(elements.tooltipContent).toBeVisible();
+			expect(elements.popoverContent).toBeVisible();
+			await user.click(elements.popoverContent);
+			expect(elements.tooltipContent).not.toBeVisible();
+			expect(elements.popoverContent).toBeVisible();
+		});
+
+		it('should not close the popover when escape is pressed while tooltip is open', async () => {
+			const { elements, user } = setupPopoverTooltip({ portalType });
+
+			expect(elements.popoverContent).not.toBeVisible();
+			await user.click(elements.popoverTrigger);
+			expect(elements.popoverContent).toBeVisible();
+			expect(elements.tooltipContent).not.toBeVisible();
+			await user.hover(elements.tooltipTrigger);
+			expect(elements.tooltipContent).toBeVisible();
+
+			await user.keyboard(kbd.ESCAPE);
+			expect(elements.tooltipContent).not.toBeVisible();
+			expect(elements.popoverContent).toBeVisible();
+			expect(elements.outside).toBeVisible();
+			await user.click(elements.outside);
+			expect(elements.tooltipContent).not.toBeVisible();
+			await waitFor(() => expect(elements.popoverContent).not.toBeVisible());
+		});
+
+		it('should not close the popover nor the tooltip when escape is pressed while tooltip is open and tooltip `closeOnEscape` is false', async () => {
+			const { elements, user } = setupPopoverTooltip({
+				portalType,
+				tooltipEscapeBehavior: 'ignore',
+			});
+
+			expect(elements.popoverContent).not.toBeVisible();
+			await user.click(elements.popoverTrigger);
+			expect(elements.popoverContent).toBeVisible();
+			expect(elements.tooltipContent).not.toBeVisible();
+			await user.hover(elements.tooltipTrigger);
+			expect(elements.tooltipContent).toBeVisible();
+
+			await user.keyboard(kbd.ESCAPE);
+			expect(elements.tooltipContent).toBeVisible();
+			expect(elements.popoverContent).toBeVisible();
+		});
 	});
 
-	it('should close the tooltip content when the popover content is clicked and tooltip is open', async () => {
-		const { elements, user } = setupPopoverTooltip();
-
-		expect(elements.popoverContent).not.toBeVisible();
-		await user.click(elements.popoverTrigger);
-		expect(elements.popoverContent).toBeVisible();
-		expect(elements.tooltipContent).not.toBeVisible();
-		await user.hover(elements.tooltipTrigger);
-		expect(elements.tooltipContent).toBeVisible();
-		await user.click(elements.tooltipContent);
-		expect(elements.tooltipContent).toBeVisible();
-		expect(elements.popoverContent).toBeVisible();
-		await user.click(elements.popoverContent);
-		expect(elements.tooltipContent).not.toBeVisible();
-		expect(elements.popoverContent).toBeVisible();
+	describe('Popover & Tags Input', () => {
+		it('should not close popover on click outside while editing tag', async () => {
+			const { user, elements } = setupPopoverTagsInput({ portalType });
+
+			await user.click(elements.popoverTrigger);
+			expect(elements.popoverContent).toBeVisible();
+			await user.dblClick(elements.getTag(0));
+			expect(elements.getEditTag(0)).toHaveFocus();
+			await user.click(elements.outside);
+			expect(elements.getEditTag(0)).not.toHaveFocus();
+			expect(elements.popoverContent).toBeVisible();
+		});
+
+		it('should not close popover on escape while editing tag', async () => {
+			const { user, elements } = setupPopoverTagsInput({ portalType });
+
+			await user.click(elements.popoverTrigger);
+			expect(elements.popoverContent).toBeVisible();
+			await user.dblClick(elements.getTag(0));
+			expect(elements.getEditTag(0)).toHaveFocus();
+			await user.keyboard(kbd.ESCAPE);
+			expect(elements.getEditTag(0)).not.toHaveFocus();
+			expect(elements.popoverContent).toBeVisible();
+		});
 	});
 
-	it('should close both tooltip and popover content when clicked outside of popover content', async () => {
-		const { elements, user } = setupPopoverTooltip();
-
-		expect(elements.popoverContent).not.toBeVisible();
-		await user.click(elements.popoverTrigger);
-		expect(elements.popoverContent).toBeVisible();
-		expect(elements.tooltipContent).not.toBeVisible();
-		await user.hover(elements.tooltipTrigger);
-		expect(elements.tooltipContent).toBeVisible();
-		await user.click(elements.tooltipContent);
-		expect(elements.tooltipContent).toBeVisible();
-		expect(elements.popoverContent).toBeVisible();
-		expect(elements.outside).toBeVisible();
-		await user.click(elements.outside);
-		await waitFor(() => expect(elements.tooltipContent).not.toBeVisible());
-		await waitFor(() => expect(elements.popoverContent).not.toBeVisible());
+	describe('Popover & Select', () => {
+		it('should not deactivate popover focus trap on escape while select is open', async () => {
+			const { user, elements } = setupPopoverSelect({ portalType });
+
+			expect(elements.popoverContent).not.toBeVisible();
+			await user.click(elements.popoverTrigger);
+			expect(elements.popoverContent).toBeVisible();
+
+			expect(elements.selectMenu).not.toBeVisible();
+			await user.click(elements.selectTrigger);
+			expect(elements.selectMenu).toBeVisible();
+
+			await user.keyboard(kbd.ESCAPE);
+			expect(elements.selectMenu).not.toBeVisible();
+			expect(elements.popoverContent).toBeVisible();
+
+			await assertActiveFocusTrap(user, elements.popoverContent);
+		});
+
+		it('should not deactivate popover focus trap on outside click while select is open', async () => {
+			const { user, elements } = setupPopoverSelect({ portalType });
+
+			expect(elements.popoverContent).not.toBeVisible();
+			await user.click(elements.popoverTrigger);
+			expect(elements.popoverContent).toBeVisible();
+
+			expect(elements.selectMenu).not.toBeVisible();
+			await user.click(elements.selectTrigger);
+			expect(elements.selectMenu).toBeVisible();
+
+			await user.click(elements.outside);
+			await waitFor(() => expect(elements.selectMenu).not.toBeVisible());
+			expect(elements.popoverContent).toBeVisible();
+			await assertActiveFocusTrap(user, elements.popoverContent);
+		});
 	});
 });
diff --git a/src/tests/portal/PortalNested.spec.ts b/src/tests/portal/PortalNested.spec.ts
index cb94224e..c6cdb2f7 100644
--- a/src/tests/portal/PortalNested.spec.ts
+++ b/src/tests/portal/PortalNested.spec.ts
@@ -1,73 +1,94 @@
-import { render, screen } from '@testing-library/svelte';
+import { render, screen, waitFor } from '@testing-library/svelte';
 import { userEvent } from '@testing-library/user-event';
 import PortalNestedTest, { structure, type Structure } from './PortalNested.svelte';
 import { testKbd as kbd } from '../utils.js';
+import type { CreateDialogProps } from '$lib/index.js';
 
 type UserEventUser = ReturnType<typeof userEvent.setup>;
 
+const assertNotVisibleOrNull = async (element: HTMLElement | null) => {
+	if (element !== null) {
+		await waitFor(() => expect(element).not.toBeVisible());
+	} else {
+		await waitFor(() => expect(element).toBeNull());
+	}
+};
+
 // Recursive function to test the components
 const testComponent = async (component: Structure, level: number, user: UserEventUser) => {
 	// Get the elements
-	const trigger = screen.getByTestId(`${component.name}-trigger-${level}`);
-	const content = screen.getByTestId(`${component.name}-content-${level}`);
-	const outside = screen.getByTestId(`${component.name}-outside-${level}`);
+	const getTrigger = () => screen.getByTestId(`${component.name}-trigger-${level}`);
+	const getContent = () => screen.queryByTestId(`${component.name}-content-${level}`);
+	const getOutside = () => screen.getByTestId(`${component.name}-outside-${level}`);
 
 	// At the root, the root trigger and outside should be visible, and the content  must not be.
-	expect(trigger).toBeVisible();
-	expect(outside).toBeVisible();
-	expect(content).not.toBeVisible();
+	expect(getTrigger()).toBeVisible();
+	expect(getOutside()).toBeVisible();
+	await assertNotVisibleOrNull(getContent());
 
 	// Click the trigger
-	await user.click(trigger);
+	await user.click(getTrigger());
 
 	// After clicking the root trigger, all its elements must be visible.
-	expect(trigger).toBeVisible();
-	expect(content).toBeVisible();
-	expect(outside).toBeVisible();
+	expect(getTrigger()).toBeVisible();
+	expect(getContent()).toBeVisible();
+	expect(getOutside()).toBeVisible();
 
 	// And all its immediate children triggers must be visible
-	if (component.children) {
-		for (const child of component.children) {
-			const childTrigger = screen.getByTestId(`${child.name}-trigger-${level + 1}`);
-			expect(childTrigger).toBeVisible();
-
-			// The children of its children must all be invisible still
-			if (child.children) {
-				for (const grandChild of child.children) {
-					const grandChildTrigger = screen.getByTestId(`${grandChild.name}-trigger-${level + 2}`);
-					expect(grandChildTrigger).not.toBeVisible();
-				}
-			}
-
-			// Recursively test the child components
-			await testComponent(child, level + 1, user);
+	for (const child of component.children ?? []) {
+		const childTrigger = screen.getByTestId(`${child.name}-trigger-${level + 1}`);
+		expect(childTrigger).toBeVisible();
+
+		// The children of its children must all be invisible still
+		for (const grandChild of child.children ?? []) {
+			const grandChildTrigger = screen.queryByTestId(`${grandChild.name}-trigger-${level + 2}`);
+			await assertNotVisibleOrNull(grandChildTrigger);
 		}
+
+		await testComponent(child, level + 1, user);
 	}
 
+	expect(getContent()).toBeVisible();
+
 	// Testing closing
 	// By clicking escape
 	await user.keyboard(kbd.ESCAPE);
-	expect(trigger).toBeVisible();
-	expect(content).not.toBeVisible();
-	expect(outside).toBeVisible();
+	expect(getTrigger()).toBeVisible();
+	await assertNotVisibleOrNull(getContent());
+	expect(getOutside()).toBeVisible();
 
 	// Reopen
-	// await userEvent.click(trigger);
-	// expect(trigger).toBeVisible();
-	// expect(content).toBeVisible();
-	// expect(outside).toBeVisible();
+	await userEvent.click(getTrigger());
+	expect(getTrigger()).toBeVisible();
+	expect(getContent()).toBeVisible();
+	expect(getOutside()).toBeVisible();
 
 	// By clicking outside
-	// await userEvent.click(outside);
-	// await sleep(500);
-	// expect(trigger).toBeVisible();
-	// expect(content).not.toBeVisible();
-	// expect(outside).toBeVisible();
+	await userEvent.click(getOutside());
+	expect(getTrigger()).toBeVisible();
+	await assertNotVisibleOrNull(getContent());
+	expect(getOutside()).toBeVisible();
+};
+
+type PortalTestOption = {
+	label: string;
+	portalType: CreateDialogProps['portal'];
+	forceVisible: CreateDialogProps['forceVisible'];
 };
 
+const portalTestOptions = [
+	{ label: 'Sibling portals & forceVisible true', portalType: 'body', forceVisible: true },
+	{ label: 'Sibling portals & forceVisible false', portalType: 'body', forceVisible: false },
+	{ label: 'Single portal & forceVisible true', portalType: undefined, forceVisible: true },
+	{ label: 'Single portal & forceVisible false', portalType: undefined, forceVisible: false },
+] satisfies PortalTestOption[];
+
 // Execute the test
-test('recursive component test', async () => {
-	const user = userEvent.setup();
-	render(PortalNestedTest);
-	await testComponent(structure, 0, user);
-});
+test.each(portalTestOptions)(
+	'recursive component test - $label forceVisible $forceVisible',
+	async ({ portalType, forceVisible }) => {
+		const user = userEvent.setup();
+		render(PortalNestedTest, { portal: portalType, forceVisible });
+		await testComponent(structure, 0, user);
+	}
+);
diff --git a/src/tests/portal/PortalNested.svelte b/src/tests/portal/PortalNested.svelte
index fdda1a15..400b61e8 100644
--- a/src/tests/portal/PortalNested.svelte
+++ b/src/tests/portal/PortalNested.svelte
@@ -2,11 +2,13 @@
 	import Dialog from './Dialog.svelte';
 	import Popover from './Popover.svelte';
 	import Select from './Select.svelte';
+	import DropdownMenu from './DropdownMenu.svelte';
 
 	const components = {
 		dialog: Dialog,
 		popover: Popover,
 		select: Select,
+		'dropdown-menu': DropdownMenu,
 	} as const;
 
 	export type Structure = {
@@ -16,14 +18,15 @@
 	export const structure: Structure = {
 		name: 'dialog',
 		children: [
-			// {
-			// 	name: 'dialog',
-			// 	children: [
-			// 		{
-			// 			name: 'select',
-			// 		},
-			// 	],
-			// },
+			{
+				name: 'dialog',
+				children: [
+					{
+						name: 'dropdown-menu',
+						children: [{ name: 'select' }],
+					},
+				],
+			},
 			{
 				name: 'popover',
 				children: [{ name: 'dialog' }, { name: 'popover' }],
@@ -34,6 +37,10 @@
 </script>
 
 <script lang="ts">
+	import type { CreateDialogProps } from '$lib/index.js';
+
+	export let portal: CreateDialogProps['portal'];
+	export let forceVisible: CreateDialogProps['forceVisible'];
 	export let cmp: Structure = structure;
 
 	$: resolvedCmp = components[cmp.name];
@@ -42,16 +49,16 @@
 
 {#if isRoot}
 	<main>
-		<svelte:component this={resolvedCmp}>
+		<svelte:component this={resolvedCmp} {portal} {forceVisible}>
 			{#each cmp.children ?? [] as child}
-				<svelte:self cmp={child} isRoot={false} />
+				<svelte:self cmp={child} isRoot={false} {portal} {forceVisible} />
 			{/each}
 		</svelte:component>
 	</main>
 {:else}
-	<svelte:component this={resolvedCmp}>
+	<svelte:component this={resolvedCmp} {portal} {forceVisible}>
 		{#each cmp.children ?? [] as child}
-			<svelte:self cmp={child} isRoot={false} />
+			<svelte:self cmp={child} isRoot={false} {portal} {forceVisible} />
 		{/each}
 	</svelte:component>
 {/if}
diff --git a/src/tests/portal/Select.svelte b/src/tests/portal/Select.svelte
index bc38f95b..844e72ee 100644
--- a/src/tests/portal/Select.svelte
+++ b/src/tests/portal/Select.svelte
@@ -1,11 +1,14 @@
 <script lang="ts">
-	import { createSelect, melt } from '$lib/index.js';
+	import { createSelect, melt, type CreateSelectProps } from '$lib/index.js';
 	import { initLevel } from './level.js';
 
+	export let portal: CreateSelectProps['portal'];
+	export let forceVisible: CreateSelectProps['forceVisible'];
+
 	const {
 		elements: { trigger, menu, option, group, groupLabel, label },
-		states: { selectedLabel },
-	} = createSelect();
+		states: { selectedLabel, open },
+	} = createSelect({ portal, forceVisible });
 
 	const options = {
 		sweet: ['Caramel', 'Chocolate', 'Strawberry', 'Cookies & Cream'],
@@ -21,20 +24,21 @@
 	{$selectedLabel || 'Select a flavor'}
 </button>
 
-<div use:melt={$menu} data-testid="select-content-{level}">
-	<slot />
-	{#each Object.entries(options) as [key, arr]}
-		<div use:melt={$group(key)}>
-			<div use:melt={$groupLabel(key)}>
-				{key}
-			</div>
-			{#each arr as item}
-				<div use:melt={$option({ value: item, label: item })}>
-					{item}
+{#if $open || !forceVisible}
+	<div use:melt={$menu} data-testid="select-content-{level}">
+		<slot />
+		{#each Object.entries(options) as [key, arr]}
+			<div use:melt={$group(key)}>
+				<div use:melt={$groupLabel(key)}>
+					{key}
 				</div>
-			{/each}
-		</div>
-	{/each}
-</div>
-
+				{#each arr as item}
+					<div use:melt={$option({ value: item, label: item })}>
+						{item}
+					</div>
+				{/each}
+			</div>
+		{/each}
+	</div>
+{/if}
 <div data-testid="select-outside-{level}" />
diff --git a/src/tests/select/Select.spec.ts b/src/tests/select/Select.spec.ts
index 9a7d6d3b..720a5404 100644
--- a/src/tests/select/Select.spec.ts
+++ b/src/tests/select/Select.spec.ts
@@ -170,9 +170,9 @@ describe('Select', () => {
 		await waitFor(() => expect(trigger).toHaveTextContent('Vanilla'), { timeout: 500 });
 	});
 
-	test('Respects the `closeOnEscape` prop', async () => {
+	test('Respects the `escapeBehavior` prop', async () => {
 		const user = userEvent.setup();
-		const { getByTestId } = render(SelectTest, { closeOnEscape: false });
+		const { getByTestId } = render(SelectTest, { escapeBehavior: 'ignore' });
 		const trigger = getByTestId('trigger');
 		const menu = getByTestId('menu');
 
@@ -238,7 +238,7 @@ describe('Select', () => {
 	});
 
 	test('Applies type="button" to the select trigger', async () => {
-		const { getByTestId } = render(SelectTest, { closeOnEscape: false });
+		const { getByTestId } = render(SelectTest, { escapeBehavior: 'ignore' });
 		const trigger = getByTestId('trigger');
 		expect(trigger).toHaveAttribute('type', 'button');
 	});
diff --git a/src/tests/select/SelectTest.svelte b/src/tests/select/SelectTest.svelte
index 0059c569..8fafb1ae 100644
--- a/src/tests/select/SelectTest.svelte
+++ b/src/tests/select/SelectTest.svelte
@@ -6,7 +6,7 @@
 
 	export let multiple = false;
 	export let defaultValue: string | undefined = undefined;
-	export let closeOnEscape = true;
+	export let escapeBehavior: CreateSelectProps['escapeBehavior'] = 'close';
 	export let closeOnOutsideClick = true;
 	export let ids: CreateSelectProps['ids'] = undefined;
 	export let onOutsideClick: CreateSelectProps['onOutsideClick'] = undefined;
@@ -24,7 +24,7 @@
 						label: defaultValue,
 				  }
 				: undefined,
-			closeOnEscape,
+			escapeBehavior,
 			closeOnOutsideClick,
 			ids,
 			onOutsideClick,