Repository: melt-ui/melt-ui
Pull request #1015 (2024-02-26 09:29:11)

fix: type narrowing for builder props
added `as const` to every object returned from...
```typescript
makeElement(name('group'), {
	returned: () => {
		return {
			...
		} as const; // <------
	},
});
```

### Before
![Untitled 2](https://github.com/melt-ui/melt-ui/assets/53095479/465dcf67-0bb7-4163-92b2-9ee3b5c50ff5)

### After
![Untitled 1](https://github.com/melt-ui/melt-ui/assets/53095479/7967f954-dd09-4444-aa7c-d44be227163a)


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

Latest commit: 981849404611630157847d8fa49bbd1670388114

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/anatolzak/melt-ui/new/fix/type-narrowing-builder-props?filename=.changeset/dry-badgers-check.md&value=---%0A%22%40melt-ui%2Fsvelte%22%3A%20patch%0A---%0A%0Afix%3A%20type%20narrowing%20for%20builder%20props%0A)


TGlide: I wonder if we can be smarter with this. Using something like this: https://xebia.com/blog/typescript-5-0-and-the-new-const-modifier-on-type-parameters/
anatolzak: I tried using the const modifier and was almost able to do it but it seems like TS has a weird bug that breaks its behavior when used in more complex situations.

I had to create a way smaller and simpler version of the makeElement function to test the const modifier without too many flowing variables to first prove a way to do it before trying to apply it to a much more complex scenario. But again, TS seems to have a weird issue that prevents me right now from moving forward with it.

[TS Playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAysD2AnCBnKBeKBXAdgax3gHccBtAXQG4Aoa0SKAUQBsIBbCHYAYQENnmAI14BjPAB4YUCAA9gnACZo4SVABooABWlzFaeIIBWEEcAB8GKAAoAbvyyoAXLACUGC9oA+1qwDp-vIgA5ijOvDggFG7oHi40dODQALK8eBAs7JzAAILBKJI68jhKsAjIKBraskUlBsamGgBKhXpMrBxcfALCYpKVZhaYAN7UUGNQyMBYiDgQCs6NNONQKGVOsDQAvrQi8DirUGyp6e1ZluKj47v7wLAtxcprFVAA9C8rT5dj1wdVug9QRomJAKcSrRAASxwQQ0uAIxBwZg0bygYEQ8DAKC+UB+t2a1VaGQ6PH4QlEEhg-Ve70m01mCmoZisgRCzhSaSJWVyIT6WiaZhcCwgUxmABVEuJGoMLCNlrioEMJsK6XMNKtVGhNpYWSgluNaTMlSL6VZ1eU3Lw0EDjeLIJKzFt4i8AFTUeUKXjAXgARmcsv1EF4Cj2zBAUAAFhABPBnAAiIhIZgKWNbajOl5uvYHD1e72WI4c05cKz+sZm9akb0aADk3urGmAiAc5DU2IN9OcVmiMuxy3bCt7y3GkejzmrCcQSfrg7Gmz1s9bmzitBdmZuUBzvAATH7sSPmDGPpDoan02vs57t-njpzi6WPhrnJWa3WG02IC228qZnNO+WUN2A5DgGxpAcBQ77oe46Jgo07gVAc7Ypsi7LtQq7upeADMu7LJBzjglCQSnhmGFeph16Fpkd7Yv+T5VlAtb1lAjbNq2fbfh21i0VAz5QLG3qxm+zaAfe7GgaJ4F4QxE5Tmx4GIcsyHUEulBAA)

The idea behind what we are trying to accomplish is to get the props returned from `makeElement` to be interpreted as const. We get the props from the `returned` method, which takes in a parameter--the store values that are passed to the makeElement function. 

I was able to get the props to be interpreted as const but only if I didn't declare the stores parameter in the `returned` method.

![image](https://github.com/melt-ui/melt-ui/assets/53095479/a94bc8f7-0f92-49c8-bf6e-d0a460d6b7ef)

However, for some reason, when I declare the stores parameter in the `returned` method, TS doesn't  return the object as const anymore.

![image](https://github.com/melt-ui/melt-ui/assets/53095479/319b19ac-cafd-40f7-be75-df21de77725f)

The weird thing is that TS knows the type of the stores parameter from the generics, but if you explicitly add the type for the stores parameter, the returned props are then interpreted as const. But the whole point was to not have to write "unnecessary" code.

![image](https://github.com/melt-ui/melt-ui/assets/53095479/c8453401-6b95-4efe-b5c4-34127b725a8c)

![image](https://github.com/melt-ui/melt-ui/assets/53095479/d3cb82a3-5dc6-41c0-8211-362a93f6a6a8)


anatolzak: @TGlide Unless I am missing something with the const modifier, it seems like it's a little broken and so maybe we should just use `as const` for now, as it is actually already used in many builders, and then if someone can figure out a better way to do this, they can create a PR

Patch:
diff --git a/.changeset/curly-olives-trade.md b/.changeset/curly-olives-trade.md
deleted file mode 100644
index 47f7b425..00000000
--- a/.changeset/curly-olives-trade.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@melt-ui/svelte': patch
----
-
-Range Calendar/Picker: fixed bug preventing a range with the same start and end date (Closes #898)
diff --git a/.changeset/khaki-files-try.md b/.changeset/khaki-files-try.md
new file mode 100644
index 00000000..39b1cd66
--- /dev/null
+++ b/.changeset/khaki-files-try.md
@@ -0,0 +1,5 @@
+---
+'@melt-ui/svelte': patch
+---
+
+Enhanced type accuracy by implementing as const for builder props, ensuring more precise type inference.
diff --git a/.changeset/mighty-weeks-think.md b/.changeset/mighty-weeks-think.md
deleted file mode 100644
index 5c677c90..00000000
--- a/.changeset/mighty-weeks-think.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@melt-ui/svelte': patch
----
-
-Tooltip: fixed bug where tooltip content style gets reset when forceVisible is true and $open gets updated #1010
diff --git a/.changeset/shaggy-ligers-attack.md b/.changeset/shaggy-ligers-attack.md
deleted file mode 100644
index 62053b49..00000000
--- a/.changeset/shaggy-ligers-attack.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@melt-ui/svelte": patch
----
-
-Calendars: ensure dates use the same calendar as the reference value
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 8a67c523..67262874 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,6 +1,6 @@
 # These are supported funding model platforms
 
-github: [tglide]
+github: [tglide, huntabyte]
 patreon: # Replace with a single Patreon username
 open_collective: # Replace with a single Open Collective username
 ko_fi: thomasglopes
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 8fc78646..a0488854 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -14,7 +14,7 @@ concurrency:
 
 permissions:
   contents: read # to fetch code (actions/checkout)
-  
+
 jobs:
   Lint:
     runs-on: ubuntu-latest
@@ -35,7 +35,6 @@ jobs:
           version: 8.6.3
           run_install: true
       - run: GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} && pnpm run check
-      
 
   Tests:
     runs-on: ubuntu-latest
@@ -46,4 +45,16 @@ jobs:
           version: 8.6.3
           run_install: true
       - run: GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} && pnpm run test
-      
\ No newline at end of file
+
+  Playwright-Tests:
+    name: Playwright Tests
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: pnpm/action-setup@v2
+        with:
+          version: 8.6.3
+          run_install: true
+      - name: Install Playwright Browsers
+        run: pnpm exec playwright install --with-deps
+      - run: GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} && pnpm run pw
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 300483e0..dfa35dc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,85 @@
 # @melt-ui/svelte
 
+## 0.76.2
+
+### Patch Changes
+
+- Fixed a bug on touch devices where an outside interaction leaked click events to other dom elements (closes #1115) ([#1118](https://github.com/melt-ui/melt-ui/pull/1118))
+
+## 0.76.1
+
+### Patch Changes
+
+- Fixed bug where focus trap deactivates in dialog and popover when clicking outside provided `closeOnOutsideClick: false` (closes #1084) ([#1094](https://github.com/melt-ui/melt-ui/pull/1094))
+
+- Combobox: menu no longer closes when composing (Closes: #1106) ([#1107](https://github.com/melt-ui/melt-ui/pull/1107))
+
+- Fixed bug where focus trap deactivates in dialog and popover when pressing escape provided `closeOnEscape: false` (closes #1091) ([#1095](https://github.com/melt-ui/melt-ui/pull/1095))
+
+- Fixed memory leak caused by race conditions for various components (closes #1086) ([#1087](https://github.com/melt-ui/melt-ui/pull/1087))
+
+- performance: avoid effect multiple runs ([#1089](https://github.com/melt-ui/melt-ui/pull/1089))
+
+- Listbox: correct `arrowSize` JSDoc defaultValue ([#1050](https://github.com/melt-ui/melt-ui/pull/1050))
+
+- Popover: fixed a bug that has the potential to introduce a memory leak via the overlay ([#1112](https://github.com/melt-ui/melt-ui/pull/1112))
+
+## 0.76.0
+
+### Minor Changes
+
+- fix: datepicker not syncing with calendar + add defaults from calendar to datepicker ([#1076](https://github.com/melt-ui/melt-ui/pull/1076))
+
+### Patch Changes
+
+- Fix incorrect arrow position for Tooltip, Popover, Listbox, Link Preview provided forceVisible: false (closes #1048) ([#1049](https://github.com/melt-ui/melt-ui/pull/1049))
+
+- fix: months not updating when changing options ([#1070](https://github.com/melt-ui/melt-ui/pull/1070))
+
+- Floating: `data-side` & `data-align` for floating content ([#1082](https://github.com/melt-ui/melt-ui/pull/1082))
+
+- fix: months not updating when changing options (range calendar) ([#1072](https://github.com/melt-ui/melt-ui/pull/1072))
+
+## 0.75.3
+
+### Patch Changes
+
+- fix(Tooltip, Link Preview, Menu, Popover, Listbox) fixed bug where content jumps to top left of page during external unmounting when using out transition on the content and else if block to render the content (closes #1058, #1039) ([#1059](https://github.com/melt-ui/melt-ui/pull/1059))
+
+- Portalled Elements: fixed a bug where clicking inside a nested 'portalled' element would cause the parent to close ([#1066](https://github.com/melt-ui/melt-ui/pull/1066))
+
+## 0.75.2
+
+### Patch Changes
+
+- Scroll Area: fixed bug with `type: 'hover'` scrollbars always showing on hover even if content not overflowing ([#1046](https://github.com/melt-ui/melt-ui/pull/1046))
+
+- Scroll Area: fixed bug with `scrollbarX` styles ([#1046](https://github.com/melt-ui/melt-ui/pull/1046))
+
+## 0.75.1
+
+### Patch Changes
+
+- Scroll Area: export types ([#1044](https://github.com/melt-ui/melt-ui/pull/1044))
+
+## 0.75.0
+
+### Minor Changes
+
+- Popover: add optional `overlay` element builder for simplified modal behavior (part of #1018) ([#1025](https://github.com/melt-ui/melt-ui/pull/1025))
+
+- Menus: add `overlay` element ([#1035](https://github.com/melt-ui/melt-ui/pull/1035))
+
+- New Builder: Scroll Area ([#908](https://github.com/melt-ui/melt-ui/pull/908))
+
+### Patch Changes
+
+- Range Calendar/Picker: fixed bug preventing a range with the same start and end date (Closes #898) ([#1037](https://github.com/melt-ui/melt-ui/pull/1037))
+
+- Tooltip: fixed bug where tooltip content style gets reset when forceVisible is true and $open gets updated #1010 ([#1011](https://github.com/melt-ui/melt-ui/pull/1011))
+
+- Calendars: ensure dates use the same calendar as the reference value ([#1026](https://github.com/melt-ui/melt-ui/pull/1026))
+
 ## 0.74.4
 
 ### Patch Changes
diff --git a/package.json b/package.json
index 90962254..e79589bc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@melt-ui/svelte",
-	"version": "0.74.4",
+	"version": "0.76.2",
 	"license": "MIT",
 	"repository": "github:melt-ui/melt-ui",
 	"author": "Thomas G. Lopes",
diff --git a/playwright.config.ts b/playwright.config.ts
index 07994026..51ef844b 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -35,17 +35,17 @@ export default defineConfig({
 	projects: [
 		{
 			name: 'chromium',
-			use: { ...devices['Desktop Chrome'] },
+			use: { ...devices['Desktop Chrome'], hasTouch: true },
 		},
 
 		{
 			name: 'firefox',
-			use: { ...devices['Desktop Firefox'] },
+			use: { ...devices['Desktop Firefox'], hasTouch: true },
 		},
 
 		{
 			name: 'webkit',
-			use: { ...devices['Desktop Safari'] },
+			use: { ...devices['Desktop Safari'], hasTouch: true },
 		},
 
 		/* Test against mobile viewports. */
diff --git a/src/docs/components/nav/mobile-nav.svelte b/src/docs/components/nav/mobile-nav.svelte
index 8581a9e5..9698413a 100644
--- a/src/docs/components/nav/mobile-nav.svelte
+++ b/src/docs/components/nav/mobile-nav.svelte
@@ -25,8 +25,8 @@
 	<Menu class="size-6" />
 	<span class="sr-only">Toggle Menu</span>
 </button>
-<div use:melt={$portalled} class="md:hidden">
-	{#if $open}
+{#if $open}
+	<div use:melt={$portalled} class="md:hidden">
 		<div
 			use:melt={$overlay}
 			class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm"
@@ -86,8 +86,8 @@
 				</div>
 			</div>
 		</div>
-	{/if}
-</div>
+	</div>
+{/if}
 
 <style lang="postcss">
 	.menu,
diff --git a/src/docs/components/tooltip.svelte b/src/docs/components/tooltip.svelte
index e381cca6..29fc37d8 100644
--- a/src/docs/components/tooltip.svelte
+++ b/src/docs/components/tooltip.svelte
@@ -1,6 +1,6 @@
 <script lang="ts">
-	import { browser } from '$app/environment';
 	import { createTooltip, melt } from '$lib/index.js';
+	import { fade } from 'svelte/transition';
 
 	const {
 		elements: { trigger, content, arrow },
@@ -20,30 +20,20 @@
 	<slot />
 </div>
 
-<div
-	use:melt={$content}
-	class="z-50 rounded-md bg-neutral-700 px-2 py-1 text-sm text-neutral-50 shadow-sm"
-	data-open={$open ? '' : undefined}
-	class:hidden={!browser}
->
-	<div use:melt={$arrow} />
-	{text}
-</div>
+{#if $open}
+	<div
+		use:melt={$content}
+		in:fade={{ duration: 150 }}
+		class="z-50 rounded-md bg-neutral-700 px-2 py-1 text-sm text-neutral-50 shadow-sm"
+	>
+		<div use:melt={$arrow} />
+		{text}
+	</div>
+{/if}
 
 <style lang="postcss">
 	[data-melt-tooltip-trigger] {
 		display: grid;
 		place-items: center;
 	}
-
-	[data-melt-tooltip-content] {
-		opacity: 0;
-		visibility: hidden;
-		transition: opacity 150ms ease;
-
-		&[data-open] {
-			opacity: 1;
-			visibility: visible;
-		}
-	}
 </style>
diff --git a/src/docs/content/builders/context-menu.md b/src/docs/content/builders/context-menu.md
index 8094e58b..98151cea 100644
--- a/src/docs/content/builders/context-menu.md
+++ b/src/docs/content/builders/context-menu.md
@@ -8,6 +8,8 @@ description:
     import { APIReference, KbdTable, Preview } from '$docs/components'
     export let schemas
     export let keyboard
+	export let previews
+	export let snippets
 </script>
 
 ## Anatomy
@@ -24,6 +26,8 @@ description:
 
 ## Usage
 
+### Basic
+
 The first thing you need to do is create a context menu using the `createContextMenu` function.
 
 ```svelte {3-5}
@@ -84,6 +88,40 @@ occurring.
 </div>
 ```
 
+### Modal Behavior
+
+To give the context menu complete modal behavior, where an overlay is rendered behind the menu to
+prevent interaction with the rest of the page while open, use the `overlay` builder element.
+
+```svelte
+<script lang="ts">
+	import { createContextMenu, melt } from '@melt-ui/svelte'
+	const {
+		elements: { trigger, menu, item, separator, arrow, overlay }
+	} = createContextMenu()
+</script>
+
+<div use:melt={$trigger}>Right click to open</div>
+<div use:melt={$overlay} />
+<div use:melt={$menu}>
+	<div use:melt={$arrow} />
+	<div use:melt={$item} />
+</div>
+```
+
+The [Modal Context Menu](#modal-context-menu) example below demonstrates this behavior in action.
+
+## Example Components
+
+### Modal Context Menu
+
+An example using the `overlay` builder element to prevent interaction with the rest of the page
+while the menu is open.
+
+<Preview code={snippets.modal}>
+    <svelte:component this={previews.modal} />
+</Preview>
+
 ## API Reference
 
 <APIReference {schemas} />
diff --git a/src/docs/content/builders/dialog.md b/src/docs/content/builders/dialog.md
index 4e406287..a1d5e168 100644
--- a/src/docs/content/builders/dialog.md
+++ b/src/docs/content/builders/dialog.md
@@ -28,16 +28,16 @@ At a high level, the anatomy of a dialog looks like this:
 
 <button use:melt={$trigger}> Open Dialog </button>
 
-<div use:melt={$portalled}>
-	{#if $open}
+{#if $open}
+	<div use:melt={$portalled}>
 		<div use:melt={$overlay} />
 		<div use:melt={$content}>
 			<h2 use:melt={$title}>Dialog Title</h2>
 			<p use:melt={$description}>Dialog description</p>
 			<button use:melt={$close}> Close Dialog </button>
 		</div>
-	{/if}
-</div>
+	</div>
+{/if}
 ```
 
 - **Trigger**: The button(s) that open the dialog
diff --git a/src/docs/content/builders/dropdown-menu.md b/src/docs/content/builders/dropdown-menu.md
index 56a9b899..c7dd2db9 100644
--- a/src/docs/content/builders/dropdown-menu.md
+++ b/src/docs/content/builders/dropdown-menu.md
@@ -5,9 +5,11 @@ description:
 ---
 
 <script>
-    import { KbdTable, APIReference } from '$docs/components'
+    import { KbdTable, APIReference, Preview } from '$docs/components'
     export let schemas
     export let keyboard
+	export let snippets
+	export let previews
 </script>
 
 ## Anatomy
@@ -24,6 +26,8 @@ description:
 
 ## Usage
 
+### Basic
+
 The first thing you need to do is create a dropdown menu using the `createDropdownMenu` function.
 
 ```svelte {3-5}
@@ -84,6 +88,40 @@ occurring.
 </div>
 ```
 
+### Modal Behavior
+
+To give the dropdown menu complete modal behavior, where an overlay is rendered behind the menu to
+prevent interaction with the rest of the page while open, use the `overlay` builder element.
+
+```svelte
+<script lang="ts">
+	import { createDropdownMenu, melt } from '@melt-ui/svelte'
+	const {
+		elements: { trigger, menu, item, separator, arrow, overlay }
+	} = createDropdownMenu()
+</script>
+
+<button type="button" use:melt={$trigger}> Open </button>
+<div use:melt={$overlay} />
+<div use:melt={$menu}>
+	<div use:melt={$arrow} />
+	<div use:melt={$item} />
+</div>
+```
+
+The [Modal Dropdown](#modal-dropdown) example below demonstrates this behavior in action.
+
+## Example Components
+
+### Modal Dropdown
+
+An example using the `overlay` builder element to prevent interaction with the rest of the page
+while the menu is open.
+
+<Preview code={snippets.modal}>
+    <svelte:component this={previews.modal} />
+</Preview>
+
 ## API Reference
 
 <APIReference {schemas} />
diff --git a/src/docs/content/builders/popover.md b/src/docs/content/builders/popover.md
index c41079c1..3ca8cc66 100644
--- a/src/docs/content/builders/popover.md
+++ b/src/docs/content/builders/popover.md
@@ -28,7 +28,9 @@ above to create your popover.
 To specify that the popover should be open by default, set the `defaultOpen` prop to `true`.
 
 ```ts {2}
-const { trigger, content, open, arrow, close } = createPopover({
+const {
+	/** ... */
+} = createPopover({
 	defaultOpen: true
 })
 ```
@@ -67,6 +69,36 @@ const popover = createPopover({
 })
 ```
 
+### Modal Behavior
+
+To give the popover modal behavior, where body scrolling is disabled and an overlay is rendered
+behind the popover to prevent interaction with the rest of the page, use the `preventScroll` prop
+and `overlay` builder element.
+
+```svelte
+<script lang="ts">
+	import { createPopover, melt } from '@melt-ui/svelte'
+
+	const {
+		elements: { content, trigger, overlay, close, arrow }
+	} = createPopover({
+		preventScroll: true
+	})
+</script>
+
+<button type="button" use:melt={$trigger}> Open </button>
+<div use:melt={$overlay} />
+<div use:melt={$content}>
+	<div use:melt={$arrow} />
+	<div>
+		<!-- ... -->
+	</div>
+	<button use:melt={$close}> Close </button>
+</div>
+```
+
+The [Modal Popover](#modal-popover) example below demonstrates this behavior in action.
+
 ## Example Components
 
 ### Nested Popovers
@@ -75,6 +107,12 @@ const popover = createPopover({
     <svelte:component this={previews.nested} />
 </Preview>
 
+### Modal Popover
+
+<Preview code={snippets.modal}>
+	<svelte:component this={previews.modal} />
+</Preview>
+
 ## API Reference
 
 <APIReference {schemas} />
diff --git a/src/docs/content/builders/scroll-area.md b/src/docs/content/builders/scroll-area.md
new file mode 100644
index 00000000..48b2d9ef
--- /dev/null
+++ b/src/docs/content/builders/scroll-area.md
@@ -0,0 +1,62 @@
+---
+title: Scroll Area
+description: Provides consistent scrollbars across platforms.
+---
+
+<script>
+	import { APIReference, Preview, Callout } from '$docs/components'
+	import { A } from '$docs/components/markdown';
+	export let snippets
+	export let previews
+	export let schemas
+</script>
+
+## Anatomy
+
+- **Root**: The container that wraps all parts of the scroll area
+- **Viewport**: A container that wraps the scrollable content
+- **Content**: The scrollable content
+- **Corner**: The corner element displayed when both scrollbars are visible
+- **ScrollbarX**: The track of the X scrollbar
+- **ThumbX**: The thumb of the X scrollbar
+- **ScrollbarY**: The track of the Y scrollbar
+- **ThumbY**: The thumb of the Y scrollbar
+
+## Examples
+
+### Always
+
+When the `type` is set to `always`, the scrollbars will always be visible.
+
+<Preview code={snippets.always} variant="dark" size="auto">
+	<svelte:component this={previews.always} />
+</Preview>
+
+### Auto
+
+When the `type` is set to `auto`, the scrollbars will only be visible when the content overflows.
+
+<Preview code={snippets.auto} variant="dark" size="auto">
+	<svelte:component this={previews.auto} />
+</Preview>
+
+### Hover
+
+When the `type` is set to `hover`, the scrollbars will only be visible when the mouse is over the
+scroll area.
+
+<Preview code={snippets.hover} variant="dark" size="auto">
+	<svelte:component this={previews.hover} />
+</Preview>
+
+### Scroll
+
+When the `type` is set to `scroll`, the scrollbars will only be visible when the user is scrolling.
+
+<Preview code={snippets.scroll} variant="dark" size="auto">
+	<svelte:component this={previews.scroll} />
+</Preview>
+
+## API Reference
+
+<APIReference {schemas} />
diff --git a/src/docs/data/builders/combobox.ts b/src/docs/data/builders/combobox.ts
index 443b9fff..71c3b728 100644
--- a/src/docs/data/builders/combobox.ts
+++ b/src/docs/data/builders/combobox.ts
@@ -1,6 +1,6 @@
 import { ATTRS, KBD, PROPS, SEE } from '$docs/constants.js';
 import type { KeyboardSchema } from '$docs/types.js';
-import { builderSchema, elementSchema } from '$docs/utils/index.js';
+import { builderSchema, elementSchema, floatingSideAndAlignDataAttrs } from '$docs/utils/index.js';
 import { comboboxEvents } from '$lib/builders/combobox/events.js';
 import { listboxIdParts } from '$lib/builders/listbox/create.js';
 import type { BuilderData } from './index.js';
@@ -153,6 +153,7 @@ const builder = builderSchema(BUILDER_NAME, {
 const menu = elementSchema('menu', {
 	description: 'The combobox menu element',
 	dataAttributes: [
+		...floatingSideAndAlignDataAttrs,
 		{
 			name: 'data-melt-combobox-menu',
 			value: ATTRS.MELT('menu'),
diff --git a/src/docs/data/builders/context-menu.ts b/src/docs/data/builders/context-menu.ts
index 0fe6b511..b75cc8e4 100644
--- a/src/docs/data/builders/context-menu.ts
+++ b/src/docs/data/builders/context-menu.ts
@@ -38,6 +38,7 @@ const {
 	subTrigger,
 	group,
 	groupLabel,
+	overlay,
 } = getMenuSchemas(BUILDER_NAME);
 
 const TRIGGER_NAME = 'trigger' as const;
@@ -54,6 +55,7 @@ const schemas = [
 	trigger,
 	menu,
 	item,
+	overlay,
 	separator,
 	arrow,
 	checkboxItemBuilder,
diff --git a/src/docs/data/builders/dropdown-menu.ts b/src/docs/data/builders/dropdown-menu.ts
index 9d4129c0..e2c2a8a3 100644
--- a/src/docs/data/builders/dropdown-menu.ts
+++ b/src/docs/data/builders/dropdown-menu.ts
@@ -28,6 +28,7 @@ const {
 	checkboxItemBuilder,
 	group,
 	groupLabel,
+	overlay,
 } = getMenuSchemas(BUILDER_NAME);
 
 const { elements, builders, states, options } = getMenuBuilderReturns(BUILDER_NAME);
@@ -56,6 +57,7 @@ const schemas = [
 	trigger,
 	menu,
 	item,
+	overlay,
 	separator,
 	arrow,
 	checkboxItemBuilder,
diff --git a/src/docs/data/builders/index.ts b/src/docs/data/builders/index.ts
index 5dbd076d..58155003 100644
--- a/src/docs/data/builders/index.ts
+++ b/src/docs/data/builders/index.ts
@@ -34,6 +34,7 @@ import { tableOfContentsData } from './table-of-contents.js';
 import { toolbarData } from './toolbar.js';
 import { tooltipData } from './tooltip.js';
 import { datePickerData } from './date-picker.js';
+import { scrollAreaData } from './scroll-area.js';
 
 export type BuilderData = {
 	schemas?: APISchema[];
@@ -65,6 +66,7 @@ export const builderMap = {
 	progress: progressData,
 	'radio-group': radioGroupData,
 	'range-calendar': rangeCalendarData,
+	'scroll-area': scrollAreaData,
 	select: selectData,
 	separator: separatorData,
 	slider: sliderData,
diff --git a/src/docs/data/builders/link-preview.ts b/src/docs/data/builders/link-preview.ts
index e37da8ab..9fa6f412 100644
--- a/src/docs/data/builders/link-preview.ts
+++ b/src/docs/data/builders/link-preview.ts
@@ -1,5 +1,5 @@
 import { ATTRS, PROPS } from '$docs/constants.js';
-import { builderSchema, elementSchema } from '$docs/utils/index.js';
+import { builderSchema, elementSchema, floatingSideAndAlignDataAttrs } from '$docs/utils/index.js';
 import { linkPreviewIdParts } from '$lib/index.js';
 import { linkPreviewEvents } from '$lib/builders/link-preview/events.js';
 import type { BuilderData } from './index.js';
@@ -77,6 +77,7 @@ const trigger = elementSchema('trigger', {
 const content = elementSchema('content', {
 	description: 'The content displayed in the linkpreview',
 	dataAttributes: [
+		...floatingSideAndAlignDataAttrs,
 		{
 			name: 'data-state',
 			value: ATTRS.OPEN_CLOSED,
diff --git a/src/docs/data/builders/menu.ts b/src/docs/data/builders/menu.ts
index 70bd252e..a7846582 100644
--- a/src/docs/data/builders/menu.ts
+++ b/src/docs/data/builders/menu.ts
@@ -7,6 +7,8 @@ import {
 	genElements,
 	genProps,
 	propsToOptions,
+	floatingSideAndAlignDataAttrs,
+	floatingSideDataAttr,
 } from '$docs/utils/index.js';
 
 import { menuEvents } from '$lib/builders/menu/events.js';
@@ -49,6 +51,7 @@ export function getMenuSchemas(name: Menu) {
 	return {
 		menu: getMenuMenuSchema(name),
 		arrow: getMenuArrowSchema(name),
+		overlay: getMenuOverlaySchema(name),
 		submenuBuilder: getMenuSubmenuBuilderSchema(),
 		submenu: getMenuSubmenuSchema(name),
 		subTrigger: getMenuSubTriggerSchema(name),
@@ -193,6 +196,7 @@ function getMenuMenuSchema(builderName: string) {
 	return elementSchema('menu', {
 		description: `The element which wraps the entire menu.`,
 		dataAttributes: [
+			...floatingSideAndAlignDataAttrs,
 			{
 				name: 'data-state',
 				value: ATTRS.OPEN_CLOSED,
@@ -206,10 +210,28 @@ function getMenuMenuSchema(builderName: string) {
 	});
 }
 
+function getMenuOverlaySchema(builderName: string) {
+	return elementSchema('overlay', {
+		description:
+			'An optional overlay element to prevent interaction with the rest of the page while the menu is open',
+		dataAttributes: [
+			{
+				name: 'data-state',
+				value: ATTRS.OPEN_CLOSED,
+			},
+			{
+				name: `data-melt-${toKebabCase(builderName)}-overlay`,
+				value: ATTRS.MELT('overlay'),
+			},
+		],
+	});
+}
+
 export function getMenuArrowSchema(builderName: string) {
 	return elementSchema('arrow', {
 		description: `An optional arrow element which points to the menu's trigger.`,
 		dataAttributes: [
+			floatingSideDataAttr,
 			{
 				name: 'data-arrow',
 				value: ATTRS.TRUE,
@@ -352,6 +374,7 @@ function getMenuSubmenuSchema(name: string) {
 	return elementSchema('submenu', {
 		description: 'A submenu element displayed when its trigger is selected.',
 		dataAttributes: [
+			...floatingSideAndAlignDataAttrs,
 			{
 				name: 'data-state',
 				value: ATTRS.OPEN_CLOSED,
diff --git a/src/docs/data/builders/popover.ts b/src/docs/data/builders/popover.ts
index e8f0ff43..d31d4173 100644
--- a/src/docs/data/builders/popover.ts
+++ b/src/docs/data/builders/popover.ts
@@ -1,6 +1,11 @@
 import { ATTRS, KBD, PROPS } from '$docs/constants.js';
 import type { KeyboardSchema } from '$docs/types.js';
-import { builderSchema, elementSchema } from '$docs/utils/index.js';
+import {
+	builderSchema,
+	elementSchema,
+	floatingSideAndAlignDataAttrs,
+	floatingSideDataAttr,
+} from '$docs/utils/index.js';
 import { popoverIdParts } from '$lib/index.js';
 import { popoverEvents } from '$lib/builders/popover/events.js';
 import type { BuilderData } from './index.js';
@@ -41,6 +46,10 @@ const builder = builderSchema(BUILDER_NAME, {
 			name: 'content',
 			description: 'The builder store used to create the popover content.',
 		},
+		{
+			name: 'overlay',
+			description: 'The builder store used to create the popover overlay.',
+		},
 		{
 			name: 'close',
 			description: 'The builder store used to create the popover close button.',
@@ -78,6 +87,11 @@ const trigger = elementSchema('trigger', {
 const content = elementSchema('content', {
 	description: 'The popover content.',
 	dataAttributes: [
+		...floatingSideAndAlignDataAttrs,
+		{
+			name: 'data-state',
+			value: ATTRS.OPEN_CLOSED,
+		},
 		{
 			name: 'data-melt-popover-content',
 			value: ATTRS.MELT('content'),
@@ -85,10 +99,26 @@ const content = elementSchema('content', {
 	],
 });
 
+const overlay = elementSchema('overlay', {
+	description:
+		'The optional popover overlay element, which can be used to give the popover modal-like behavior where the user cannot interact with the rest of the page while the popover is open.',
+	dataAttributes: [
+		{
+			name: 'data-state',
+			value: ATTRS.OPEN_CLOSED,
+		},
+		{
+			name: 'data-melt-popover-overlay',
+			value: ATTRS.MELT('overlay'),
+		},
+	],
+});
+
 const arrow = elementSchema('arrow', {
 	title: 'arrow',
 	description: 'The optional arrow element.',
 	dataAttributes: [
+		floatingSideDataAttr,
 		{
 			name: 'data-arrow',
 			value: ATTRS.TRUE,
@@ -137,7 +167,7 @@ const keyboard: KeyboardSchema = [
 	},
 ];
 
-const schemas = [builder, trigger, content, close, arrow];
+const schemas = [builder, trigger, content, overlay, close, arrow];
 
 const features = [
 	'Full keyboard navigation',
diff --git a/src/docs/data/builders/scroll-area.ts b/src/docs/data/builders/scroll-area.ts
new file mode 100644
index 00000000..6dbef842
--- /dev/null
+++ b/src/docs/data/builders/scroll-area.ts
@@ -0,0 +1,181 @@
+import { ATTRS } from '$docs/constants.js';
+import { builderSchema, elementSchema } from '$docs/utils/index.js';
+import type { BuilderData } from './index.js';
+import { scrollAreaIdParts } from '$lib/builders/scroll-area/create.js';
+
+/**
+ * Props that are also returned in the form of stores via the `options` property.
+ */
+const OPTION_PROPS = [
+	{
+		name: 'type',
+		type: ['"auto"', '"always"', '"scroll"', '"hover"'],
+		default: '"hover"',
+		description: 'Determins when the scrollbar should be visible.',
+	},
+	{
+		name: 'hideDelay',
+		type: 'number',
+		default: '600',
+		description:
+			'When the `type` is `"scroll"` or `"hover"`, this determines how long the scrollbar should be visible after the user either stops scrolling or stops hovering over the scroll area.',
+	},
+	{
+		name: 'dir',
+		type: ['ltr', 'rtl'],
+		default: 'ltr',
+		description: 'The reading direction of the scroll area.',
+	},
+];
+
+const BUILDER_NAME = 'scroll area';
+
+const builder = builderSchema(BUILDER_NAME, {
+	ids: scrollAreaIdParts,
+	title: 'createScrollArea',
+	props: OPTION_PROPS,
+	elements: [
+		{
+			name: 'root',
+			description: 'The container that wraps all parts of the scroll area.',
+		},
+		{
+			name: 'viewport',
+			description:
+				'A container that wraps the scrollable content and is used to calculate the scrollbar size.',
+		},
+		{
+			name: 'content',
+			description: 'The scrollable content.',
+		},
+		{
+			name: 'scrollbarX',
+			description: 'The track of the horizontal scrollbar.',
+		},
+		{
+			name: 'thumbX',
+			description: 'The thumb of the horizontal scrollbar.',
+		},
+		{
+			name: 'scrollbarY',
+			description: 'The track of the vertical scrollbar.',
+		},
+		{
+			name: 'thumbY',
+			description: 'The thumb of the vertical scrollbar.',
+		},
+	],
+	options: OPTION_PROPS,
+});
+
+const root = elementSchema('root', {
+	description: 'The container that wraps all parts of the scroll area.',
+	dataAttributes: [
+		{
+			name: 'data-melt-scroll-area',
+			value: ATTRS.MELT('scroll area root'),
+		},
+	],
+});
+
+const viewport = elementSchema('viewport', {
+	description:
+		'The container that wraps the scrollable content and is used to calculate the scrollbar size.',
+	dataAttributes: [
+		{
+			name: 'data-melt-scroll-area-viewport',
+			value: ATTRS.MELT('scroll area viewport'),
+		},
+	],
+});
+
+const content = elementSchema('content', {
+	description: 'The scrollable content. This is the element that will be scrolled.',
+	dataAttributes: [
+		{
+			name: 'data-melt-scroll-area-content',
+			value: ATTRS.MELT('scroll area content'),
+		},
+	],
+});
+
+const scrollbarX = elementSchema('scrollbarX', {
+	description: 'The track of the horizontal scrollbar.',
+	dataAttributes: [
+		{
+			name: 'data-state',
+			value: '`"visible" | "hidden"`',
+		},
+		{
+			name: 'data-melt-scroll-area-scrollbar',
+			value: ATTRS.MELT('scroll area scrollbar'),
+		},
+	],
+	events: ['pointerdown', 'pointerup', 'pointermove'],
+});
+const thumbX = elementSchema('thumbX', {
+	description: 'The thumb of the horizontal scrollbar.',
+	dataAttributes: [
+		{
+			name: 'data-state',
+			value: '`"visible" | "hidden"`',
+		},
+		{
+			name: 'data-melt-scroll-area-thumb',
+			value: ATTRS.MELT('scroll area thumb'),
+		},
+	],
+	events: ['pointerdown', 'pointerup'],
+});
+
+const scrollbarY = elementSchema('scrollbarY', {
+	description: 'The track of the vertical scrollbar.',
+	dataAttributes: [
+		{
+			name: 'data-state',
+			value: '`"visible" | "hidden"`',
+		},
+		{
+			name: 'data-melt-scroll-area-scrollbar',
+			value: ATTRS.MELT('scroll area scrollbar'),
+		},
+	],
+	events: ['pointerdown', 'pointerup', 'pointermove'],
+});
+
+const thumbY = elementSchema('thumbY', {
+	description: 'The thumb of the vertical scrollbar.',
+	dataAttributes: [
+		{
+			name: 'data-state',
+			value: '`"visible" | "hidden"`',
+		},
+		{
+			name: 'data-melt-scroll-area-thumb',
+			value: ATTRS.MELT('scroll area thumb'),
+		},
+	],
+	events: ['pointerdown', 'pointerup'],
+});
+
+const schemas: BuilderData['schemas'] = [
+	builder,
+	root,
+	viewport,
+	content,
+	scrollbarY,
+	thumbY,
+	scrollbarX,
+	thumbX,
+];
+
+const features: BuilderData['features'] = [
+	'Scrolling behavior is native to the platform',
+	'Scrollbar is positioned above the content so no layout shifting occurs',
+	'Keyboard controls are unaffected and follow platform conventions',
+];
+
+export const scrollAreaData: BuilderData = {
+	schemas,
+	features,
+};
diff --git a/src/docs/data/builders/select.ts b/src/docs/data/builders/select.ts
index 264c7016..6eb233a3 100644
--- a/src/docs/data/builders/select.ts
+++ b/src/docs/data/builders/select.ts
@@ -1,6 +1,11 @@
 import { ATTRS, KBD, PROPS, SEE } from '$docs/constants.js';
 import type { KeyboardSchema } from '$docs/types.js';
-import { builderSchema, elementSchema } from '$docs/utils/index.js';
+import {
+	builderSchema,
+	elementSchema,
+	floatingSideAndAlignDataAttrs,
+	floatingSideDataAttr,
+} from '$docs/utils/index.js';
 import { listboxIdParts } from '$lib/builders/listbox/create.js';
 import { selectEvents } from '$lib/builders/select/events.js';
 import type { BuilderData } from './index.js';
@@ -154,6 +159,7 @@ const trigger = elementSchema('trigger', {
 const menu = elementSchema('menu', {
 	description: 'The menu element',
 	dataAttributes: [
+		...floatingSideAndAlignDataAttrs,
 		{
 			name: 'data-melt-select-menu',
 			value: ATTRS.MELT('menu'),
@@ -216,6 +222,7 @@ const label = elementSchema('label', {
 const arrow = elementSchema('arrow', {
 	description: 'The optional arrow element',
 	dataAttributes: [
+		floatingSideDataAttr,
 		{
 			name: 'data-arrow',
 			value: ATTRS.TRUE,
diff --git a/src/docs/data/builders/tooltip.ts b/src/docs/data/builders/tooltip.ts
index 9ebdd71a..05243a89 100644
--- a/src/docs/data/builders/tooltip.ts
+++ b/src/docs/data/builders/tooltip.ts
@@ -1,6 +1,11 @@
 import { ATTRS, KBD, PROPS } from '$docs/constants.js';
 import type { KeyboardSchema } from '$docs/types.js';
-import { builderSchema, elementSchema } from '$docs/utils/index.js';
+import {
+	builderSchema,
+	elementSchema,
+	floatingSideAndAlignDataAttrs,
+	floatingSideDataAttr,
+} from '$docs/utils/index.js';
 import { tooltipIdParts } from '$lib/index.js';
 import { tooltipEvents } from '$lib/builders/tooltip/events.js';
 import type { BuilderData } from './index.js';
@@ -99,6 +104,7 @@ const content = elementSchema('content', {
 			name: 'data-state',
 			value: ATTRS.OPEN_CLOSED,
 		},
+		...floatingSideAndAlignDataAttrs,
 		{
 			name: 'data-melt-tooltip-content',
 			value: ATTRS.MELT('tooltip content'),
@@ -114,6 +120,7 @@ const arrow = elementSchema('arrow', {
 			name: 'data-arrow',
 			value: ATTRS.TRUE,
 		},
+		floatingSideDataAttr,
 		{
 			name: 'data-melt-tooltip-arrow',
 			value: ATTRS.MELT('tooltip arrow'),
diff --git a/src/docs/previews/context-menu/modal/tailwind/index.svelte b/src/docs/previews/context-menu/modal/tailwind/index.svelte
new file mode 100644
index 00000000..370b65b5
--- /dev/null
+++ b/src/docs/previews/context-menu/modal/tailwind/index.svelte
@@ -0,0 +1,151 @@
+<script lang="ts">
+	import { createContextMenu, melt } from '$lib/index.js';
+	import { writable } from 'svelte/store';
+	import { ChevronRight, Check } from '$icons/index.js';
+
+	const settingsSync = writable(true);
+	const hideMeltUI = writable(false);
+
+	const {
+		elements: { trigger, menu, item, separator, overlay },
+		builders: { createSubmenu, createMenuRadioGroup, createCheckboxItem },
+	} = createContextMenu({
+		loop: true,
+	});
+
+	const {
+		elements: { subMenu: subMenuA, subTrigger: subTriggerA },
+	} = createSubmenu();
+
+	const {
+		elements: { radioGroup, radioItem },
+		helpers: { isChecked },
+	} = createMenuRadioGroup({
+		defaultValue: 'Hunter Johnston',
+	});
+
+	const {
+		elements: { checkboxItem },
+	} = createCheckboxItem({
+		checked: settingsSync,
+	});
+
+	const {
+		elements: { checkboxItem: checkboxItemA },
+	} = createCheckboxItem({
+		checked: hideMeltUI,
+	});
+
+	const personsArr = [
+		'Hunter Johnston',
+		'Thomas G. Lopes',
+		'Adrian Gonz',
+		'Franck Poingt',
+	];
+</script>
+
+<span class="trigger" use:melt={$trigger} aria-label="Update dimensions">
+	Right click me.
+</span>
+<div use:melt={$overlay} class="fixed inset-0 z-40" />
+<div class="force-dark menu" use:melt={$menu}>
+	<div class="item" use:melt={$item}>About Melt UI</div>
+	<div class="item" use:melt={$item}>Check for Updates...</div>
+	<div class="separator" use:melt={$separator} />
+	<div class="item" use:melt={$checkboxItem}>
+		<div class="check">
+			{#if $settingsSync}
+				<Check class="size-4" />
+			{/if}
+		</div>
+		Settings Sync is On
+	</div>
+	<div class="item" use:melt={$subTriggerA}>
+		Profiles
+		<div class="rightSlot">
+			<ChevronRight class="size-4" />
+		</div>
+	</div>
+	<div class="menu subMenu" use:melt={$subMenuA}>
+		<div class="text">People</div>
+		<div use:melt={$radioGroup}>
+			{#each personsArr as person}
+				<div class="item" use:melt={$radioItem({ value: person })}>
+					<div class="check">
+						{#if $isChecked(person)}
+							<div class="dot" />
+						{/if}
+					</div>
+					{person}
+				</div>
+			{/each}
+		</div>
+	</div>
+	<div use:melt={$separator} class="separator" />
+
+	<div class="item" use:melt={$checkboxItemA}>
+		<div class="check">
+			{#if $hideMeltUI}
+				<Check class="size-4" />
+			{/if}
+		</div>
+		Hide Melt UI
+		<div class="rightSlot">⌘H</div>
+	</div>
+	<div class="item" use:melt={$item} data-disabled>
+		Show All Components
+		<div class="rightSlot">⇧⌘N</div>
+	</div>
+	<div use:melt={$separator} class="separator" />
+	<div class="item" use:melt={$item}>
+		Quit Melt UI
+		<div class="rightSlot">⌘Q</div>
+	</div>
+</div>
+
+<style lang="postcss">
+	.menu {
+		@apply z-40 flex max-h-[300px] min-w-[220px] flex-col shadow;
+		@apply rounded-lg bg-white p-1 lg:max-h-none;
+		@apply ring-0 !important;
+	}
+	.subMenu {
+		@apply min-w-[220px] shadow-md shadow-neutral-900/30;
+	}
+	.item {
+		@apply relative h-6 min-h-[24px] select-none rounded-md pl-6 pr-1;
+		@apply z-40 text-magnum-900 outline-none;
+		@apply data-[highlighted]:bg-magnum-200 data-[highlighted]:text-magnum-900;
+		@apply data-[disabled]:text-neutral-300;
+		@apply flex items-center text-sm leading-none;
+		@apply ring-0 !important;
+	}
+
+	.trigger {
+		@apply block rounded-xl border-2 border-dashed border-magnum-900 font-semibold text-magnum-700;
+		@apply w-[300px] bg-magnum-100 py-12 text-center shadow;
+	}
+	.check {
+		@apply absolute left-2 top-1/2 text-magnum-500;
+		translate: 0 calc(-50% + 1px);
+	}
+
+	.dot {
+		@apply h-[4.75px] w-[4.75px] rounded-full bg-magnum-900;
+	}
+
+	.separator {
+		@apply m-[5px] h-[1px] bg-magnum-200;
+	}
+
+	.rightSlot {
+		@apply ml-auto pl-5;
+	}
+
+	.check {
+		@apply absolute left-0 inline-flex w-6 items-center justify-center;
+	}
+	.text {
+		@apply pl-6 text-xs leading-6 text-neutral-600;
+	}
+</style>
diff --git a/src/docs/previews/dialog/alert/css/index.svelte b/src/docs/previews/dialog/alert/css/index.svelte
index a420fcf1..871b2204 100644
--- a/src/docs/previews/dialog/alert/css/index.svelte
+++ b/src/docs/previews/dialog/alert/css/index.svelte
@@ -22,8 +22,8 @@
 </script>
 
 <button use:melt={$trigger} class="trigger"> Delete Item </button>
-<div use:melt={$portalled}>
-	{#if $open}
+{#if $open}
+	<div use:melt={$portalled}>
 		<div use:melt={$overlay} class="overlay" />
 		<div
 			class="content"
@@ -51,8 +51,8 @@
 				<X class="size-4" />
 			</button>
 		</div>
-	{/if}
-</div>
+	</div>
+{/if}
 
 <style>
 	.trigger {
diff --git a/src/docs/previews/dialog/alert/tailwind/index.svelte b/src/docs/previews/dialog/alert/tailwind/index.svelte
index e2d0c132..442b16f3 100644
--- a/src/docs/previews/dialog/alert/tailwind/index.svelte
+++ b/src/docs/previews/dialog/alert/tailwind/index.svelte
@@ -29,8 +29,8 @@
 	Delete Item
 </button>
 
-<div class="force-dark" use:melt={$portalled}>
-	{#if $open}
+{#if $open}
+	<div class="force-dark" use:melt={$portalled}>
 		<div use:melt={$overlay} class="fixed inset-0 z-50 bg-black/50" />
 		<div
 			class="fixed left-[50%] top-[50%] z-50 max-h-[85vh] w-[90vw]
@@ -78,5 +78,5 @@
 				<X class="size-4" />
 			</button>
 		</div>
-	{/if}
-</div>
+	</div>
+{/if}
diff --git a/src/docs/previews/dialog/controlled/css/index.svelte b/src/docs/previews/dialog/controlled/css/index.svelte
index 67dd5241..289b8a5e 100644
--- a/src/docs/previews/dialog/controlled/css/index.svelte
+++ b/src/docs/previews/dialog/controlled/css/index.svelte
@@ -25,8 +25,8 @@
 </script>
 
 <button use:melt={$trigger} class="trigger"> Open Dialog </button>
-<div use:melt={$portalled}>
-	{#if $open}
+{#if $open}
+	<div use:melt={$portalled}>
 		<div use:melt={$overlay} class="overlay" />
 		<div
 			class="content"
@@ -59,8 +59,8 @@
 				<X class="size-4" />
 			</button>
 		</div>
-	{/if}
-</div>
+	</div>
+{/if}
 
 <style lang="postcss">
 	.trigger {
diff --git a/src/docs/previews/dialog/controlled/tailwind/index.svelte b/src/docs/previews/dialog/controlled/tailwind/index.svelte
index 5b9c1bec..fa0dfdd7 100644
--- a/src/docs/previews/dialog/controlled/tailwind/index.svelte
+++ b/src/docs/previews/dialog/controlled/tailwind/index.svelte
@@ -31,8 +31,8 @@
 >
 	Open Dialog
 </button>
-<div class="force-dark" use:melt={$portalled}>
-	{#if $open}
+{#if $open}
+	<div class="force-dark" use:melt={$portalled}>
 		<div use:melt={$overlay} class="fixed inset-0 z-50 bg-black/50" />
 		<div
 			class="fixed left-[50%] top-[50%] z-50 max-h-[85vh] w-[90vw]
@@ -101,5 +101,5 @@
 				<X class="size-4" />
 			</button>
 		</div>
-	{/if}
-</div>
+	</div>
+{/if}
diff --git a/src/docs/previews/dialog/drawer/css/index.svelte b/src/docs/previews/dialog/drawer/css/index.svelte
index d03a33d7..05cc5385 100644
--- a/src/docs/previews/dialog/drawer/css/index.svelte
+++ b/src/docs/previews/dialog/drawer/css/index.svelte
@@ -21,8 +21,8 @@
 </script>
 
 <button use:melt={$trigger} class="trigger"> View Notifications </button>
-<div use:melt={$portalled}>
-	{#if $open}
+{#if $open}
+	<div use:melt={$portalled}>
 		<div
 			use:melt={$overlay}
 			class="overlay"
@@ -57,8 +57,8 @@
 				</div>
 			</section>
 		</div>
-	{/if}
-</div>
+	</div>
+{/if}
 
 <style>
 	.trigger {
diff --git a/src/docs/previews/dialog/drawer/tailwind/index.svelte b/src/docs/previews/dialog/drawer/tailwind/index.svelte
index 916af375..b516de07 100644
--- a/src/docs/previews/dialog/drawer/tailwind/index.svelte
+++ b/src/docs/previews/dialog/drawer/tailwind/index.svelte
@@ -28,8 +28,8 @@
 >
 	View Notifications
 </button>
-<div class="force-dark" use:melt={$portalled}>
-	{#if $open}
+{#if $open}
+	<div class="force-dark" use:melt={$portalled}>
 		<div
 			use:melt={$overlay}
 			class="fixed inset-0 z-50 bg-black/50"
@@ -88,5 +88,5 @@
 				</div>
 			</section>
 		</div>
-	{/if}
-</div>
+	</div>
+{/if}
diff --git a/src/docs/previews/dialog/main/css/index.svelte b/src/docs/previews/dialog/main/css/index.svelte
index e9df12ab..dee12837 100644
--- a/src/docs/previews/dialog/main/css/index.svelte
+++ b/src/docs/previews/dialog/main/css/index.svelte
@@ -19,8 +19,8 @@
 </script>
 
 <button use:melt={$trigger} class="trigger"> Open Dialog </button>
-<div use:melt={$portalled}>
-	{#if $open}
+{#if $open}
+	<div use:melt={$portalled}>
 		<div use:melt={$overlay} class="overlay" />
 		<div
 			class="content"
@@ -53,8 +53,8 @@
 				<X class="size-4" />
 			</button>
 		</div>
-	{/if}
-</div>
+	</div>
+{/if}
 
 <style lang="postcss">
 	.trigger {
diff --git a/src/docs/previews/dialog/main/tailwind/index.svelte b/src/docs/previews/dialog/main/tailwind/index.svelte
index add998df..a64e1d1c 100644
--- a/src/docs/previews/dialog/main/tailwind/index.svelte
+++ b/src/docs/previews/dialog/main/tailwind/index.svelte
@@ -29,8 +29,8 @@
 	Open Dialog
 </button>
 
-<div class="force-dark" use:melt={$portalled}>
-	{#if $open}
+{#if $open}
+	<div class="force-dark" use:melt={$portalled}>
 		<div
 			use:melt={$overlay}
 			class="fixed inset-0 z-50 bg-black/50"
@@ -100,5 +100,5 @@
 				<X class="size-4" />
 			</button>
 		</div>
-	{/if}
-</div>
+	</div>
+{/if}
diff --git a/src/docs/previews/dialog/nested/css/index.svelte b/src/docs/previews/dialog/nested/css/index.svelte
index 1e753b0d..88a82051 100644
--- a/src/docs/previews/dialog/nested/css/index.svelte
+++ b/src/docs/previews/dialog/nested/css/index.svelte
@@ -36,8 +36,8 @@
 </script>
 
 <button use:melt={$trigger} class="trigger"> Open Dialog </button>
-<div use:melt={$portalled}>
-	{#if $open}
+{#if $open}
+	<div use:melt={$portalled}>
 		<div use:melt={$overlay} class="overlay" />
 		<div
 			class="content"
@@ -62,11 +62,11 @@
 				<X />
 			</button>
 		</div>
-	{/if}
-</div>
+	</div>
+{/if}
 
-<div use:melt={$portalledNested}>
-	{#if $openNested}
+{#if $openNested}
+	<div use:melt={$portalledNested}>
 		<div use:melt={$overlayNested} class="overlay overlay-nested" />
 		<div
 			class="content content-nested"
@@ -90,8 +90,8 @@
 				<X />
 			</button>
 		</div>
-	{/if}
-</div>
+	</div>
+{/if}
 
 <style>
 	.trigger {
diff --git a/src/docs/previews/dialog/nested/tailwind/index.svelte b/src/docs/previews/dialog/nested/tailwind/index.svelte
index 83a28825..852b639a 100644
--- a/src/docs/previews/dialog/nested/tailwind/index.svelte
+++ b/src/docs/previews/dialog/nested/tailwind/index.svelte
@@ -38,8 +38,8 @@
 >
 	Open Dialog
 </button>
-<div class="force-dark" use:melt={$portalled}>
-	{#if $open}
+{#if $open}
+	<div class="force-dark" use:melt={$portalled}>
 		<div use:melt={$overlay} class="fixed inset-0 z-50 bg-black/50" />
 		<div
 			class="fixed left-[50%] top-[50%] z-50 max-h-[85vh] w-[90vw]
@@ -75,8 +75,8 @@
 					Open second
 				</button>
 			</div>
-			<div class="force-dark" use:melt={$portalledNested}>
-				{#if $openNested}
+			{#if $openNested}
+				<div class="force-dark" use:melt={$portalledNested}>
 					<div
 						use:melt={$overlayNested}
 						class="fixed inset-0 z-50 bg-black/75"
@@ -124,8 +124,8 @@
 							<X class="size-4" />
 						</button>
 					</div>
-				{/if}
-			</div>
+				</div>
+			{/if}
 		</div>
 		<button
 			use:melt={$close}
@@ -135,5 +135,5 @@
 		>
 			<X />
 		</button>
-	{/if}
-</div>
+	</div>
+{/if}
diff --git a/src/docs/previews/dropdown-menu/main/tailwind/index.svelte b/src/docs/previews/dropdown-menu/main/tailwind/index.svelte
index c59b78a8..d96c4818 100644
--- a/src/docs/previews/dropdown-menu/main/tailwind/index.svelte
+++ b/src/docs/previews/dropdown-menu/main/tailwind/index.svelte
@@ -128,7 +128,7 @@
 
 <style lang="postcss">
 	.menu {
-		@apply z-10 flex max-h-[300px] min-w-[220px] flex-col shadow-lg;
+		@apply z-40 flex max-h-[300px] min-w-[220px] flex-col shadow-lg;
 		@apply rounded-md bg-white p-1 shadow-neutral-900/30 lg:max-h-none;
 		@apply ring-0 !important;
 	}
@@ -137,7 +137,7 @@
 	}
 	.item {
 		@apply relative h-6 min-h-[24px] select-none rounded-sm pl-6 pr-1;
-		@apply z-20 text-magnum-900 outline-none;
+		@apply z-40 text-magnum-900 outline-none;
 		@apply data-[highlighted]:bg-magnum-200 data-[highlighted]:text-magnum-900;
 		@apply data-[disabled]:text-neutral-300;
 		@apply flex items-center text-sm leading-none;
diff --git a/src/docs/previews/dropdown-menu/modal/tailwind/index.svelte b/src/docs/previews/dropdown-menu/modal/tailwind/index.svelte
new file mode 100644
index 00000000..5d2d26bf
--- /dev/null
+++ b/src/docs/previews/dropdown-menu/modal/tailwind/index.svelte
@@ -0,0 +1,179 @@
+<script lang="ts">
+	import { createDropdownMenu, melt } from '$lib/index.js';
+	import { writable } from 'svelte/store';
+	import { AlignJustify, ChevronRight, Check } from '$icons/index.js';
+	import { fly } from 'svelte/transition';
+
+	const settingsSync = writable(true);
+	const hideMeltUI = writable(false);
+
+	const {
+		elements: { trigger, menu, item, separator, arrow, overlay },
+		builders: { createSubmenu, createMenuRadioGroup, createCheckboxItem },
+		states: { open },
+	} = createDropdownMenu({
+		forceVisible: true,
+		loop: true,
+	});
+
+	const {
+		elements: { subMenu, subTrigger },
+		states: { subOpen },
+	} = createSubmenu();
+
+	const {
+		elements: { radioGroup, radioItem },
+		helpers: { isChecked },
+	} = createMenuRadioGroup({
+		defaultValue: 'Hunter Johnston',
+	});
+
+	const {
+		elements: { checkboxItem },
+	} = createCheckboxItem({
+		checked: settingsSync,
+	});
+
+	const {
+		elements: { checkboxItem: checkboxItemA },
+	} = createCheckboxItem({
+		checked: hideMeltUI,
+	});
+
+	const personsArr = [
+		'Hunter Johnston',
+		'Thomas G. Lopes',
+		'Adrian Gonz',
+		'Franck Poingt',
+	];
+</script>
+
+<button
+	type="button"
+	class="trigger"
+	use:melt={$trigger}
+	aria-label="Update dimensions"
+>
+	<AlignJustify class="size-4" />
+	<span class="sr-only">Open Popover</span>
+</button>
+
+{#if $open}
+	<div use:melt={$overlay} class="fixed inset-0 z-40" />
+	<div
+		class="force-dark menu"
+		use:melt={$menu}
+		transition:fly={{ duration: 150, y: -10 }}
+	>
+		<div class="item" use:melt={$item}>About Melt UI</div>
+		<div class="item" use:melt={$item}>Check for Updates...</div>
+		<div class="separator" use:melt={$separator} />
+		<div class="item" use:melt={$checkboxItem}>
+			<div class="check">
+				{#if $settingsSync}
+					<Check class="size-4" />
+				{/if}
+			</div>
+			Settings Sync is On
+		</div>
+		<div class="item !hidden md:!flex" use:melt={$subTrigger}>
+			Profiles
+			<div class="rightSlot">
+				<ChevronRight class="size-4" />
+			</div>
+		</div>
+		{#if $subOpen}
+			<div
+				class="menu subMenu"
+				use:melt={$subMenu}
+				transition:fly={{ x: -50, duration: 150 }}
+			>
+				<div class="text">People</div>
+				<div use:melt={$radioGroup}>
+					{#each personsArr as person}
+						<div class="item" use:melt={$radioItem({ value: person })}>
+							<div class="check">
+								{#if $isChecked(person)}
+									<div class="dot" />
+								{/if}
+							</div>
+							{person}
+						</div>
+					{/each}
+				</div>
+			</div>
+		{/if}
+		<div use:melt={$separator} class="separator" />
+
+		<div class="item" use:melt={$checkboxItemA}>
+			<div class="check">
+				{#if $hideMeltUI}
+					<Check class="size-4" />
+				{/if}
+			</div>
+			Hide Melt UI
+			<div class="rightSlot">⌘H</div>
+		</div>
+		<div class="item" use:melt={$item} data-disabled>
+			Show All Components
+			<div class="rightSlot">⇧⌘N</div>
+		</div>
+		<div use:melt={$separator} class="separator" />
+		<div class="item" use:melt={$item}>
+			Quit Melt UI
+			<div class="rightSlot">⌘Q</div>
+		</div>
+		<div use:melt={$arrow} />
+	</div>
+{/if}
+
+<style lang="postcss">
+	.menu {
+		@apply z-40 flex max-h-[300px] min-w-[220px] flex-col shadow-lg;
+		@apply rounded-md bg-white p-1 shadow-neutral-900/30 lg:max-h-none;
+		@apply ring-0 !important;
+	}
+	.subMenu {
+		@apply min-w-[220px] shadow-md shadow-neutral-900/30;
+	}
+	.item {
+		@apply relative h-6 min-h-[24px] select-none rounded-sm pl-6 pr-1;
+		@apply z-40 text-magnum-900 outline-none;
+		@apply data-[highlighted]:bg-magnum-200 data-[highlighted]:text-magnum-900;
+		@apply data-[disabled]:text-neutral-300;
+		@apply flex items-center text-sm leading-none;
+		@apply ring-0 !important;
+	}
+	.trigger {
+		@apply inline-flex h-9 w-9 items-center justify-center rounded-full bg-white;
+		@apply text-magnum-900 transition-colors hover:bg-white/90;
+		@apply data-[highlighted]:ring-magnum-400 data-[highlighted]:ring-offset-2 !important;
+		@apply p-0 text-sm font-medium  data-[highlighted]:outline-none;
+	}
+	.check {
+		@apply absolute left-2 top-1/2 text-magnum-500;
+		translate: 0 calc(-50% + 1px);
+	}
+
+	.dot {
+		@apply h-[4.75px] w-[4.75px] rounded-full bg-magnum-900;
+	}
+
+	.separator {
+		@apply m-[5px] h-[1px] bg-magnum-200;
+	}
+
+	.rightSlot {
+		@apply ml-auto pl-5;
+	}
+
+	.icon {
+		@apply h-[13px] w-[13px];
+	}
+	.check {
+		@apply absolute left-0 inline-flex w-6 items-center justify-center;
+	}
+	.text {
+		@apply pl-6 text-xs leading-6 text-neutral-600;
+	}
+</style>
diff --git a/src/docs/previews/popover/modal/tailwind/index.svelte b/src/docs/previews/popover/modal/tailwind/index.svelte
new file mode 100644
index 00000000..a2c1bd8d
--- /dev/null
+++ b/src/docs/previews/popover/modal/tailwind/index.svelte
@@ -0,0 +1,95 @@
+<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, overlay },
+		states: { open },
+	} = createPopover({
+		forceVisible: true,
+		preventScroll: true,
+	});
+</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={$overlay} class="fixed inset-0 z-40" />
+	<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-40 w-60 rounded-[4px] bg-white p-5 shadow-sm;
+	}
+</style>
diff --git a/src/docs/previews/scroll-area/always/tailwind/flavors.ts b/src/docs/previews/scroll-area/always/tailwind/flavors.ts
new file mode 100644
index 00000000..d2ae44fa
--- /dev/null
+++ b/src/docs/previews/scroll-area/always/tailwind/flavors.ts
@@ -0,0 +1,32 @@
+export const flavors = [
+	'Vanilla',
+	'Chocolate',
+	'Strawberry',
+	'Mint Chocolate Chip',
+	'Cookies and Cream',
+	'Rocky Road',
+	'Pistachio',
+	'Neapolitan',
+	'Butter Pecan',
+	'Salted Caramel',
+	'Coffee',
+	'Mango',
+	'Raspberry Ripple',
+	'Lemon Sorbet',
+	'Green Tea',
+	'Coconut',
+	'Black Cherry',
+	'Banana',
+	'Almond Fudge',
+	'Cinnamon',
+	'Blueberry Cheesecake',
+	'Tiramisu',
+	'Red Velvet',
+	'Matcha',
+	'Peanut Butter Cup',
+	'Cookie Dough',
+	'Rum Raisin',
+	'Birthday Cake',
+	'Lychee',
+	'Honey Lavender',
+];
diff --git a/src/docs/previews/scroll-area/always/tailwind/index.svelte b/src/docs/previews/scroll-area/always/tailwind/index.svelte
new file mode 100644
index 00000000..6e0256c3
--- /dev/null
+++ b/src/docs/previews/scroll-area/always/tailwind/index.svelte
@@ -0,0 +1,40 @@
+<script lang="ts">
+	import { createScrollArea, melt } from '$lib/index.js';
+	import { flavors } from './flavors.js';
+
+	const {
+		elements: { root, content, viewport, corner, scrollbarY, thumbY },
+	} = createScrollArea({
+		type: 'always',
+		dir: 'ltr',
+	});
+</script>
+
+<div
+	use:melt={$root}
+	class="relative h-72 w-56 overflow-hidden rounded-md border border-neutral-700 bg-neutral-800/90 text-white shadow-lg"
+>
+	<div use:melt={$viewport} class="h-full w-full rounded-[inherit]">
+		<div use:melt={$content}>
+			<div class="p-7">
+				<h4 class="mb-4 font-semibold leading-none">Endless Flavors</h4>
+				{#each flavors as flavor (flavor)}
+					<div class="text-sm">
+						{flavor}
+					</div>
+					<div role="separator" class="my-2 h-px w-full bg-magnum-800/30" />
+				{/each}
+			</div>
+		</div>
+	</div>
+	<div
+		use:melt={$scrollbarY}
+		class="flex h-full w-2.5 touch-none select-none border-l border-l-transparent bg-neutral-300/10 p-px transition-colors"
+	>
+		<div
+			use:melt={$thumbY}
+			class="relative flex-1 rounded-full bg-neutral-300/50"
+		/>
+	</div>
+	<div use:melt={$corner} />
+</div>
diff --git a/src/docs/previews/scroll-area/auto/tailwind/flavors.ts b/src/docs/previews/scroll-area/auto/tailwind/flavors.ts
new file mode 100644
index 00000000..d2ae44fa
--- /dev/null
+++ b/src/docs/previews/scroll-area/auto/tailwind/flavors.ts
@@ -0,0 +1,32 @@
+export const flavors = [
+	'Vanilla',
+	'Chocolate',
+	'Strawberry',
+	'Mint Chocolate Chip',
+	'Cookies and Cream',
+	'Rocky Road',
+	'Pistachio',
+	'Neapolitan',
+	'Butter Pecan',
+	'Salted Caramel',
+	'Coffee',
+	'Mango',
+	'Raspberry Ripple',
+	'Lemon Sorbet',
+	'Green Tea',
+	'Coconut',
+	'Black Cherry',
+	'Banana',
+	'Almond Fudge',
+	'Cinnamon',
+	'Blueberry Cheesecake',
+	'Tiramisu',
+	'Red Velvet',
+	'Matcha',
+	'Peanut Butter Cup',
+	'Cookie Dough',
+	'Rum Raisin',
+	'Birthday Cake',
+	'Lychee',
+	'Honey Lavender',
+];
diff --git a/src/docs/previews/scroll-area/auto/tailwind/index.svelte b/src/docs/previews/scroll-area/auto/tailwind/index.svelte
new file mode 100644
index 00000000..f6ae99d9
--- /dev/null
+++ b/src/docs/previews/scroll-area/auto/tailwind/index.svelte
@@ -0,0 +1,88 @@
+<script lang="ts">
+	import { createScrollArea, melt } from '$lib/index.js';
+	import { flavors } from './flavors.js';
+
+	const {
+		elements: { root, content, viewport, corner, scrollbarY, thumbY },
+	} = createScrollArea({
+		type: 'auto',
+		dir: 'ltr',
+	});
+
+	const {
+		elements: {
+			root: rootA,
+			content: contentA,
+			viewport: viewportA,
+			corner: cornerA,
+			scrollbarY: scrollbarYA,
+			thumbY: thumbYA,
+		},
+	} = createScrollArea({
+		type: 'auto',
+		dir: 'ltr',
+	});
+
+	let height = 288;
+</script>
+
+<div class="flex flex-col items-center gap-4 sm:flex-row">
+	<div
+		use:melt={$rootA}
+		class="relative h-72 w-56 overflow-hidden rounded-md border border-neutral-700 bg-neutral-800/90 text-white shadow-lg"
+	>
+		<div use:melt={$viewportA} class="h-full w-full rounded-[inherit]">
+			<div use:melt={$contentA}>
+				<div class="p-7">
+					<h4 class="mb-4 font-semibold leading-none">Endless Flavors</h4>
+					{#each flavors as flavor (flavor)}
+						<div class="text-sm">
+							{flavor}
+						</div>
+						<div role="separator" class="my-2 h-px w-full bg-magnum-800/30" />
+					{/each}
+				</div>
+			</div>
+		</div>
+		<div
+			use:melt={$scrollbarYA}
+			class="flex h-full w-2.5 touch-none select-none border-l border-l-transparent bg-neutral-300/10 p-px transition-colors"
+		>
+			<div
+				use:melt={$thumbYA}
+				class="relative flex-1 rounded-full bg-neutral-300/50"
+			/>
+		</div>
+		<div use:melt={$cornerA} />
+	</div>
+
+	<div
+		use:melt={$root}
+		class="relative h-72 w-56 overflow-hidden rounded-md border border-neutral-700 bg-neutral-800/90 text-white shadow-lg"
+		style:height="{height}px"
+	>
+		<div use:melt={$viewport} class="h-full w-full rounded-[inherit]">
+			<div use:melt={$content}>
+				<div class="p-7">
+					<h4 class="mb-4 font-semibold leading-none">A Few Flavors</h4>
+					{#each flavors.slice(0, 5) as flavor (flavor)}
+						<div class="text-sm">
+							{flavor}
+						</div>
+						<div role="separator" class="my-2 h-px w-full bg-magnum-800/30" />
+					{/each}
+				</div>
+			</div>
+		</div>
+		<div
+			use:melt={$scrollbarY}
+			class="flex h-full w-2.5 touch-none select-none border-l border-l-transparent bg-neutral-300/10 p-px transition-colors"
+		>
+			<div
+				use:melt={$thumbY}
+				class="relative flex-1 rounded-full bg-neutral-300/50"
+			/>
+		</div>
+		<div use:melt={$corner} />
+	</div>
+</div>
diff --git a/src/docs/previews/scroll-area/hover/tailwind/flavors.ts b/src/docs/previews/scroll-area/hover/tailwind/flavors.ts
new file mode 100644
index 00000000..d2ae44fa
--- /dev/null
+++ b/src/docs/previews/scroll-area/hover/tailwind/flavors.ts
@@ -0,0 +1,32 @@
+export const flavors = [
+	'Vanilla',
+	'Chocolate',
+	'Strawberry',
+	'Mint Chocolate Chip',
+	'Cookies and Cream',
+	'Rocky Road',
+	'Pistachio',
+	'Neapolitan',
+	'Butter Pecan',
+	'Salted Caramel',
+	'Coffee',
+	'Mango',
+	'Raspberry Ripple',
+	'Lemon Sorbet',
+	'Green Tea',
+	'Coconut',
+	'Black Cherry',
+	'Banana',
+	'Almond Fudge',
+	'Cinnamon',
+	'Blueberry Cheesecake',
+	'Tiramisu',
+	'Red Velvet',
+	'Matcha',
+	'Peanut Butter Cup',
+	'Cookie Dough',
+	'Rum Raisin',
+	'Birthday Cake',
+	'Lychee',
+	'Honey Lavender',
+];
diff --git a/src/docs/previews/scroll-area/hover/tailwind/index.svelte b/src/docs/previews/scroll-area/hover/tailwind/index.svelte
new file mode 100644
index 00000000..0d4a0cef
--- /dev/null
+++ b/src/docs/previews/scroll-area/hover/tailwind/index.svelte
@@ -0,0 +1,40 @@
+<script lang="ts">
+	import { createScrollArea, melt } from '$lib/index.js';
+	import { flavors } from './flavors.js';
+
+	const {
+		elements: { root, content, viewport, corner, scrollbarY, thumbY },
+	} = createScrollArea({
+		type: 'hover',
+		dir: 'ltr',
+	});
+</script>
+
+<div
+	use:melt={$root}
+	class="relative h-72 w-56 overflow-hidden rounded-md border border-neutral-700 bg-neutral-800/90 text-white shadow-lg"
+>
+	<div use:melt={$viewport} class="h-full w-full rounded-[inherit]">
+		<div use:melt={$content}>
+			<div class="p-7">
+				<h4 class="mb-4 font-semibold leading-none">Endless Flavors</h4>
+				{#each flavors as flavor (flavor)}
+					<div class="text-sm">
+						{flavor}
+					</div>
+					<div role="separator" class="my-2 h-px w-full bg-magnum-800/30" />
+				{/each}
+			</div>
+		</div>
+	</div>
+	<div
+		use:melt={$scrollbarY}
+		class="flex h-full w-2.5 touch-none select-none border-l border-l-transparent bg-neutral-300/10 p-px transition-colors"
+	>
+		<div
+			use:melt={$thumbY}
+			class="relative flex-1 rounded-full bg-neutral-300/50"
+		/>
+	</div>
+	<div use:melt={$corner} />
+</div>
diff --git a/src/docs/previews/scroll-area/main/tailwind/flavors.ts b/src/docs/previews/scroll-area/main/tailwind/flavors.ts
new file mode 100644
index 00000000..d2ae44fa
--- /dev/null
+++ b/src/docs/previews/scroll-area/main/tailwind/flavors.ts
@@ -0,0 +1,32 @@
+export const flavors = [
+	'Vanilla',
+	'Chocolate',
+	'Strawberry',
+	'Mint Chocolate Chip',
+	'Cookies and Cream',
+	'Rocky Road',
+	'Pistachio',
+	'Neapolitan',
+	'Butter Pecan',
+	'Salted Caramel',
+	'Coffee',
+	'Mango',
+	'Raspberry Ripple',
+	'Lemon Sorbet',
+	'Green Tea',
+	'Coconut',
+	'Black Cherry',
+	'Banana',
+	'Almond Fudge',
+	'Cinnamon',
+	'Blueberry Cheesecake',
+	'Tiramisu',
+	'Red Velvet',
+	'Matcha',
+	'Peanut Butter Cup',
+	'Cookie Dough',
+	'Rum Raisin',
+	'Birthday Cake',
+	'Lychee',
+	'Honey Lavender',
+];
diff --git a/src/docs/previews/scroll-area/main/tailwind/index.svelte b/src/docs/previews/scroll-area/main/tailwind/index.svelte
new file mode 100644
index 00000000..7f5c6362
--- /dev/null
+++ b/src/docs/previews/scroll-area/main/tailwind/index.svelte
@@ -0,0 +1,55 @@
+<script lang="ts">
+	import { createScrollArea, melt } from '$lib/index.js';
+	import { flavors } from './flavors.js';
+
+	const {
+		elements: {
+			root,
+			content,
+			viewport,
+			corner,
+			scrollbarY,
+			thumbY,
+			thumbX,
+			scrollbarX,
+		},
+	} = createScrollArea({
+		type: 'hover',
+		dir: 'ltr',
+	});
+</script>
+
+<div
+	use:melt={$root}
+	class="relative h-72 w-52 overflow-hidden rounded-md border bg-white text-magnum-900 shadow-lg"
+>
+	<div use:melt={$viewport} class="h-full w-full rounded-[inherit]">
+		<div use:melt={$content}>
+			<div class="p-4">
+				<h4 class="mb-4 font-semibold leading-none">Endless Flavors</h4>
+				{#each flavors as flavor (flavor)}
+					<div class="text-sm">
+						{flavor}
+					</div>
+					<div role="separator" class="my-2 h-px w-full bg-magnum-600" />
+				{/each}
+			</div>
+		</div>
+	</div>
+	<div
+		use:melt={$scrollbarY}
+		class="flex h-full w-2.5 touch-none select-none border-l border-l-transparent bg-magnum-800/10 p-px transition-colors"
+	>
+		<div
+			use:melt={$thumbY}
+			class="relative flex-1 rounded-full bg-magnum-600"
+		/>
+	</div>
+	<div
+		use:melt={$scrollbarX}
+		class="flex h-2.5 w-full touch-none select-none border-t border-t-transparent bg-magnum-800/10 p-px"
+	>
+		<div use:melt={$thumbX} class="relative rounded-full bg-magnum-600" />
+	</div>
+	<div use:melt={$corner} />
+</div>
diff --git a/src/docs/previews/scroll-area/scroll/tailwind/flavors.ts b/src/docs/previews/scroll-area/scroll/tailwind/flavors.ts
new file mode 100644
index 00000000..d2ae44fa
--- /dev/null
+++ b/src/docs/previews/scroll-area/scroll/tailwind/flavors.ts
@@ -0,0 +1,32 @@
+export const flavors = [
+	'Vanilla',
+	'Chocolate',
+	'Strawberry',
+	'Mint Chocolate Chip',
+	'Cookies and Cream',
+	'Rocky Road',
+	'Pistachio',
+	'Neapolitan',
+	'Butter Pecan',
+	'Salted Caramel',
+	'Coffee',
+	'Mango',
+	'Raspberry Ripple',
+	'Lemon Sorbet',
+	'Green Tea',
+	'Coconut',
+	'Black Cherry',
+	'Banana',
+	'Almond Fudge',
+	'Cinnamon',
+	'Blueberry Cheesecake',
+	'Tiramisu',
+	'Red Velvet',
+	'Matcha',
+	'Peanut Butter Cup',
+	'Cookie Dough',
+	'Rum Raisin',
+	'Birthday Cake',
+	'Lychee',
+	'Honey Lavender',
+];
diff --git a/src/docs/previews/scroll-area/scroll/tailwind/index.svelte b/src/docs/previews/scroll-area/scroll/tailwind/index.svelte
new file mode 100644
index 00000000..3066072b
--- /dev/null
+++ b/src/docs/previews/scroll-area/scroll/tailwind/index.svelte
@@ -0,0 +1,40 @@
+<script lang="ts">
+	import { createScrollArea, melt } from '$lib/index.js';
+	import { flavors } from './flavors.js';
+
+	const {
+		elements: { root, content, viewport, corner, scrollbarY, thumbY },
+	} = createScrollArea({
+		type: 'scroll',
+		dir: 'ltr',
+	});
+</script>
+
+<div
+	use:melt={$root}
+	class="relative h-72 w-56 overflow-hidden rounded-md border border-neutral-700 bg-neutral-800/90 text-white shadow-lg"
+>
+	<div use:melt={$viewport} class="h-full w-full rounded-[inherit]">
+		<div use:melt={$content}>
+			<div class="p-7">
+				<h4 class="mb-4 font-semibold leading-none">Endless Flavors</h4>
+				{#each flavors as flavor (flavor)}
+					<div class="text-sm">
+						{flavor}
+					</div>
+					<div role="separator" class="my-2 h-px w-full bg-magnum-800/30" />
+				{/each}
+			</div>
+		</div>
+	</div>
+	<div
+		use:melt={$scrollbarY}
+		class="flex h-full w-2.5 touch-none select-none border-l border-l-transparent bg-neutral-300/10 p-px transition-colors"
+	>
+		<div
+			use:melt={$thumbY}
+			class="relative flex-1 rounded-full bg-neutral-300/50"
+		/>
+	</div>
+	<div use:melt={$corner} />
+</div>
diff --git a/src/docs/utils/content.ts b/src/docs/utils/content.ts
index 9fca1509..498bc192 100644
--- a/src/docs/utils/content.ts
+++ b/src/docs/utils/content.ts
@@ -136,3 +136,16 @@ export function elementSchema(name: string, schema: ElementSchema): APISchema {
 		events: customEvents,
 	};
 }
+
+export const floatingSideDataAttr = {
+	name: 'data-side',
+	value: "'top' | 'right' | 'bottom' | 'left'",
+};
+
+export const floatingSideAndAlignDataAttrs = [
+	floatingSideDataAttr,
+	{
+		name: 'data-align',
+		value: "'start' | 'center' | 'end'",
+	},
+];
diff --git a/src/lib/builders/accordion/create.ts b/src/lib/builders/accordion/create.ts
index 3fc78ee2..ddad700b 100644
--- a/src/lib/builders/accordion/create.ts
+++ b/src/lib/builders/accordion/create.ts
@@ -59,9 +59,10 @@ export const createAccordion = <Multiple extends boolean = false>(
 	});
 
 	const root = makeElement(name(), {
-		returned: () => ({
-			'data-melt-id': meltIds.root,
-		}),
+		returned: () =>
+			({
+				'data-melt-id': meltIds.root,
+			} as const),
 	});
 
 	const parseItemProps = (props: AccordionItemProps) => {
@@ -89,7 +90,7 @@ export const createAccordion = <Multiple extends boolean = false>(
 				return {
 					'data-state': isSelected(itemValue, $value) ? 'open' : 'closed',
 					'data-disabled': disabledAttr(disabled),
-				};
+				} as const;
 			};
 		},
 	});
@@ -108,7 +109,7 @@ export const createAccordion = <Multiple extends boolean = false>(
 					'data-disabled': disabledAttr(disabled),
 					'data-value': itemValue,
 					'data-state': isSelected(itemValue, $value) ? 'open' : 'closed',
-				};
+				} as const;
 			};
 		},
 		action: (node: HTMLElement): MeltActionReturn<AccordionEvents['trigger']> => {
@@ -182,7 +183,7 @@ export const createAccordion = <Multiple extends boolean = false>(
 					style: styleToString({
 						display: isVisible ? undefined : 'none',
 					}),
-				};
+				} as const;
 			};
 		},
 		action: (node: HTMLElement) => {
@@ -210,7 +211,7 @@ export const createAccordion = <Multiple extends boolean = false>(
 					role: 'heading',
 					'aria-level': level,
 					'data-heading-level': level,
-				};
+				} as const;
 			};
 		},
 	});
diff --git a/src/lib/builders/avatar/create.ts b/src/lib/builders/avatar/create.ts
index 14be4549..3d2389a0 100644
--- a/src/lib/builders/avatar/create.ts
+++ b/src/lib/builders/avatar/create.ts
@@ -54,7 +54,7 @@ export const createAvatar = (props?: CreateAvatarProps) => {
 			return {
 				src: $src,
 				style: imageStyles,
-			};
+			} as const;
 		},
 	});
 
@@ -69,7 +69,7 @@ export const createAvatar = (props?: CreateAvatarProps) => {
 						  })
 						: undefined,
 				hidden: $loadingStatus === 'loaded' ? true : undefined,
-			};
+			} as const;
 		},
 	});
 
diff --git a/src/lib/builders/calendar/create.ts b/src/lib/builders/calendar/create.ts
index b3073b0a..0f6fd1bd 100644
--- a/src/lib/builders/calendar/create.ts
+++ b/src/lib/builders/calendar/create.ts
@@ -44,7 +44,7 @@ import { derived, writable } from 'svelte/store';
 import type { CalendarEvents } from './events.js';
 import type { CalendarValue, CreateCalendarProps } from './types.js';
 
-const defaults = {
+export const defaults = {
 	isDateDisabled: undefined,
 	isDateUnavailable: undefined,
 	value: undefined,
@@ -286,7 +286,7 @@ export function createCalendar<
 				'data-invalid': $isInvalid ? '' : undefined,
 				'data-disabled': $disabled ? '' : undefined,
 				'data-readonly': $readonly ? '' : undefined,
-			};
+			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<CalendarEvents['calendar']> => {
 			/**
@@ -323,7 +323,7 @@ export function createCalendar<
 			return {
 				'aria-hidden': true,
 				'data-disabled': $disabled ? '' : undefined,
-			};
+			} as const;
 		},
 	});
 
@@ -340,11 +340,11 @@ export function createCalendar<
 			return {
 				tabindex: -1,
 				role: 'grid',
-				'aria-readonly': $readonly ? ('true' as const) : undefined,
-				'aria-disabled': $disabled ? ('true' as const) : undefined,
+				'aria-readonly': $readonly ? 'true' : undefined,
+				'aria-disabled': $disabled ? 'true' : undefined,
 				'data-readonly': $readonly ? '' : undefined,
 				'data-disabled': $disabled ? '' : undefined,
-			};
+			} as const;
 		},
 	});
 
@@ -362,10 +362,10 @@ export function createCalendar<
 				role: 'button',
 				type: 'button' as const,
 				'aria-label': 'Previous',
-				'aria-disabled': disabled ? ('true' as const) : undefined,
+				'aria-disabled': disabled ? 'true' : undefined,
 				'data-disabled': disabled ? '' : undefined,
 				disabled: disabled ? true : undefined,
-			};
+			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<CalendarEvents['prevButton']> => {
 			const unsub = executeCallbacks(
@@ -394,10 +394,10 @@ export function createCalendar<
 				role: 'button',
 				type: 'button' as const,
 				'aria-label': 'Next',
-				'aria-disabled': disabled ? ('true' as const) : undefined,
+				'aria-disabled': disabled ? 'true' : undefined,
 				'data-disabled': disabled ? '' : undefined,
 				disabled: disabled ? true : undefined,
-			};
+			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<CalendarEvents['nextButton']> => {
 			const unsub = executeCallbacks(
@@ -514,20 +514,48 @@ export function createCalendar<
 	 * Updates the displayed months based on changes in the placeholder value,
 	 * which determines the months to show in the calendar.
 	 */
-	effect(
-		[placeholder, weekStartsOn, locale, fixedWeeks, numberOfMonths],
-		([$placeholder, $weekStartsOn, $locale, $fixedWeeks, $numberOfMonths]) => {
-			if (!isBrowser || !$placeholder) return;
+	effect([placeholder], ([$placeholder]) => {
+		if (!isBrowser || !$placeholder) return;
 
-			const $visibleMonths = visibleMonths.get();
+		const $visibleMonths = visibleMonths.get();
 
-			/**
-			 * If the placeholder's month is already in the visible months,
-			 * we don't need to do anything.
-			 */
-			if ($visibleMonths.some((month) => isSameMonth(month, $placeholder))) {
-				return;
-			}
+		/**
+		 * If the placeholder's month is already in the visible months,
+		 * we don't need to do anything.
+		 */
+		if ($visibleMonths.some((month) => isSameMonth(month, $placeholder))) {
+			return;
+		}
+
+		const $weekStartsOn = weekStartsOn.get();
+		const $locale = locale.get();
+		const $fixedWeeks = fixedWeeks.get();
+		const $numberOfMonths = numberOfMonths.get();
+
+		const defaultMonthProps = {
+			weekStartsOn: $weekStartsOn,
+			locale: $locale,
+			fixedWeeks: $fixedWeeks,
+			numberOfMonths: $numberOfMonths,
+		};
+
+		months.set(
+			createMonths({
+				...defaultMonthProps,
+				dateObj: $placeholder,
+			})
+		);
+	});
+
+	/**
+	 * Updates the displayed months based on changes in the the options values,
+	 * which determines the months to show in the calendar.
+	 */
+	effect(
+		[weekStartsOn, locale, fixedWeeks, numberOfMonths],
+		([$weekStartsOn, $locale, $fixedWeeks, $numberOfMonths]) => {
+			const $placeholder = placeholder.get();
+			if (!isBrowser || !$placeholder) return;
 
 			const defaultMonthProps = {
 				weekStartsOn: $weekStartsOn,
diff --git a/src/lib/builders/checkbox/create.ts b/src/lib/builders/checkbox/create.ts
index 3e33ba85..b139d4f1 100644
--- a/src/lib/builders/checkbox/create.ts
+++ b/src/lib/builders/checkbox/create.ts
@@ -72,7 +72,7 @@ export function createCheckbox(props?: CreateCheckboxProps) {
 		stores: [checked, name, value, required, disabled],
 		returned: ([$checked, $name, $value, $required, $disabled]) => {
 			return {
-				type: 'checkbox' as const,
+				type: 'checkbox',
 				'aria-hidden': true,
 				hidden: true,
 				tabindex: -1,
diff --git a/src/lib/builders/collapsible/create.ts b/src/lib/builders/collapsible/create.ts
index 7c0a264b..bd8022bf 100644
--- a/src/lib/builders/collapsible/create.ts
+++ b/src/lib/builders/collapsible/create.ts
@@ -32,10 +32,11 @@ export function createCollapsible(props?: CreateCollapsibleProps) {
 
 	const root = makeElement(name(), {
 		stores: [open, disabled],
-		returned: ([$open, $disabled]) => ({
-			'data-state': $open ? 'open' : 'closed',
-			'data-disabled': disabledAttr($disabled),
-		}),
+		returned: ([$open, $disabled]) =>
+			({
+				'data-state': $open ? 'open' : 'closed',
+				'data-disabled': disabledAttr($disabled),
+			} as const),
 	});
 
 	const trigger = makeElement(name('trigger'), {
@@ -72,7 +73,7 @@ export function createCollapsible(props?: CreateCollapsibleProps) {
 			hidden: $isVisible ? undefined : true,
 			style: styleToString({
 				display: $isVisible ? undefined : 'none',
-			}),
+			} as const),
 		}),
 	});
 
diff --git a/src/lib/builders/context-menu/create.ts b/src/lib/builders/context-menu/create.ts
index f7a0867e..7365de14 100644
--- a/src/lib/builders/context-menu/create.ts
+++ b/src/lib/builders/context-menu/create.ts
@@ -74,18 +74,7 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 	const nextFocusable = withGet.writable<HTMLElement | null>(null);
 	const prevFocusable = withGet.writable<HTMLElement | null>(null);
 
-	const {
-		item,
-		createCheckboxItem,
-		arrow,
-		createSubmenu,
-		createMenuRadioGroup,
-		ids,
-		separator,
-		handleTypeaheadSearch,
-		group,
-		groupLabel,
-	} = createMenuBuilder({
+	const { elements, builders, ids, options, helpers, states } = createMenuBuilder({
 		rootOpen,
 		rootOptions,
 		rootActiveTrigger: withGet(rootActiveTrigger),
@@ -96,6 +85,8 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 		ids: withDefaults.ids,
 	});
 
+	const { handleTypeaheadSearch } = helpers;
+
 	const point = writable<Point | null>(null);
 	const virtual: WithGet<Readable<VirtualElement | null>> = withGet(
 		derived([point], ([$point]) => {
@@ -168,9 +159,10 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 					unsubPopper();
 					if (!$isVisible || !$rootActiveTrigger) return;
 					tick().then(() => {
+						unsubPopper();
 						setMeltMenuAttribute(node, selector);
 						const $virtual = virtual.get();
-						const popper = usePopper(node, {
+						unsubPopper = usePopper(node, {
 							anchorElement: $virtual ? $virtual : $rootActiveTrigger,
 							open: rootOpen,
 							options: {
@@ -186,9 +178,7 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 								portal: getPortalDestination(node, $portal),
 								escapeKeydown: $closeOnEscape ? undefined : null,
 							},
-						});
-						if (!popper || !popper.destroy) return;
-						unsubPopper = popper.destroy;
+						}).destroy;
 					});
 				}
 			);
@@ -317,23 +307,13 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 	return {
 		ids,
 		elements: {
+			...elements,
 			menu,
 			trigger,
-			item,
-			arrow,
-			separator,
-			group,
-			groupLabel,
-		},
-		states: {
-			open: rootOpen,
-		},
-		builders: {
-			createSubmenu,
-			createCheckboxItem,
-			createMenuRadioGroup,
 		},
-		options: rootOptions,
+		states,
+		builders,
+		options,
 	};
 }
 
diff --git a/src/lib/builders/date-field/create.ts b/src/lib/builders/date-field/create.ts
index 7f761e1f..1052352f 100644
--- a/src/lib/builders/date-field/create.ts
+++ b/src/lib/builders/date-field/create.ts
@@ -274,7 +274,7 @@ export function createDateField(props?: CreateDateFieldProps) {
 				id: $labelId,
 				'data-invalid': $isInvalid ? '' : undefined,
 				'data-disabled': $disabled ? '' : undefined,
-			};
+			} as const;
 		},
 		action: (node: HTMLElement) => {
 			const unsub = executeCallbacks(
@@ -307,7 +307,7 @@ export function createDateField(props?: CreateDateFieldProps) {
 				id: $validationId,
 				'data-invalid': $isInvalid ? '' : undefined,
 				style: $isInvalid ? undefined : validStyle,
-			};
+			} as const;
 		},
 	});
 
@@ -346,11 +346,11 @@ export function createDateField(props?: CreateDateFieldProps) {
 				id: $ids.field,
 				'aria-labelledby': $ids.label,
 				'aria-describedby': describedBy,
-				'aria-disabled': $disabled ? ('true' as const) : undefined,
-				'aria-readonly': $readonly ? ('true' as const) : undefined,
+				'aria-disabled': $disabled ? 'true' : undefined,
+				'aria-readonly': $readonly ? 'true' : undefined,
 				'data-invalid': $isInvalid ? '' : undefined,
 				'data-disabled': $disabled ? '' : undefined,
-			};
+			} as const;
 		},
 		// even if we don't need the element we need to specify it
 		// or TS will complain when svelte tries to pass it
@@ -443,9 +443,9 @@ export function createDateField(props?: CreateDateFieldProps) {
 				const inReadonlySegments = $readonlySegmentsSet.has(part);
 				const defaultAttrs = {
 					...getSegmentAttrs(part, props),
-					'aria-invalid': $isInvalid ? ('true' as const) : undefined,
-					'aria-disabled': $disabled ? ('true' as const) : undefined,
-					'aria-readonly': $readonly || inReadonlySegments ? ('true' as const) : undefined,
+					'aria-invalid': $isInvalid ? 'true' : undefined,
+					'aria-disabled': $disabled ? 'true' : undefined,
+					'aria-readonly': $readonly || inReadonlySegments ? 'true' : undefined,
 					'data-invalid': $isInvalid ? '' : undefined,
 					'data-disabled': $disabled ? '' : undefined,
 					'data-segment': `${part}`,
@@ -466,7 +466,7 @@ export function createDateField(props?: CreateDateFieldProps) {
 					contenteditable: $readonly || inReadonlySegments || $disabled ? false : true,
 					'aria-describedby': describedBy,
 					tabindex: $disabled ? undefined : 0,
-				};
+				} as const;
 			};
 		},
 		action: (node: HTMLElement): MeltActionReturn<DateFieldEvents['segment']> =>
diff --git a/src/lib/builders/date-picker/create.ts b/src/lib/builders/date-picker/create.ts
index 3adbfce7..2bb4e2aa 100644
--- a/src/lib/builders/date-picker/create.ts
+++ b/src/lib/builders/date-picker/create.ts
@@ -16,6 +16,7 @@ import { pickerOpenFocus } from '$lib/internal/helpers/date/focus.js';
 import { createFormatter, dateStore, getDefaultDate } from '$lib/internal/helpers/date/index.js';
 import type { MeltActionReturn } from '$lib/internal/types.js';
 import type { DatePickerEvents } from './events.js';
+import { defaults as calendarDefaults } from '../calendar/create.js';
 
 const defaults = {
 	isDateDisabled: undefined,
@@ -36,6 +37,18 @@ const defaults = {
 	minValue: undefined,
 	maxValue: undefined,
 	weekdayFormat: 'narrow',
+	...omit(
+		calendarDefaults,
+		'isDateDisabled',
+		'isDateUnavailable',
+		'value',
+		'locale',
+		'disabled',
+		'readonly',
+		'minValue',
+		'maxValue',
+		'weekdayFormat'
+	),
 } satisfies CreateDatePickerProps;
 
 export function createDatePicker(props?: CreateDatePickerProps) {
@@ -83,7 +96,7 @@ export function createDatePicker(props?: CreateDatePickerProps) {
 				'aria-label': 'Open date picker',
 				'data-segment': 'trigger',
 				disabled: $disabled ? true : undefined,
-			};
+			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<DatePickerEvents['trigger']> => {
 			const unsubKeydown = addMeltEventListener(node, 'keydown', handleTriggerKeydown);
@@ -131,6 +144,18 @@ export function createDatePicker(props?: CreateDatePickerProps) {
 		calendar.options.maxValue.set($maxValue);
 	});
 
+	effect([options.numberOfMonths], ([$numberOfMonths]) => {
+		calendar.options.numberOfMonths.set($numberOfMonths);
+	});
+
+	effect([options.fixedWeeks], ([$fixedWeeks]) => {
+		calendar.options.fixedWeeks.set($fixedWeeks);
+	});
+
+	effect([options.weekStartsOn], ([$weekStartsOn]) => {
+		calendar.options.weekStartsOn.set($weekStartsOn);
+	});
+
 	const dateFieldOptions = omit(
 		dateField.options,
 		'locale',
diff --git a/src/lib/builders/date-range-field/create.ts b/src/lib/builders/date-range-field/create.ts
index 9997b79e..b003c376 100644
--- a/src/lib/builders/date-range-field/create.ts
+++ b/src/lib/builders/date-range-field/create.ts
@@ -182,7 +182,7 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 				id: $labelId,
 				'data-invalid': $isInvalid ? '' : undefined,
 				'data-disabled': $disabled ? '' : undefined,
-			};
+			} as const;
 		},
 		action: (node: HTMLElement) => {
 			const unsub = executeCallbacks(
@@ -229,7 +229,7 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 				'aria-labelledby': $ids.label,
 				'aria-describedby': describedBy,
 				'data-invalid': $isInvalid ? '' : undefined,
-			};
+			} as const;
 		},
 		action: () => {
 			getAnnouncer();
@@ -252,7 +252,7 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 				id: $validationId,
 				'data-invalid': $isInvalid ? '' : undefined,
 				style: $isInvalid ? undefined : validStyle,
-			};
+			} as const;
 		},
 	});
 
diff --git a/src/lib/builders/date-range-picker/create.ts b/src/lib/builders/date-range-picker/create.ts
index 5f046587..66664ca4 100644
--- a/src/lib/builders/date-range-picker/create.ts
+++ b/src/lib/builders/date-range-picker/create.ts
@@ -95,7 +95,7 @@ export function createDateRangePicker(props?: CreateDateRangePickerProps) {
 				'aria-label': 'Open date picker',
 				'data-segment': 'trigger',
 				disabled: $disabled ? true : undefined,
-			};
+			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<DateRangePickerEvents['trigger']> => {
 			const unsubKeydown = addMeltEventListener(node, 'keydown', handleTriggerKeydown);
diff --git a/src/lib/builders/dialog/create.ts b/src/lib/builders/dialog/create.ts
index 9f028bb2..24e43af8 100644
--- a/src/lib/builders/dialog/create.ts
+++ b/src/lib/builders/dialog/create.ts
@@ -171,14 +171,14 @@ export function createDialog(props?: CreateDialogProps) {
 				role: role.get(),
 				'aria-describedby': $descriptionId,
 				'aria-labelledby': $titleId,
-				'aria-modal': $isVisible ? ('true' as const) : undefined,
+				'aria-modal': $isVisible ? 'true' : undefined,
 				'data-state': $open ? 'open' : 'closed',
 				tabindex: -1,
 				hidden: $isVisible ? undefined : true,
 				style: styleToString({
 					display: $isVisible ? undefined : 'none',
 				}),
-			};
+			} as const;
 		},
 
 		action: (node: HTMLElement) => {
@@ -186,26 +186,30 @@ export function createDialog(props?: CreateDialogProps) {
 			let deactivate = noop;
 
 			const destroy = executeCallbacks(
-				effect([open], ([$open]) => {
-					if (!$open) return;
-
-					const focusTrap = createFocusTrap({
-						immediate: false,
-						escapeDeactivates: true,
-						clickOutsideDeactivates: true,
-						returnFocusOnDeactivate: false,
-						fallbackFocus: node,
-					});
-
-					activate = focusTrap.activate;
-					deactivate = focusTrap.deactivate;
-					const ac = focusTrap.useFocusTrap(node);
-					if (ac && ac.destroy) {
-						return ac.destroy;
-					} else {
-						return focusTrap.deactivate;
+				effect(
+					[open, closeOnOutsideClick, closeOnEscape],
+					([$open, $closeOnOutsideClick, $closeOnEscape]) => {
+						if (!$open) return;
+
+						const focusTrap = createFocusTrap({
+							immediate: false,
+							escapeDeactivates: $closeOnEscape,
+							clickOutsideDeactivates: $closeOnOutsideClick,
+							allowOutsideClick: true,
+							returnFocusOnDeactivate: false,
+							fallbackFocus: node,
+						});
+
+						activate = focusTrap.activate;
+						deactivate = focusTrap.deactivate;
+						const ac = focusTrap.useFocusTrap(node);
+						if (ac && ac.destroy) {
+							return ac.destroy;
+						} else {
+							return focusTrap.deactivate;
+						}
 					}
-				}),
+				),
 				effect([closeOnOutsideClick, open], ([$closeOnOutsideClick, $open]) => {
 					return useModal(node, {
 						open: $open,
@@ -223,15 +227,7 @@ export function createDialog(props?: CreateDialogProps) {
 				effect([closeOnEscape], ([$closeOnEscape]) => {
 					if (!$closeOnEscape) return noop;
 
-					const escapeKeydown = useEscapeKeydown(node, {
-						handler: () => {
-							handleClose();
-						},
-					});
-					if (escapeKeydown && escapeKeydown.destroy) {
-						return escapeKeydown.destroy;
-					}
-					return noop;
+					return useEscapeKeydown(node, { handler: handleClose }).destroy;
 				}),
 
 				effect([isVisible], ([$isVisible]) => {
@@ -256,20 +252,16 @@ export function createDialog(props?: CreateDialogProps) {
 
 	const portalled = makeElement(name('portalled'), {
 		stores: portal,
-		returned: ($portal) => ({
-			'data-portal': portalAttr($portal),
-		}),
+		returned: ($portal) =>
+			({
+				'data-portal': portalAttr($portal),
+			} as const),
 		action: (node: HTMLElement) => {
 			const unsubPortal = effect([portal], ([$portal]) => {
 				if ($portal === null) return noop;
 				const portalDestination = getPortalDestination(node, $portal);
 				if (portalDestination === null) return noop;
-				const portalAction = usePortal(node, portalDestination);
-				if (portalAction && portalAction.destroy) {
-					return portalAction.destroy;
-				} else {
-					return noop;
-				}
+				return usePortal(node, portalDestination).destroy;
 			});
 
 			return {
@@ -282,16 +274,18 @@ export function createDialog(props?: CreateDialogProps) {
 
 	const title = makeElement(name('title'), {
 		stores: [ids.title],
-		returned: ([$titleId]) => ({
-			id: $titleId,
-		}),
+		returned: ([$titleId]) =>
+			({
+				id: $titleId,
+			} as const),
 	});
 
 	const description = makeElement(name('description'), {
 		stores: [ids.description],
-		returned: ([$descriptionId]) => ({
-			id: $descriptionId,
-		}),
+		returned: ([$descriptionId]) =>
+			({
+				id: $descriptionId,
+			} as const),
 	});
 
 	const close = makeElement(name('close'), {
diff --git a/src/lib/builders/dropdown-menu/create.ts b/src/lib/builders/dropdown-menu/create.ts
index b95bc935..bb878a57 100644
--- a/src/lib/builders/dropdown-menu/create.ts
+++ b/src/lib/builders/dropdown-menu/create.ts
@@ -37,19 +37,7 @@ export function createDropdownMenu(props?: CreateDropdownMenuProps) {
 	const nextFocusable = withGet(writable<HTMLElement | null>(null));
 	const prevFocusable = withGet(writable<HTMLElement | null>(null));
 
-	const {
-		trigger,
-		menu,
-		item,
-		arrow,
-		createSubmenu,
-		createCheckboxItem,
-		createMenuRadioGroup,
-		separator,
-		group,
-		groupLabel,
-		ids,
-	} = createMenuBuilder({
+	const { elements, builders, ids, states, options } = createMenuBuilder({
 		rootOptions,
 		rootOpen,
 		rootActiveTrigger: withGet(rootActiveTrigger),
@@ -62,23 +50,9 @@ export function createDropdownMenu(props?: CreateDropdownMenuProps) {
 
 	return {
 		ids,
-		elements: {
-			trigger,
-			menu,
-			item,
-			arrow,
-			separator,
-			group,
-			groupLabel,
-		},
-		states: {
-			open: rootOpen,
-		},
-		builders: {
-			createCheckboxItem,
-			createSubmenu,
-			createMenuRadioGroup,
-		},
-		options: rootOptions,
+		elements,
+		states,
+		builders,
+		options,
 	};
 }
diff --git a/src/lib/builders/hidden-input/create.ts b/src/lib/builders/hidden-input/create.ts
index 274ba916..2c9d73bc 100644
--- a/src/lib/builders/hidden-input/create.ts
+++ b/src/lib/builders/hidden-input/create.ts
@@ -29,7 +29,7 @@ export function createHiddenInput(props: CreateHiddenInputProps) {
 			return {
 				name: $name,
 				value: $value?.toString(),
-				'aria-hidden': 'true' as const,
+				'aria-hidden': 'true',
 				hidden: true,
 				disabled: $disabled,
 				required: $required,
@@ -41,7 +41,7 @@ export function createHiddenInput(props: CreateHiddenInputProps) {
 					margin: 0,
 					transform: 'translateX(-100%)',
 				}),
-			};
+			} as const;
 		},
 		action: (node: HTMLInputElement) => {
 			// When value changes, emit a change event
diff --git a/src/lib/builders/index.ts b/src/lib/builders/index.ts
index 251e81ea..94b8dd9d 100644
--- a/src/lib/builders/index.ts
+++ b/src/lib/builders/index.ts
@@ -20,6 +20,7 @@ export * from './popover/index.js';
 export * from './progress/index.js';
 export * from './radio-group/index.js';
 export * from './range-calendar/index.js';
+export * from './scroll-area/index.js';
 export * from './select/index.js';
 export * from './separator/index.js';
 export * from './slider/index.js';
diff --git a/src/lib/builders/link-preview/create.ts b/src/lib/builders/link-preview/create.ts
index bb013909..53c15b18 100644
--- a/src/lib/builders/link-preview/create.ts
+++ b/src/lib/builders/link-preview/create.ts
@@ -28,6 +28,7 @@ import { generateIds } from '../../internal/helpers/id.js';
 import { omit } from '../../internal/helpers/object.js';
 import type { LinkPreviewEvents } from './events.js';
 import type { CreateLinkPreviewProps } from './types.js';
+import { tick } from 'svelte';
 
 type LinkPreviewParts = 'trigger' | 'content' | 'arrow';
 const { name } = createElHelpers<LinkPreviewParts>('hover-card');
@@ -115,13 +116,13 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 		stores: [open, ids.trigger, ids.content],
 		returned: ([$open, $triggerId, $contentId]) => {
 			return {
-				role: 'button' as const,
-				'aria-haspopup': 'dialog' as const,
+				role: 'button',
+				'aria-haspopup': 'dialog',
 				'aria-expanded': $open,
 				'data-state': $open ? 'open' : 'closed',
 				'aria-controls': $contentId,
 				id: $triggerId,
-			};
+			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<LinkPreviewEvents['trigger']> => {
 			const unsub = executeCallbacks(
@@ -163,7 +164,7 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 				id: $contentId,
 				'data-state': $isVisible ? 'open' : 'closed',
 				'data-portal': portalAttr($portal),
-			};
+			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<LinkPreviewEvents['content']> => {
 			let unsub = noop;
@@ -189,35 +190,37 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 					unsubPopper();
 					if (!$isVisible || !$activeTrigger) return;
 
-					const popper = usePopper(node, {
-						anchorElement: $activeTrigger,
-						open: open,
-						options: {
-							floating: $positioning,
-							modal: {
-								closeOnInteractOutside: $closeOnOutsideClick,
-								onClose: () => {
-									open.set(false);
-									$activeTrigger.focus();
+					tick().then(() => {
+						unsubPopper();
+						unsubPopper = usePopper(node, {
+							anchorElement: $activeTrigger,
+							open: open,
+							options: {
+								floating: $positioning,
+								modal: {
+									closeOnInteractOutside: $closeOnOutsideClick,
+									onClose: () => {
+										open.set(false);
+										$activeTrigger.focus();
+									},
+									shouldCloseOnInteractOutside: (e) => {
+										onOutsideClick.get()?.(e);
+										if (e.defaultPrevented) return false;
+										if (
+											isHTMLElement($activeTrigger) &&
+											$activeTrigger.contains(e.target as Element)
+										)
+											return false;
+										return true;
+									},
+									open: $isVisible,
 								},
-								shouldCloseOnInteractOutside: (e) => {
-									onOutsideClick.get()?.(e);
-									if (e.defaultPrevented) return false;
-									if (isHTMLElement($activeTrigger) && $activeTrigger.contains(e.target as Element))
-										return false;
-									return true;
-								},
-								open: $isVisible,
+								portal: getPortalDestination(node, $portal),
+								focusTrap: null,
+								escapeKeydown: $closeOnEscape ? undefined : null,
 							},
-							portal: getPortalDestination(node, $portal),
-							focusTrap: null,
-							escapeKeydown: $closeOnEscape ? undefined : null,
-						},
+						}).destroy;
 					});
-
-					if (popper && popper.destroy) {
-						unsubPopper = popper.destroy;
-					}
 				}
 			);
 
@@ -260,14 +263,15 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 
 	const arrow = makeElement(name('arrow'), {
 		stores: arrowSize,
-		returned: ($arrowSize) => ({
-			'data-arrow': true,
-			style: styleToString({
-				position: 'absolute',
-				width: `var(--arrow-size, ${$arrowSize}px)`,
-				height: `var(--arrow-size, ${$arrowSize}px)`,
-			}),
-		}),
+		returned: ($arrowSize) =>
+			({
+				'data-arrow': true,
+				style: styleToString({
+					position: 'absolute',
+					width: `var(--arrow-size, ${$arrowSize}px)`,
+					height: `var(--arrow-size, ${$arrowSize}px)`,
+				}),
+			} as const),
 	});
 
 	effect([containSelection], ([$containSelection]) => {
diff --git a/src/lib/builders/listbox/create.ts b/src/lib/builders/listbox/create.ts
index f15a19fe..31be7d0d 100644
--- a/src/lib/builders/listbox/create.ts
+++ b/src/lib/builders/listbox/create.ts
@@ -32,7 +32,6 @@ import {
 	prev,
 	removeHighlight,
 	removeScroll,
-	sleep,
 	stripValues,
 	styleToString,
 	toWritableStores,
@@ -213,7 +212,7 @@ export function createListbox<
 		if (!triggerEl) return;
 
 		// The active trigger is used to anchor the menu to the input element.
-		activeTrigger.set(triggerEl);
+		if (triggerEl !== activeTrigger.get()) activeTrigger.set(triggerEl);
 
 		// Wait a tick for the menu to open then highlight the selected item.
 		await tick();
@@ -227,8 +226,7 @@ export function createListbox<
 	}
 
 	/** Closes the menu & clears the active trigger */
-	async function closeMenu() {
-		await sleep(0);
+	function closeMenu() {
 		open.set(false);
 		highlightedItem.set(null);
 	}
@@ -368,7 +366,10 @@ export function createListbox<
 						return;
 					}
 					// Pressing enter with a highlighted item should select it.
-					if (e.key === kbd.ENTER || (e.key === kbd.SPACE && isHTMLButtonElement(node))) {
+					if (
+						(e.key === kbd.ENTER && !e.isComposing) ||
+						(e.key === kbd.SPACE && isHTMLButtonElement(node))
+					) {
 						e.preventDefault();
 						const $highlightedItem = highlightedItem.get();
 						if ($highlightedItem) {
@@ -481,39 +482,39 @@ export function createListbox<
 
 						if (!$isVisible || !$activeTrigger) return;
 
-						const ignoreHandler = createClickOutsideIgnore(ids.trigger.get());
-
-						const popper = usePopper(node, {
-							anchorElement: $activeTrigger,
-							open,
-							options: {
-								floating: $positioning,
-								focusTrap: null,
-								modal: {
-									closeOnInteractOutside: $closeOnOutsideClick,
-									onClose: closeMenu,
-									open: $isVisible,
-									shouldCloseOnInteractOutside: (e) => {
-										onOutsideClick.get()?.(e);
-										if (e.defaultPrevented) return false;
-										const target = e.target;
-										if (!isElement(target)) return false;
-										if (target === $activeTrigger || $activeTrigger.contains(target)) {
-											return false;
-										}
-										// return opposite of the result of the ignoreHandler
-										if (ignoreHandler(e)) return false;
-										return true;
+						tick().then(() => {
+							unsubPopper();
+							const ignoreHandler = createClickOutsideIgnore(ids.trigger.get());
+
+							unsubPopper = usePopper(node, {
+								anchorElement: $activeTrigger,
+								open,
+								options: {
+									floating: $positioning,
+									focusTrap: null,
+									modal: {
+										closeOnInteractOutside: $closeOnOutsideClick,
+										onClose: closeMenu,
+										open: $isVisible,
+										shouldCloseOnInteractOutside: (e) => {
+											onOutsideClick.get()?.(e);
+											if (e.defaultPrevented) return false;
+											const target = e.target;
+											if (!isElement(target)) return false;
+											if (target === $activeTrigger || $activeTrigger.contains(target)) {
+												return false;
+											}
+											// return opposite of the result of the ignoreHandler
+											if (ignoreHandler(e)) return false;
+											return true;
+										},
 									},
-								},
 
-								escapeKeydown: null,
-								portal: getPortalDestination(node, $portal),
-							},
+									escapeKeydown: null,
+									portal: getPortalDestination(node, $portal),
+								},
+							}).destroy;
 						});
-						if (popper && popper.destroy) {
-							unsubPopper = popper.destroy;
-						}
 					}
 				)
 			);
@@ -538,7 +539,7 @@ export function createListbox<
 			return {
 				id: $labelId,
 				for: $triggerId,
-			};
+			} as const;
 		},
 		action: labelAction,
 	});
@@ -596,18 +597,20 @@ export function createListbox<
 
 	const group = makeElement(name('group'), {
 		returned: () => {
-			return (groupId: string) => ({
-				role: 'group',
-				'aria-labelledby': groupId,
-			});
+			return (groupId: string) =>
+				({
+					role: 'group',
+					'aria-labelledby': groupId,
+				} as const);
 		},
 	});
 
 	const groupLabel = makeElement(name('group-label'), {
 		returned: () => {
-			return (groupId: string) => ({
-				id: groupId,
-			});
+			return (groupId: string) =>
+				({
+					id: groupId,
+				} as const);
 		},
 	});
 
@@ -623,14 +626,15 @@ export function createListbox<
 
 	const arrow = makeElement(name('arrow'), {
 		stores: arrowSize,
-		returned: ($arrowSize) => ({
-			'data-arrow': true,
-			style: styleToString({
-				position: 'absolute',
-				width: `var(--arrow-size, ${$arrowSize}px)`,
-				height: `var(--arrow-size, ${$arrowSize}px)`,
-			}),
-		}),
+		returned: ($arrowSize) =>
+			({
+				'data-arrow': true,
+				style: styleToString({
+					position: 'absolute',
+					width: `var(--arrow-size, ${$arrowSize}px)`,
+					height: `var(--arrow-size, ${$arrowSize}px)`,
+				}),
+			} as const),
 	});
 
 	/* ------------------- */
diff --git a/src/lib/builders/listbox/types.ts b/src/lib/builders/listbox/types.ts
index a35c9a01..550b0ef5 100644
--- a/src/lib/builders/listbox/types.ts
+++ b/src/lib/builders/listbox/types.ts
@@ -30,7 +30,7 @@ export type CreateListboxProps<
 
 	/**
 	 * The size of the arrow in pixels.
-	 * @default 8
+	 * @default undefined
 	 */
 	arrowSize?: number;
 
diff --git a/src/lib/builders/menu/create.ts b/src/lib/builders/menu/create.ts
index c42e929f..9c013dbd 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 { usePopper } from '$lib/internal/actions/index.js';
+import { useEscapeKeydown, usePopper, usePortal } from '$lib/internal/actions/index.js';
 import {
 	FIRST_LAST_KEYS,
 	SELECTION_KEYS,
@@ -185,8 +185,9 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 					unsubPopper();
 					if (!$isVisible || !$rootActiveTrigger) return;
 					tick().then(() => {
+						unsubPopper();
 						setMeltMenuAttribute(node, selector);
-						const popper = usePopper(node, {
+						unsubPopper = usePopper(node, {
 							anchorElement: $rootActiveTrigger,
 							open: rootOpen,
 							options: {
@@ -214,11 +215,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 								portal: getPortalDestination(node, $portal),
 								escapeKeydown: $closeOnEscape ? undefined : null,
 							},
-						});
-
-						if (popper && popper.destroy) {
-							unsubPopper = popper.destroy;
-						}
+						}).destroy;
 					});
 				}
 			);
@@ -326,14 +323,60 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 
 	const rootArrow = makeElement(name('arrow'), {
 		stores: arrowSize,
-		returned: ($arrowSize) => ({
-			'data-arrow': true,
-			style: styleToString({
-				position: 'absolute',
-				width: `var(--arrow-size, ${$arrowSize}px)`,
-				height: `var(--arrow-size, ${$arrowSize}px)`,
-			}),
-		}),
+		returned: ($arrowSize) =>
+			({
+				'data-arrow': true,
+				style: styleToString({
+					position: 'absolute',
+					width: `var(--arrow-size, ${$arrowSize}px)`,
+					height: `var(--arrow-size, ${$arrowSize}px)`,
+				}),
+			} as const),
+	});
+
+	const overlay = makeElement(name('overlay'), {
+		stores: [isVisible],
+		returned: ([$isVisible]) => {
+			return {
+				hidden: $isVisible ? undefined : true,
+				tabindex: -1,
+				style: styleToString({
+					display: $isVisible ? undefined : 'none',
+				}),
+				'aria-hidden': 'true',
+				'data-state': stateAttr($isVisible),
+			} as const;
+		},
+		action: (node: HTMLElement) => {
+			let unsubEscapeKeydown = noop;
+
+			if (closeOnEscape.get()) {
+				const escapeKeydown = useEscapeKeydown(node, {
+					handler: () => {
+						rootOpen.set(false);
+						const $rootActiveTrigger = rootActiveTrigger.get();
+						if ($rootActiveTrigger) $rootActiveTrigger.focus();
+					},
+				});
+				if (escapeKeydown && escapeKeydown.destroy) {
+					unsubEscapeKeydown = escapeKeydown.destroy;
+				}
+			}
+
+			const unsubPortal = effect([portal], ([$portal]) => {
+				if ($portal === null) return noop;
+				const portalDestination = getPortalDestination(node, $portal);
+				if (portalDestination === null) return noop;
+				return usePortal(node, portalDestination).destroy;
+			});
+
+			return {
+				destroy() {
+					unsubEscapeKeydown();
+					unsubPortal();
+				},
+			};
+		},
 	});
 
 	const item = makeElement(name('item'), {
@@ -342,7 +385,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 				role: 'menuitem',
 				tabindex: -1,
 				'data-orientation': 'vertical',
-			};
+			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<MenuEvents['item']> => {
 			setMeltMenuAttribute(node, selector);
@@ -402,18 +445,20 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 
 	const group = makeElement(name('group'), {
 		returned: () => {
-			return (groupId: string) => ({
-				role: 'group',
-				'aria-labelledby': groupId,
-			});
+			return (groupId: string) =>
+				({
+					role: 'group',
+					'aria-labelledby': groupId,
+				} as const);
 		},
 	});
 
 	const groupLabel = makeElement(name('group-label'), {
 		returned: () => {
-			return (groupId: string) => ({
-				id: groupId,
-			});
+			return (groupId: string) =>
+				({
+					id: groupId,
+				} as const);
 		},
 	});
 
@@ -536,9 +581,10 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		const value = overridable(valueWritable, args.onValueChange);
 
 		const radioGroup = makeElement(name('radio-group'), {
-			returned: () => ({
-				role: 'group',
-			}),
+			returned: () =>
+				({
+					role: 'group',
+				} as const),
 		});
 
 		const radioItemDefaults = {
@@ -561,7 +607,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 						'data-value': itemValue,
 						'data-orientation': 'vertical',
 						tabindex: -1,
-					};
+					} as const;
 				};
 			},
 			action: (node: HTMLElement): MeltActionReturn<MenuEvents['radioItem']> => {
@@ -742,9 +788,10 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 						const activeTrigger = subActiveTrigger.get();
 						if (!activeTrigger) return;
 						tick().then(() => {
+							unsubPopper();
 							const parentMenuEl = getParentMenu(activeTrigger);
 
-							const popper = usePopper(node, {
+							unsubPopper = usePopper(node, {
 								anchorElement: activeTrigger,
 								open: subOpen,
 								options: {
@@ -754,11 +801,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 									focusTrap: null,
 									escapeKeydown: null,
 								},
-							});
-
-							if (popper && popper.destroy) {
-								unsubPopper = popper.destroy;
-							}
+							}).destroy;
 						});
 					}
 				);
@@ -1025,14 +1068,15 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 
 		const subArrow = makeElement(name('subarrow'), {
 			stores: arrowSize,
-			returned: ($arrowSize) => ({
-				'data-arrow': true,
-				style: styleToString({
-					position: 'absolute',
-					width: `var(--arrow-size, ${$arrowSize}px)`,
-					height: `var(--arrow-size, ${$arrowSize}px)`,
-				}),
-			}),
+			returned: ($arrowSize) =>
+				({
+					'data-arrow': true,
+					style: styleToString({
+						position: 'absolute',
+						width: `var(--arrow-size, ${$arrowSize}px)`,
+						height: `var(--arrow-size, ${$arrowSize}px)`,
+					}),
+				} as const),
 		});
 
 		/* -------------------------------------------------------------------------------------------------
@@ -1368,20 +1412,29 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 	}
 
 	return {
+		elements: {
+			trigger: rootTrigger,
+			menu: rootMenu,
+			overlay,
+			item,
+			group,
+			groupLabel,
+			arrow: rootArrow,
+			separator,
+		},
+		builders: {
+			createCheckboxItem,
+			createSubmenu,
+			createMenuRadioGroup,
+		},
+		states: {
+			open: rootOpen,
+		},
+		helpers: {
+			handleTypeaheadSearch,
+		},
 		ids: rootIds,
-		trigger: rootTrigger,
-		menu: rootMenu,
-		open: rootOpen,
-		item,
-		group,
-		groupLabel,
-		arrow: rootArrow,
 		options: opts.rootOptions,
-		createCheckboxItem,
-		createSubmenu,
-		createMenuRadioGroup,
-		separator,
-		handleTypeaheadSearch,
 	};
 }
 
@@ -1559,3 +1612,7 @@ function isFocusWithinSubmenu(submenuId: string) {
 	const submenuEl = activeEl.closest(`[data-id="${submenuId}"]`);
 	return isHTMLElement(submenuEl);
 }
+
+function stateAttr(open: boolean) {
+	return open ? 'open' : 'closed';
+}
diff --git a/src/lib/builders/menu/types.ts b/src/lib/builders/menu/types.ts
index 16a0fbde..3d620b2f 100644
--- a/src/lib/builders/menu/types.ts
+++ b/src/lib/builders/menu/types.ts
@@ -210,6 +210,7 @@ export type _MenuParts =
 	| 'arrow'
 	| 'checkbox-item'
 	| 'item'
+	| 'overlay'
 	| 'radio-group'
 	| 'radio-item'
 	| 'submenu'
diff --git a/src/lib/builders/menubar/create.ts b/src/lib/builders/menubar/create.ts
index c109fd1a..9777b3cb 100644
--- a/src/lib/builders/menubar/create.ts
+++ b/src/lib/builders/menubar/create.ts
@@ -163,7 +163,8 @@ export function createMenubar(props?: CreateMenubarProps) {
 						if (!($rootOpen && $rootActiveTrigger)) return;
 
 						tick().then(() => {
-							const popper = usePopper(node, {
+							unsubPopper();
+							unsubPopper = usePopper(node, {
 								anchorElement: $rootActiveTrigger,
 								open: rootOpen,
 								options: {
@@ -186,11 +187,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 										open: $rootOpen,
 									},
 								},
-							});
-
-							if (popper && popper.destroy) {
-								unsubPopper = popper.destroy;
-							}
+							}).destroy;
 						});
 					}
 				);
@@ -234,7 +231,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 						const isCharacterKey = e.key.length === 1;
 						const isModifierKey = e.ctrlKey || e.altKey || e.metaKey;
 						if (!isModifierKey && isCharacterKey) {
-							m.handleTypeaheadSearch(e.key, getMenuItems(menuEl));
+							m.helpers.handleTypeaheadSearch(e.key, getMenuItems(menuEl));
 						}
 					})
 				);
@@ -410,22 +407,12 @@ export function createMenubar(props?: CreateMenubarProps) {
 		return {
 			ids: m.ids,
 			elements: {
+				...m.elements,
 				menu,
 				trigger,
-				item: m.item,
-				arrow: m.arrow,
-				separator: m.separator,
-				group: m.group,
-				groupLabel: m.groupLabel,
-			},
-			builders: {
-				createCheckboxItem: m.createCheckboxItem,
-				createSubmenu: m.createSubmenu,
-				createMenuRadioGroup: m.createMenuRadioGroup,
-			},
-			states: {
-				open: rootOpen,
 			},
+			builders: m.builders,
+			states: m.states,
 			options,
 		};
 	};
diff --git a/src/lib/builders/pagination/create.ts b/src/lib/builders/pagination/create.ts
index 0fc758b9..f5952823 100644
--- a/src/lib/builders/pagination/create.ts
+++ b/src/lib/builders/pagination/create.ts
@@ -46,9 +46,10 @@ export function createPagination(props: CreatePaginationProps) {
 	});
 
 	const root = makeElement(name(), {
-		returned: () => ({
-			'data-scope': 'pagination',
-		}),
+		returned: () =>
+			({
+				'data-scope': 'pagination',
+			} as const),
 	});
 
 	const pages = derived([page, totalPages, siblingCount], ([$page, $totalPages, $siblingCount]) => {
@@ -99,7 +100,7 @@ export function createPagination(props: CreatePaginationProps) {
 					'aria-label': `Page ${pageItem.value}`,
 					'data-value': pageItem.value,
 					'data-selected': pageItem.value === $page ? '' : undefined,
-				};
+				} as const;
 			};
 		},
 		action: (node: HTMLElement): MeltActionReturn<PaginationEvents['pageTrigger']> => {
diff --git a/src/lib/builders/pin-input/create.ts b/src/lib/builders/pin-input/create.ts
index ee780cff..4e8f774d 100644
--- a/src/lib/builders/pin-input/create.ts
+++ b/src/lib/builders/pin-input/create.ts
@@ -68,7 +68,7 @@ export function createPinInput(props?: CreatePinInputProps) {
 			return {
 				id: $rootId,
 				'data-complete': $value.length && $value.every((v) => v.length > 0) ? '' : undefined,
-			};
+			} as const;
 		},
 	});
 
@@ -98,7 +98,7 @@ export function createPinInput(props?: CreatePinInputProps) {
 					disabled: disabledAttr($disabled),
 					type: $type,
 					value: currValue,
-				};
+				} as const;
 			};
 		},
 		action: (node: HTMLInputElement): MeltActionReturn<PinInputEvents['input']> => {
diff --git a/src/lib/builders/popover/create.ts b/src/lib/builders/popover/create.ts
index 7c1553a1..ddc8ef49 100644
--- a/src/lib/builders/popover/create.ts
+++ b/src/lib/builders/popover/create.ts
@@ -17,16 +17,21 @@ import {
 	removeScroll,
 	styleToString,
 	toWritableStores,
-	sleep,
 	portalAttr,
+	generateIds,
+	withGet,
 } from '$lib/internal/helpers/index.js';
 
-import { usePopper, type InteractOutsideEvent } from '$lib/internal/actions/index.js';
+import {
+	useEscapeKeydown,
+	usePopper,
+	usePortal,
+	type InteractOutsideEvent,
+} from '$lib/internal/actions/index.js';
 import { safeOnMount } from '$lib/internal/helpers/lifecycle.js';
 import type { Defaults, MeltActionReturn } from '$lib/internal/types.js';
 import { tick } from 'svelte';
 import { writable } from 'svelte/store';
-import { generateIds } from '../../internal/helpers/id.js';
 import type { PopoverEvents } from './events.js';
 import type { CreatePopoverProps } from './types.js';
 
@@ -48,7 +53,7 @@ const defaults = {
 	onOutsideClick: undefined,
 } satisfies Defaults<CreatePopoverProps>;
 
-type PopoverParts = 'trigger' | 'content' | 'arrow' | 'close';
+type PopoverParts = 'trigger' | 'content' | 'arrow' | 'close' | 'overlay';
 const { name } = createElHelpers<PopoverParts>('popover');
 
 export const popoverIdParts = ['trigger', 'content'] as const;
@@ -75,7 +80,7 @@ export function createPopover(args?: CreatePopoverProps) {
 	const openWritable = withDefaults.open ?? writable(withDefaults.defaultOpen);
 	const open = overridable(openWritable, withDefaults?.onOpenChange);
 
-	const activeTrigger = writable<HTMLElement | null>(null);
+	const activeTrigger = withGet.writable<HTMLElement | null>(null);
 
 	const ids = toWritableStores({ ...generateIds(popoverIdParts), ...withDefaults.ids });
 
@@ -83,8 +88,7 @@ export function createPopover(args?: CreatePopoverProps) {
 		activeTrigger.set(document.getElementById(ids.trigger.get()));
 	});
 
-	async function handleClose() {
-		await sleep(0);
+	function handleClose() {
 		open.set(false);
 		const triggerEl = document.getElementById(ids.trigger.get());
 		handleFocus({ prop: closeFocus.get(), defaultEl: triggerEl });
@@ -104,7 +108,7 @@ export function createPopover(args?: CreatePopoverProps) {
 				id: $contentId,
 				'data-state': $isVisible ? 'open' : 'closed',
 				'data-portal': portalAttr($portal),
-			};
+			} as const;
 		},
 		action: (node: HTMLElement) => {
 			let unsubPopper = noop;
@@ -131,38 +135,38 @@ export function createPopover(args?: CreatePopoverProps) {
 					unsubPopper();
 					if (!$isVisible || !$activeTrigger) return;
 
-					const popper = usePopper(node, {
-						anchorElement: $activeTrigger,
-						open,
-						options: {
-							floating: $positioning,
-							focusTrap: $disableFocusTrap
-								? null
-								: {
-										returnFocusOnDeactivate: false,
-										clickOutsideDeactivates: true,
-										escapeDeactivates: true,
-								  },
-							modal: {
-								shouldCloseOnInteractOutside: shouldCloseOnInteractOutside,
-								onClose: handleClose,
-								open: $isVisible,
-								closeOnInteractOutside: $closeOnOutsideClick,
+					tick().then(() => {
+						unsubPopper();
+						unsubPopper = usePopper(node, {
+							anchorElement: $activeTrigger,
+							open,
+							options: {
+								floating: $positioning,
+								focusTrap: $disableFocusTrap
+									? null
+									: {
+											returnFocusOnDeactivate: false,
+											clickOutsideDeactivates: $closeOnOutsideClick,
+											allowOutsideClick: true,
+											escapeDeactivates: $closeOnEscape,
+									  },
+								modal: {
+									shouldCloseOnInteractOutside: shouldCloseOnInteractOutside,
+									onClose: handleClose,
+									open: $isVisible,
+									closeOnInteractOutside: $closeOnOutsideClick,
+								},
+								escapeKeydown: $closeOnEscape
+									? {
+											handler: () => {
+												handleClose();
+											},
+									  }
+									: null,
+								portal: getPortalDestination(node, $portal),
 							},
-							escapeKeydown: $closeOnEscape
-								? {
-										handler: () => {
-											handleClose();
-										},
-								  }
-								: null,
-							portal: getPortalDestination(node, $portal),
-						},
+						}).destroy;
 					});
-
-					if (popper && popper.destroy) {
-						unsubPopper = popper.destroy;
-					}
 				}
 			);
 
@@ -179,7 +183,7 @@ export function createPopover(args?: CreatePopoverProps) {
 		open.update((prev) => {
 			return !prev;
 		});
-		if (triggerEl) {
+		if (triggerEl && triggerEl !== activeTrigger.get()) {
 			activeTrigger.set(triggerEl);
 		}
 	}
@@ -196,13 +200,13 @@ export function createPopover(args?: CreatePopoverProps) {
 	}
 
 	const trigger = makeElement(name('trigger'), {
-		stores: [open, ids.content, ids.trigger],
-		returned: ([$open, $contentId, $triggerId]) => {
+		stores: [isVisible, ids.content, ids.trigger],
+		returned: ([$isVisible, $contentId, $triggerId]) => {
 			return {
 				role: 'button',
 				'aria-haspopup': 'dialog',
-				'aria-expanded': $open,
-				'data-state': $open ? 'open' : 'closed',
+				'aria-expanded': $isVisible ? 'true' : 'false',
+				'data-state': stateAttr($isVisible),
 				'aria-controls': $contentId,
 				id: $triggerId,
 			} as const;
@@ -225,16 +229,64 @@ export function createPopover(args?: CreatePopoverProps) {
 		},
 	});
 
+	const overlay = makeElement(name('overlay'), {
+		stores: [isVisible],
+		returned: ([$isVisible]) => {
+			return {
+				hidden: $isVisible ? undefined : true,
+				tabindex: -1,
+				style: styleToString({
+					display: $isVisible ? undefined : 'none',
+				}),
+				'aria-hidden': 'true',
+				'data-state': stateAttr($isVisible),
+			} 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;
+				const portalDestination = getPortalDestination(node, $portal);
+				if (portalDestination === null) return;
+				unsubPortal = usePortal(node, portalDestination).destroy;
+			});
+
+			return {
+				destroy() {
+					unsubEscapeKeydown();
+					unsubDerived();
+					unsubPortal();
+				},
+			};
+		},
+	});
+
 	const arrow = makeElement(name('arrow'), {
 		stores: arrowSize,
-		returned: ($arrowSize) => ({
-			'data-arrow': true,
-			style: styleToString({
-				position: 'absolute',
-				width: `var(--arrow-size, ${$arrowSize}px)`,
-				height: `var(--arrow-size, ${$arrowSize}px)`,
-			}),
-		}),
+		returned: ($arrowSize) =>
+			({
+				'data-arrow': true,
+				style: styleToString({
+					position: 'absolute',
+					width: `var(--arrow-size, ${$arrowSize}px)`,
+					height: `var(--arrow-size, ${$arrowSize}px)`,
+				}),
+			} as const),
 	});
 
 	const close = makeElement(name('close'), {
@@ -296,6 +348,7 @@ export function createPopover(args?: CreatePopoverProps) {
 			content,
 			arrow,
 			close,
+			overlay,
 		},
 		states: {
 			open,
@@ -303,3 +356,7 @@ export function createPopover(args?: CreatePopoverProps) {
 		options,
 	};
 }
+
+function stateAttr(open: boolean) {
+	return open ? 'open' : 'closed';
+}
diff --git a/src/lib/builders/progress/create.ts b/src/lib/builders/progress/create.ts
index 5c3f725f..3d61d918 100644
--- a/src/lib/builders/progress/create.ts
+++ b/src/lib/builders/progress/create.ts
@@ -37,7 +37,7 @@ export const createProgress = (props?: CreateProgressProps) => {
 				'data-value': $value,
 				'data-state': $value === null ? 'indeterminate' : $value === $max ? 'complete' : 'loading',
 				'data-max': $max,
-			};
+			} as const;
 		},
 	});
 
diff --git a/src/lib/builders/range-calendar/create.ts b/src/lib/builders/range-calendar/create.ts
index 9ba83009..1353d0b5 100644
--- a/src/lib/builders/range-calendar/create.ts
+++ b/src/lib/builders/range-calendar/create.ts
@@ -285,7 +285,7 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 				'data-invalid': $isInvalid ? '' : undefined,
 				'data-disabled': $disabled ? '' : undefined,
 				'data-readonly': $readonly ? '' : undefined,
-			};
+			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<RangeCalendarEvents['calendar']> => {
 			/**
@@ -312,20 +312,21 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 			return {
 				'aria-hidden': true,
 				'data-disabled': $disabled ? '' : undefined,
-			};
+			} as const;
 		},
 	});
 
 	const grid = makeElement(name('grid'), {
 		stores: [readonly, disabled],
-		returned: ([$readonly, $disabled]) => ({
-			tabindex: -1,
-			role: 'grid',
-			'aria-readonly': $readonly ? ('true' as const) : undefined,
-			'aria-disabled': $disabled ? ('true' as const) : undefined,
-			'data-readonly': $readonly ? '' : undefined,
-			'data-disabled': $disabled ? '' : undefined,
-		}),
+		returned: ([$readonly, $disabled]) =>
+			({
+				tabindex: -1,
+				role: 'grid',
+				'aria-readonly': $readonly ? 'true' : undefined,
+				'aria-disabled': $disabled ? 'true' : undefined,
+				'data-readonly': $readonly ? '' : undefined,
+				'data-disabled': $disabled ? '' : undefined,
+			} as const),
 	});
 
 	const prevButton = makeElement(name('prevButton'), {
@@ -336,10 +337,10 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 				role: 'button',
 				type: 'button' as const,
 				'aria-label': 'Previous',
-				'aria-disabled': disabled ? ('true' as const) : undefined,
+				'aria-disabled': disabled ? 'true' : undefined,
 				disabled: disabled ? true : undefined,
 				'data-disabled': disabled ? '' : undefined,
-			};
+			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<RangeCalendarEvents['prevButton']> => {
 			const unsub = executeCallbacks(
@@ -361,10 +362,10 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 				role: 'button',
 				type: 'button' as const,
 				'aria-label': 'Next',
-				'aria-disabled': disabled ? ('true' as const) : undefined,
+				'aria-disabled': disabled ? 'true' : undefined,
 				disabled: disabled ? true : undefined,
 				'data-disabled': disabled ? '' : undefined,
-			};
+			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<RangeCalendarEvents['nextButton']> => {
 			const unsub = executeCallbacks(
@@ -545,20 +546,49 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 	 * Updates the displayed months based on changes in the placeholder value,
 	 * which determines the months to show in the calendar.
 	 */
-	effect(
-		[placeholder, weekStartsOn, locale, fixedWeeks, numberOfMonths],
-		([$placeholder, $weekStartsOn, $locale, $fixedWeeks, $numberOfMonths]) => {
-			if (!isBrowser || !$placeholder) return;
+	effect([placeholder], ([$placeholder]) => {
+		if (!isBrowser || !$placeholder) return;
 
-			const $visibleMonths = visibleMonths.get();
+		const $visibleMonths = visibleMonths.get();
 
-			/**
-			 * If the placeholder's month is already in the visible months,
-			 * we don't need to do anything.
-			 */
-			if ($visibleMonths.some((month) => isSameMonth(month, $placeholder))) {
-				return;
-			}
+		/**
+		 * If the placeholder's month is already in the visible months,
+		 * we don't need to do anything.
+		 */
+		if ($visibleMonths.some((month) => isSameMonth(month, $placeholder))) {
+			return;
+		}
+
+		const $weekStartsOn = weekStartsOn.get();
+		const $locale = locale.get();
+		const $fixedWeeks = fixedWeeks.get();
+		const $numberOfMonths = numberOfMonths.get();
+
+		const defaultMonthProps = {
+			weekStartsOn: $weekStartsOn,
+			locale: $locale,
+			fixedWeeks: $fixedWeeks,
+			numberOfMonths: $numberOfMonths,
+		};
+
+		months.set(
+			createMonths({
+				...defaultMonthProps,
+				dateObj: $placeholder,
+			})
+		);
+	});
+
+	/**
+	 * Updates the displayed months based on changes in the options values,
+	 * which determines the months to show in the calendar.
+	 */
+	effect(
+		[weekStartsOn, locale, fixedWeeks, numberOfMonths],
+		([$weekStartsOn, $locale, $fixedWeeks, $numberOfMonths]) => {
+			const $placeholder = placeholder.get();
+
+			if (!isBrowser || !$placeholder) return;
 
 			const defaultMonthProps = {
 				weekStartsOn: $weekStartsOn,
diff --git a/src/lib/builders/scroll-area/create.ts b/src/lib/builders/scroll-area/create.ts
new file mode 100644
index 00000000..936d8fec
--- /dev/null
+++ b/src/lib/builders/scroll-area/create.ts
@@ -0,0 +1,520 @@
+import {
+	addEventListener,
+	addMeltEventListener,
+	makeElement,
+	createElHelpers,
+	effect,
+	executeCallbacks,
+	generateIds,
+	isHTMLElement,
+	noop,
+	styleToString,
+	toWritableStores,
+	type IdObj,
+	type ToWritableStores,
+	omit,
+	withGet,
+	type WithGet,
+} from '$lib/internal/helpers/index.js';
+import { derived, writable, type Readable, type Writable } from 'svelte/store';
+import type { CreateScrollAreaProps } from './types.js';
+import type { MeltActionReturn, Orientation, TextDirection } from '$lib/internal/types.js';
+import {
+	addUnlinkedScrollListener,
+	getScrollPositionFromPointer,
+	getThumbOffsetFromScroll,
+	getThumbRatio,
+	isScrollingWithinScrollbarBounds,
+	toInt,
+	type Sizes,
+} from './helpers.js';
+import { createScrollbarX, createScrollbarY, getScrollbarActionByType } from './scrollbars.js';
+import type { ScrollAreaEvents } from './events.js';
+
+type ScrollAreaParts = 'root' | 'viewport' | 'content' | 'scrollbar' | 'thumb' | 'corner';
+export const { name } = createElHelpers<ScrollAreaParts>('scroll-area');
+
+export const scrollAreaIdParts = [
+	'root',
+	'viewport',
+	'content',
+	'scrollbarX',
+	'scrollbarY',
+	'thumbX',
+	'thumbY',
+] as const;
+export type ScrollAreaIdParts = typeof scrollAreaIdParts;
+
+export type ScrollAreaRootState = {
+	cornerWidth: WithGet<Writable<number>>;
+	cornerHeight: WithGet<Writable<number>>;
+	scrollbarXEnabled: WithGet<Writable<boolean>>;
+	scrollbarYEnabled: WithGet<Writable<boolean>>;
+	scrollbarXEl: WithGet<Writable<HTMLElement | null>>;
+	scrollbarYEl: WithGet<Writable<HTMLElement | null>>;
+	scrollAreaEl: WithGet<Writable<HTMLElement | null>>;
+	viewportEl: WithGet<Writable<HTMLElement | null>>;
+	contentEl: WithGet<Writable<HTMLElement | null>>;
+	options: ToWritableStores<Required<Omit<CreateScrollAreaProps, 'ids'>>>;
+	ids: ToWritableStores<IdObj<ScrollAreaIdParts>>;
+};
+
+export type ScrollAreaScrollbarState = {
+	isHorizontal: WithGet<Writable<boolean>>;
+	domRect: WithGet<Writable<DOMRect | null>>;
+	prevWebkitUserSelect: WithGet<Writable<string>>;
+	pointerOffset: WithGet<Writable<number>>;
+	thumbEl: WithGet<Writable<HTMLElement | null>>;
+	thumbOffset: WithGet<Writable<number>>;
+	scrollbarEl: WithGet<Writable<HTMLElement | null>>;
+	sizes: WithGet<Writable<Sizes>>;
+	orientation: WithGet<Writable<Orientation>>;
+	hasThumb: WithGet<Readable<boolean>>;
+	isVisible: WithGet<Writable<boolean>>;
+	handleWheelScroll: (e: WheelEvent, payload: number) => void;
+	handleThumbDown: (payload: { x: number; y: number }) => void;
+	handleThumbUp: (e: MouseEvent) => void;
+	onThumbPositionChange: () => void;
+	onDragScroll: (payload: number) => void;
+	handleSizeChange: () => void;
+};
+
+export type ScrollAreaState = {
+	rootState: ScrollAreaRootState;
+	scrollbarState: ScrollAreaScrollbarState;
+};
+
+const defaults = {
+	type: 'hover' as const,
+	hideDelay: 600,
+	dir: 'ltr' as const,
+};
+
+export function createScrollArea(props?: CreateScrollAreaProps) {
+	const withDefaults = { ...defaults, ...props };
+
+	const options = toWritableStores(omit(withDefaults, 'ids'));
+
+	const cornerWidth = withGet.writable(0);
+	const cornerHeight = withGet.writable(0);
+	const scrollbarXEnabled = withGet.writable(false);
+	const scrollbarYEnabled = withGet.writable(false);
+
+	const scrollAreaEl = withGet.writable<HTMLElement | null>(null);
+	const viewportEl = withGet.writable<HTMLElement | null>(null);
+	const contentEl = withGet.writable<HTMLElement | null>(null);
+	const scrollbarXEl = withGet.writable<HTMLElement | null>(null);
+	const scrollbarYEl = withGet.writable<HTMLElement | null>(null);
+
+	const ids = toWritableStores({ ...generateIds(scrollAreaIdParts), ...withDefaults.ids });
+
+	const rootState: ScrollAreaRootState = {
+		cornerWidth,
+		cornerHeight,
+		scrollbarXEnabled,
+		scrollbarYEnabled,
+		viewportEl,
+		contentEl,
+		options,
+		scrollbarXEl,
+		scrollbarYEl,
+		scrollAreaEl,
+		ids,
+	};
+
+	const root = makeElement(name(), {
+		stores: [cornerWidth, cornerHeight, ids.root],
+		returned: ([$cornerWidth, $cornderHeight, $rootId]) => {
+			return {
+				style: styleToString({
+					position: 'relative',
+					'--melt-scroll-area-corner-width': `${$cornerWidth}px`,
+					'--melt-scroll-area-corner-height': `${$cornderHeight}px`,
+				}),
+				id: $rootId,
+			};
+		},
+		action: (node: HTMLElement) => {
+			scrollAreaEl.set(node);
+			return {
+				destroy() {
+					scrollAreaEl.set(null);
+				},
+			};
+		},
+	});
+
+	const viewport = makeElement(name('viewport'), {
+		stores: [scrollbarXEnabled, scrollbarYEnabled, ids.viewport],
+		returned: ([$scrollbarXEnabled, $scrollbarYEnabled, $viewportId]) => {
+			return {
+				style: styleToString({
+					'scrollbar-width': 'none',
+					'-ms-overflow-style': 'none',
+					'-webkit-overflow-scrolling': 'touch',
+					'-webkit-scrollbar': 'none',
+					'overflow-x': $scrollbarXEnabled ? 'scroll' : 'hidden',
+					'overflow-y': $scrollbarYEnabled ? 'scroll' : 'hidden',
+				}),
+				id: $viewportId,
+			};
+		},
+		action: (node: HTMLElement) => {
+			// Ensure we hide any native scrollbars on the viewport element
+			const styleNode = document.createElement('style');
+			styleNode.innerHTML = `
+				/* Hide scrollbars cross-browser and enable momentum scroll for touch
+					devices */
+				[data-melt-scroll-area-viewport] {
+					scrollbar-width: none;
+					-ms-overflow-style: none;
+					-webkit-overflow-scrolling: touch;
+				}
+
+				[data-melt-scroll-area-viewport]::-webkit-scrollbar {
+					display: none;
+				}
+			`;
+			node.parentElement?.insertBefore(styleNode, node);
+
+			viewportEl.set(node);
+
+			return {
+				destroy() {
+					styleNode.remove();
+					viewportEl.set(null);
+				},
+			};
+		},
+	});
+
+	const content = makeElement(name('content'), {
+		stores: [ids.content],
+		returned: ([$contentId]) => {
+			return {
+				style: styleToString({
+					'min-width': '100%',
+					display: 'table',
+				}),
+				id: $contentId,
+			};
+		},
+		action: (node: HTMLElement) => {
+			contentEl.set(node);
+			return {
+				destroy() {
+					contentEl.set(null);
+				},
+			};
+		},
+	});
+
+	function createScrollbar(orientationProp: Orientation = 'vertical') {
+		const orientation = withGet.writable(orientationProp);
+		const isHorizontal = withGet.writable(orientationProp === 'horizontal');
+		const domRect = withGet.writable<DOMRect | null>(null);
+		const prevWebkitUserSelect = withGet.writable('');
+		const pointerOffset = withGet.writable(0);
+		const thumbEl = withGet.writable<HTMLElement | null>(null);
+		const thumbOffset = withGet.writable(0);
+		const scrollbarEl = withGet.writable<HTMLElement | null>(null);
+
+		const sizes = withGet.writable<Sizes>({
+			content: 0,
+			viewport: 0,
+			scrollbar: {
+				size: 0,
+				paddingStart: 0,
+				paddingEnd: 0,
+			},
+		});
+
+		const isVisible = withGet.writable(false);
+
+		const hasThumb = withGet.derived(sizes, ($sizes) => {
+			const thumbRatio = getThumbRatio($sizes.viewport, $sizes.content);
+			return Boolean(thumbRatio > 0 && thumbRatio < 1);
+		});
+
+		function getScrollPosition(pointerPos: number, dir?: TextDirection) {
+			return getScrollPositionFromPointer(pointerPos, pointerOffset.get(), sizes.get(), dir);
+		}
+
+		function handleWheelScroll(e: WheelEvent, payload: number) {
+			const $viewportEl = viewportEl.get();
+			if (!$viewportEl) return;
+			if (isHorizontal.get()) {
+				const scrollPos = $viewportEl.scrollLeft + e.deltaY;
+
+				$viewportEl.scrollLeft = scrollPos;
+
+				if (isScrollingWithinScrollbarBounds(scrollPos, payload)) {
+					e.preventDefault();
+				}
+			} else {
+				const scrollPos = $viewportEl.scrollTop + e.deltaY;
+
+				$viewportEl.scrollTop = scrollPos;
+
+				if (isScrollingWithinScrollbarBounds(scrollPos, payload)) {
+					e.preventDefault();
+				}
+			}
+		}
+
+		function handleThumbDown(payload: { x: number; y: number }) {
+			if (isHorizontal.get()) {
+				pointerOffset.set(payload.x);
+			} else {
+				pointerOffset.set(payload.y);
+			}
+		}
+
+		function handleThumbUp() {
+			pointerOffset.set(0);
+		}
+
+		function onThumbPositionChange() {
+			const $viewportEl = viewportEl.get();
+			const $thumbEl = thumbEl.get();
+			if (!$viewportEl || !$thumbEl) return;
+
+			const scrollPos = isHorizontal.get() ? $viewportEl.scrollLeft : $viewportEl.scrollTop;
+			const offset = getThumbOffsetFromScroll(scrollPos, sizes.get(), rootState.options.dir.get());
+			thumbOffset.set(offset);
+		}
+
+		function onDragScroll(payload: number) {
+			const $viewportEl = viewportEl.get();
+			if (!$viewportEl) return;
+			if (isHorizontal.get()) {
+				$viewportEl.scrollLeft = getScrollPosition(payload, rootState.options.dir.get());
+			} else {
+				$viewportEl.scrollTop = getScrollPosition(payload);
+			}
+		}
+
+		function handleSizeChange() {
+			const $scrollbarEl = scrollbarState.scrollbarEl.get();
+			if (!$scrollbarEl) return;
+
+			const $isHorizontal = scrollbarState.isHorizontal.get();
+			const $viewportEl = rootState.viewportEl.get();
+
+			if ($isHorizontal) {
+				scrollbarState.sizes.set({
+					content: $viewportEl?.scrollWidth ?? 0,
+					viewport: $viewportEl?.offsetWidth ?? 0,
+					scrollbar: {
+						size: $scrollbarEl.clientWidth ?? 0,
+						paddingStart: toInt(getComputedStyle($scrollbarEl).paddingLeft),
+						paddingEnd: toInt(getComputedStyle($scrollbarEl).paddingRight),
+					},
+				});
+			} else {
+				scrollbarState.sizes.set({
+					content: $viewportEl?.scrollHeight ?? 0,
+					viewport: $viewportEl?.offsetHeight ?? 0,
+					scrollbar: {
+						size: $scrollbarEl.clientHeight ?? 0,
+						paddingStart: toInt(getComputedStyle($scrollbarEl).paddingLeft),
+						paddingEnd: toInt(getComputedStyle($scrollbarEl).paddingRight),
+					},
+				});
+			}
+		}
+
+		const scrollbarState: ScrollAreaScrollbarState = {
+			isHorizontal,
+			domRect,
+			prevWebkitUserSelect,
+			pointerOffset,
+			thumbEl,
+			thumbOffset,
+			sizes,
+			orientation,
+			handleThumbDown,
+			handleThumbUp,
+			onThumbPositionChange,
+			onDragScroll,
+			handleWheelScroll,
+			hasThumb,
+			scrollbarEl,
+			isVisible,
+			handleSizeChange,
+		};
+
+		const scrollbarActionByType = getScrollbarActionByType(options.type.get());
+		const scrollAreaState = { rootState, scrollbarState };
+
+		const scrollbar =
+			orientationProp === 'horizontal'
+				? createScrollbarX(scrollAreaState, scrollbarActionByType)
+				: createScrollbarY(scrollAreaState, scrollbarActionByType);
+
+		const thumb = createScrollbarThumb(scrollAreaState);
+
+		return {
+			scrollbar,
+			thumb,
+		};
+	}
+
+	const { scrollbar: scrollbarX, thumb: thumbX } = createScrollbar('horizontal');
+	const { scrollbar: scrollbarY, thumb: thumbY } = createScrollbar('vertical');
+
+	const corner = createScrollAreaCorner(rootState);
+
+	return {
+		options,
+		elements: {
+			root,
+			viewport,
+			content,
+			corner,
+			scrollbarX,
+			scrollbarY,
+			thumbX,
+			thumbY,
+		},
+	};
+}
+
+function createScrollbarThumb(state: ScrollAreaState) {
+	const { scrollbarState, rootState } = state;
+	function handlePointerDown(e: MouseEvent) {
+		const thumb = e.target;
+		if (!isHTMLElement(thumb)) return;
+		const thumbRect = thumb.getBoundingClientRect();
+		const x = e.clientX - thumbRect.left;
+		const y = e.clientY - thumbRect.top;
+		scrollbarState.handleThumbDown({ x, y });
+	}
+
+	function handlePointerUp(e: MouseEvent) {
+		scrollbarState.handleThumbUp(e);
+	}
+
+	let unsubListener: (() => void) | undefined = undefined;
+
+	function handleScroll() {
+		if (unsubListener) return;
+		const $viewportEl = rootState.viewportEl.get();
+		if ($viewportEl) {
+			unsubListener = addUnlinkedScrollListener($viewportEl, scrollbarState.onThumbPositionChange);
+		}
+		scrollbarState.onThumbPositionChange();
+	}
+
+	const thumb = makeElement(name('thumb'), {
+		stores: [scrollbarState.hasThumb, scrollbarState.isHorizontal, scrollbarState.thumbOffset],
+		returned: ([$hasThumb, $isHorizontal, $offset]) => {
+			return {
+				style: styleToString({
+					width: 'var(--melt-scroll-area-thumb-width)',
+					height: 'var(--melt-scroll-area-thumb-height)',
+					transform: $isHorizontal
+						? `translate3d(${Math.round($offset)}px, 0, 0)`
+						: `translate3d(0, ${Math.round($offset)}px, 0)`,
+				}),
+				'data-state': $hasThumb ? 'visible' : 'hidden',
+			};
+		},
+		action: (node: HTMLElement): MeltActionReturn<ScrollAreaEvents['thumb']> => {
+			scrollbarState.thumbEl.set(node);
+
+			const unsubEffect = effect([scrollbarState.sizes], ([_]) => {
+				const $viewportEl = rootState.viewportEl.get();
+				if (!$viewportEl) return noop;
+
+				scrollbarState.onThumbPositionChange();
+
+				return addEventListener($viewportEl, 'scroll', handleScroll);
+			});
+
+			const unsubEvents = executeCallbacks(
+				addMeltEventListener(node, 'pointerdown', handlePointerDown),
+				addMeltEventListener(node, 'pointerup', handlePointerUp)
+			);
+
+			return {
+				destroy() {
+					unsubListener?.();
+					unsubEvents();
+					unsubEffect();
+				},
+			};
+		},
+	});
+
+	return thumb;
+}
+
+function createScrollAreaCorner(rootState: ScrollAreaRootState) {
+	const width = writable(0);
+	const height = writable(0);
+
+	const hasSize = derived([width, height], ([$width, $height]) => !!$width && !!$height);
+
+	function setCornerHeight() {
+		const offsetHeight = rootState.scrollbarXEl.get()?.offsetHeight || 0;
+		rootState.cornerHeight.set(offsetHeight);
+		height.set(offsetHeight);
+	}
+
+	function setCornerWidth() {
+		const offsetWidth = rootState.scrollbarYEl.get()?.offsetWidth || 0;
+		rootState.cornerWidth.set(offsetWidth);
+		width.set(offsetWidth);
+	}
+
+	effect([rootState.scrollbarXEl], ([$scrollbarXEl]) => {
+		if ($scrollbarXEl) {
+			setCornerHeight();
+		}
+	});
+
+	effect([rootState.scrollbarYEl], ([$scrollbarYEl]) => {
+		if ($scrollbarYEl) {
+			setCornerWidth();
+		}
+	});
+
+	const hasBothScrollbarsVisible = derived(
+		[rootState.scrollbarXEl, rootState.scrollbarYEl],
+		([$scrollbarXEl, $scrollbarYEl]) => {
+			return !!$scrollbarXEl && !!$scrollbarYEl;
+		}
+	);
+
+	const hasCorner = derived(
+		[rootState.options.type, hasBothScrollbarsVisible],
+		([$type, $hasBoth]) => {
+			return $type !== 'scroll' && $hasBoth;
+		}
+	);
+
+	const shouldDisplay = derived(
+		[hasCorner, hasSize],
+		([$hasCorner, $hasSize]) => $hasCorner && $hasSize
+	);
+
+	const corner = makeElement(name('corner'), {
+		stores: [width, height, rootState.options.dir, shouldDisplay],
+		returned: ([$width, $height, $dir, $shouldDisplay]) => {
+			return {
+				style: styleToString({
+					display: $shouldDisplay ? 'block' : 'none',
+					width: `${$width}px`,
+					height: `${$height}px`,
+					position: 'absolute',
+					right: $dir === 'ltr' ? 0 : undefined,
+					left: $dir === 'rtl' ? 0 : undefined,
+					bottom: 0,
+				}),
+			};
+		},
+	});
+
+	return corner;
+}
diff --git a/src/lib/builders/scroll-area/events.ts b/src/lib/builders/scroll-area/events.ts
new file mode 100644
index 00000000..b6dbfb3d
--- /dev/null
+++ b/src/lib/builders/scroll-area/events.ts
@@ -0,0 +1,8 @@
+import type { GroupedEvents } from '$lib/internal/types.js';
+
+export const scrollAreaEvents = {
+	scrollbar: ['pointerdown', 'pointerup', 'pointermove'],
+	thumb: ['pointerdown', 'pointerup'],
+} as const;
+
+export type ScrollAreaEvents = GroupedEvents<typeof scrollAreaEvents>;
diff --git a/src/lib/builders/scroll-area/helpers.ts b/src/lib/builders/scroll-area/helpers.ts
new file mode 100644
index 00000000..7bc95766
--- /dev/null
+++ b/src/lib/builders/scroll-area/helpers.ts
@@ -0,0 +1,122 @@
+import { noop, safeOnDestroy, clamp } from '$lib/internal/helpers/index.js';
+import type { TextDirection } from '$lib/internal/types.js';
+
+export type Sizes = {
+	content: number;
+	viewport: number;
+	scrollbar: {
+		size: number;
+		paddingStart: number;
+		paddingEnd: number;
+	};
+};
+
+export function debounceCallback(cb: () => void, delay: number) {
+	let debounceTimer = 0;
+
+	safeOnDestroy(() => {
+		clearTimeout(debounceTimer);
+	});
+
+	return () => {
+		window.clearTimeout(debounceTimer);
+		debounceTimer = window.setTimeout(cb, delay);
+	};
+}
+
+export function resizeObserver(node: HTMLElement, handleResize: () => void) {
+	let animationFrame = 0;
+
+	const observer = new ResizeObserver(() => {
+		cancelAnimationFrame(animationFrame);
+		animationFrame = requestAnimationFrame(handleResize);
+	});
+
+	observer.observe(node);
+
+	return () => {
+		window.cancelAnimationFrame(animationFrame);
+		observer.unobserve(node);
+	};
+}
+
+// Custom scroll handler to avoid scroll-linked effects
+// https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Scroll-linked_effects
+export function addUnlinkedScrollListener(node: HTMLElement, handler = noop) {
+	let prevPosition = { left: node.scrollLeft, top: node.scrollTop };
+	let rAF = 0;
+	(function loop() {
+		const position = { left: node.scrollLeft, top: node.scrollTop };
+		const isHorizontalScroll = prevPosition.left !== position.left;
+		const isVerticalScroll = prevPosition.top !== position.top;
+		if (isHorizontalScroll || isVerticalScroll) handler();
+		prevPosition = position;
+		rAF = window.requestAnimationFrame(loop);
+	})();
+	return () => window.cancelAnimationFrame(rAF);
+}
+
+export function isScrollingWithinScrollbarBounds(scrollPos: number, maxScrollPos: number) {
+	return scrollPos > 0 && scrollPos < maxScrollPos;
+}
+
+// https://github.com/tmcw-up-for-adoption/simple-linear-scale/blob/master/index.js
+function linearScale(input: readonly [number, number], output: readonly [number, number]) {
+	return (value: number) => {
+		if (input[0] === input[1] || output[0] === output[1]) return output[0];
+		const ratio = (output[1] - output[0]) / (input[1] - input[0]);
+		return output[0] + ratio * (value - input[0]);
+	};
+}
+
+export function toInt(value?: string) {
+	return value ? parseInt(value, 10) : 0;
+}
+
+export function getThumbRatio(viewportSize: number, contentSize: number) {
+	const ratio = viewportSize / contentSize;
+	return isNaN(ratio) ? 0 : ratio;
+}
+
+export function getThumbSize(sizes: Sizes) {
+	const ratio = getThumbRatio(sizes.viewport, sizes.content);
+	const scrollbarPadding = sizes.scrollbar.paddingStart + sizes.scrollbar.paddingEnd;
+	const thumbSize = (sizes.scrollbar.size - scrollbarPadding) * ratio;
+	// minimum of 18 matches macOS minimum
+	return Math.max(thumbSize, 18);
+}
+
+export function getScrollPositionFromPointer(
+	pointerPos: number,
+	pointerOffset: number,
+	sizes: Sizes,
+	dir: TextDirection = 'ltr'
+) {
+	const thumbSizePx = getThumbSize(sizes);
+	const thumbCenter = thumbSizePx / 2;
+	const offset = pointerOffset || thumbCenter;
+	const thumbOffsetFromEnd = thumbSizePx - offset;
+	const minPointerPos = sizes.scrollbar.paddingStart + offset;
+	const maxPointerPos = sizes.scrollbar.size - sizes.scrollbar.paddingEnd - thumbOffsetFromEnd;
+	const maxScrollPos = sizes.content - sizes.viewport;
+	const scrollRange = dir === 'ltr' ? [0, maxScrollPos] : [maxScrollPos * -1, 0];
+	const interpolate = linearScale([minPointerPos, maxPointerPos], scrollRange as [number, number]);
+	return interpolate(pointerPos);
+}
+
+export function getThumbOffsetFromScroll(
+	scrollPos: number,
+	sizes: Sizes,
+	dir: TextDirection = 'ltr'
+) {
+	const thumbSizePx = getThumbSize(sizes);
+	const scrollbarPadding = sizes.scrollbar.paddingStart + sizes.scrollbar.paddingEnd;
+	const scrollbar = sizes.scrollbar.size - scrollbarPadding;
+	const maxScrollPos = sizes.content - sizes.viewport;
+	const maxThumbPos = scrollbar - thumbSizePx;
+	const [scrollClampMin, scrollClampMax] =
+		dir === 'ltr' ? [0, maxScrollPos] : [maxScrollPos * -1, 0];
+	const scrollWithoutMomentum = clamp(scrollClampMin, scrollPos, scrollClampMax);
+	const interpolate = linearScale([0, maxScrollPos], [0, maxThumbPos]);
+	return interpolate(scrollWithoutMomentum);
+}
diff --git a/src/lib/builders/scroll-area/index.ts b/src/lib/builders/scroll-area/index.ts
new file mode 100644
index 00000000..c7734bfb
--- /dev/null
+++ b/src/lib/builders/scroll-area/index.ts
@@ -0,0 +1,2 @@
+export * from './create.js';
+export * from './types.js';
diff --git a/src/lib/builders/scroll-area/scrollbars.ts b/src/lib/builders/scroll-area/scrollbars.ts
new file mode 100644
index 00000000..cc724a0a
--- /dev/null
+++ b/src/lib/builders/scroll-area/scrollbars.ts
@@ -0,0 +1,426 @@
+import {
+	addEventListener,
+	addMeltEventListener,
+	effect,
+	executeCallbacks,
+	isFirefox,
+	isHTMLElement,
+	isTouchDevice,
+	makeElement,
+	noop,
+	styleToString,
+} from '$lib/internal/helpers/index.js';
+import { createStateMachine } from '$lib/internal/helpers/store/stateMachine.js';
+import type { Action } from 'svelte/action';
+import { name, type ScrollAreaState } from './create.js';
+import { debounceCallback, getThumbSize, resizeObserver } from './helpers.js';
+import type { ScrollAreaType } from './types.js';
+import type { MeltActionReturn } from '$lib/internal/types.js';
+import type { ScrollAreaEvents } from './events.js';
+
+export type CreateScrollbarAction = (state: ScrollAreaState) => Action<HTMLElement>;
+
+/**
+ * The base scrollbar action is used for all scrollbar types,
+ * and provides the basic functionality for dragging the scrollbar
+ * thumb and scrolling the content.
+ *
+ * The other scrollbar actions will extend this one, preventing a ton
+ * of code duplication.
+ */
+export function createBaseScrollbarAction(state: ScrollAreaState) {
+	const { rootState, scrollbarState } = state;
+	scrollbarState.isVisible.set(true);
+
+	function handleDragScroll(e: MouseEvent) {
+		const $domRect = scrollbarState.domRect.get();
+		if (!$domRect) return;
+		const x = e.clientX - $domRect.left;
+		const y = e.clientY - $domRect.top;
+		const $isHorizontal = scrollbarState.isHorizontal.get();
+		if ($isHorizontal) {
+			scrollbarState.onDragScroll(x);
+		} else {
+			scrollbarState.onDragScroll(y);
+		}
+	}
+
+	function handlePointerDown(e: PointerEvent) {
+		if (e.button !== 0) return;
+
+		const target = e.target;
+		if (!isHTMLElement(target)) return;
+		target.setPointerCapture(e.pointerId);
+
+		const currentTarget = e.currentTarget;
+		if (!isHTMLElement(currentTarget)) return;
+		scrollbarState.domRect.set(currentTarget.getBoundingClientRect());
+		scrollbarState.prevWebkitUserSelect.set(document.body.style.webkitUserSelect);
+		document.body.style.webkitUserSelect = 'none';
+
+		const $viewportEl = rootState.viewportEl.get();
+		if ($viewportEl) {
+			$viewportEl.style.scrollBehavior = 'auto';
+		}
+
+		handleDragScroll(e);
+	}
+
+	function handlePointerMove(e: PointerEvent) {
+		handleDragScroll(e);
+	}
+
+	function handlePointerUp(e: PointerEvent) {
+		const target = e.target;
+		if (!isHTMLElement(target)) return;
+		if (target.hasPointerCapture(e.pointerId)) {
+			target.releasePointerCapture(e.pointerId);
+		}
+		document.body.style.webkitUserSelect = scrollbarState.prevWebkitUserSelect.get();
+		const $viewportEl = rootState.viewportEl.get();
+		if ($viewportEl) {
+			$viewportEl.style.scrollBehavior = '';
+		}
+		scrollbarState.domRect.set(null);
+	}
+
+	function handleWheel(e: WheelEvent) {
+		const target = e.target;
+		const currentTarget = e.currentTarget;
+		if (!isHTMLElement(target) || !isHTMLElement(currentTarget)) return;
+
+		const isScrollbarWheel = currentTarget.contains(target);
+		if (!isScrollbarWheel) return;
+
+		const $sizes = scrollbarState.sizes.get();
+		if (!$sizes) return;
+		const maxScrollPos = $sizes.content - $sizes.viewport;
+		scrollbarState.handleWheelScroll(e, maxScrollPos);
+	}
+
+	function baseAction(node: HTMLElement) {
+		scrollbarState.scrollbarEl.set(node);
+
+		const unsubEvents = executeCallbacks(
+			addMeltEventListener(node, 'pointerdown', handlePointerDown),
+			addMeltEventListener(node, 'pointermove', handlePointerMove),
+			addMeltEventListener(node, 'pointerup', handlePointerUp),
+			addEventListener(document, 'wheel', handleWheel, { passive: false })
+		);
+
+		const unsubResizeContent = effect([rootState.contentEl], ([$contentEl]) => {
+			if (!$contentEl) return noop;
+
+			return resizeObserver($contentEl, scrollbarState.handleSizeChange);
+		});
+
+		return {
+			destroy() {
+				unsubEvents();
+				unsubResizeContent();
+			},
+		};
+	}
+
+	return baseAction;
+}
+
+/**
+ * The auto scrollbar action will show the scrollbar when the content
+ * overflows the viewport, and hide it when it doesn't.
+ */
+export function createAutoScrollbarAction(state: ScrollAreaState) {
+	// always create the base action first, so we can override any
+	// state mutations that occur there
+	const baseAction = createBaseScrollbarAction(state);
+	const { rootState, scrollbarState } = state;
+
+	const handleResize = debounceCallback(() => {
+		const $viewportEl = rootState.viewportEl.get();
+		if (!$viewportEl) return;
+		const isOverflowX = $viewportEl.offsetWidth < $viewportEl.scrollWidth;
+		const isOverflowY = $viewportEl.offsetHeight < $viewportEl.scrollHeight;
+
+		scrollbarState.isVisible.set(scrollbarState.isHorizontal.get() ? isOverflowX : isOverflowY);
+	}, 10);
+
+	function scrollbarAutoAction(node: HTMLElement) {
+		const unsubBaseAction = baseAction(node)?.destroy;
+		handleResize();
+		const unsubObservers: Array<() => void> = [];
+
+		const $viewportEl = rootState.viewportEl.get();
+		if ($viewportEl) {
+			unsubObservers.push(resizeObserver($viewportEl, handleResize));
+		}
+		const $contentEl = rootState.contentEl.get();
+		if ($contentEl) {
+			unsubObservers.push(resizeObserver($contentEl, handleResize));
+		}
+
+		return {
+			destroy() {
+				unsubObservers.forEach((unsub) => unsub());
+				unsubBaseAction();
+			},
+		};
+	}
+
+	return scrollbarAutoAction;
+}
+
+/**
+ * The hover scrollbar action will show the scrollbar when the user
+ * hovers over the scroll area, and hide it when they leave after
+ * an optionally specified delay.
+ */
+export function createHoverScrollbarAction(state: ScrollAreaState) {
+	// always create the base action first, so we can override any
+	// state mutations that occur there
+	const baseAction = createBaseScrollbarAction(state);
+	const { rootState, scrollbarState } = state;
+
+	// with the hover scrollbar, we want it to be hidden by default
+	// and only show it when the user hovers over the scroll area
+	scrollbarState.isVisible.set(false);
+
+	let timeout: ReturnType<typeof setTimeout> | number | undefined;
+
+	function handlePointerEnter() {
+		window.clearTimeout(timeout);
+		if (scrollbarState.isVisible.get()) return;
+		const $viewportEl = rootState.viewportEl.get();
+		if (!$viewportEl) return;
+		const isOverflowX = $viewportEl.offsetWidth < $viewportEl.scrollWidth;
+		const isOverflowY = $viewportEl.offsetHeight < $viewportEl.scrollHeight;
+
+		scrollbarState.isVisible.set(scrollbarState.isHorizontal.get() ? isOverflowX : isOverflowY);
+	}
+
+	function handlePointerLeave() {
+		timeout = window.setTimeout(() => {
+			if (!scrollbarState.isVisible.get()) return;
+			scrollbarState.isVisible.set(false);
+		}, rootState.options.hideDelay.get());
+	}
+
+	function scrollbarHoverAction(node: HTMLElement) {
+		const unsubBaseAction = baseAction(node)?.destroy;
+		const scrollAreaEl = node.closest('[data-melt-scroll-area]');
+		let unsubScrollAreaListeners = noop;
+
+		if (scrollAreaEl) {
+			if (isTouchDevice()) {
+				unsubScrollAreaListeners = executeCallbacks(
+					addEventListener(scrollAreaEl, 'touchstart', handlePointerEnter),
+					addEventListener(scrollAreaEl, 'touchend', handlePointerLeave)
+				);
+			} else if (isFirefox()) {
+				/**
+				 * Firefox triggers pointerleave events if you tab away from the window
+				 * without moving the pointer, so we use mouseenter/mouseleave instead
+				 * which works as expected.
+				 *
+				 * In Firefox, mouseenter is not triggered if the pointer was over the scroll area
+				 * before events were loaded and then starts moving,
+				 * so we use pointerenter which works as expected.
+				 */
+				unsubScrollAreaListeners = executeCallbacks(
+					addEventListener(scrollAreaEl, 'pointerenter', handlePointerEnter),
+					addEventListener(scrollAreaEl, 'mouseenter', handlePointerEnter),
+					addEventListener(scrollAreaEl, 'mouseleave', handlePointerLeave)
+				);
+			} else {
+				unsubScrollAreaListeners = executeCallbacks(
+					addEventListener(scrollAreaEl, 'pointerenter', handlePointerEnter),
+					addEventListener(scrollAreaEl, 'pointerleave', handlePointerLeave)
+				);
+			}
+		}
+
+		return {
+			destroy() {
+				unsubBaseAction?.();
+				unsubScrollAreaListeners();
+			},
+		};
+	}
+
+	return scrollbarHoverAction;
+}
+
+/**
+ * The scroll scrollbar action will only show the scrollbar
+ * when the user is actively scrolling the content.
+ */
+export function createScrollScrollbarAction(state: ScrollAreaState) {
+	// always create the base action first, so we can
+	// override any state mutations that occur there
+	const baseAction = createBaseScrollbarAction(state);
+	const { rootState, scrollbarState } = state;
+
+	const machine = createStateMachine('hidden', {
+		hidden: {
+			SCROLL: 'scrolling',
+		},
+		scrolling: {
+			SCROLL_END: 'idle',
+			POINTER_ENTER: 'interacting',
+		},
+		interacting: {
+			SCROLL: 'interacting',
+			POINTER_LEAVE: 'idle',
+		},
+		idle: {
+			HIDE: 'hidden',
+			SCROLL: 'scrolling',
+			POINTER_ENTER: 'interacting',
+		},
+	});
+
+	effect([machine.state], ([$status]) => {
+		if ($status === 'idle') {
+			window.setTimeout(() => {
+				machine.dispatch('HIDE');
+			}, rootState.options.hideDelay.get());
+		}
+		if ($status === 'hidden') {
+			scrollbarState.isVisible.set(false);
+		} else {
+			scrollbarState.isVisible.set(true);
+		}
+	});
+
+	const debounceScrollEnd = debounceCallback(() => machine.dispatch('SCROLL_END'), 100);
+
+	effect([rootState.viewportEl, scrollbarState.isHorizontal], ([$viewportEl, $isHorizontal]) => {
+		const scrollDirection = $isHorizontal ? 'scrollLeft' : 'scrollTop';
+
+		let unsub = noop;
+
+		if ($viewportEl) {
+			let prevScrollPos = $viewportEl[scrollDirection];
+			const handleScroll = () => {
+				const scrollPos = $viewportEl[scrollDirection];
+				const hasScrollInDirectionChanged = prevScrollPos !== scrollPos;
+				if (hasScrollInDirectionChanged) {
+					machine.dispatch('SCROLL');
+					debounceScrollEnd();
+				}
+				prevScrollPos = scrollPos;
+			};
+
+			unsub = addEventListener($viewportEl, 'scroll', handleScroll);
+		}
+
+		return () => {
+			unsub();
+		};
+	});
+
+	function scrollbarScrollAction(node: HTMLElement) {
+		const unsubBaseAction = baseAction(node)?.destroy;
+
+		const unsubListeners = executeCallbacks(
+			addEventListener(node, 'pointerenter', () => machine.dispatch('POINTER_ENTER')),
+			addEventListener(node, 'pointerleave', () => machine.dispatch('POINTER_LEAVE'))
+		);
+
+		return {
+			destroy() {
+				unsubBaseAction?.();
+				unsubListeners();
+			},
+		};
+	}
+
+	return scrollbarScrollAction;
+}
+
+/**
+ * Creates the horizontal/x-axis scrollbar builder element.
+ */
+export function createScrollbarX(state: ScrollAreaState, createAction: CreateScrollbarAction) {
+	const action = createAction(state);
+	const { rootState, scrollbarState } = state;
+
+	return makeElement(name('scrollbar'), {
+		stores: [scrollbarState.sizes, rootState.options.dir, scrollbarState.isVisible],
+		returned: ([$sizes, $dir, $isVisible]) => {
+			return {
+				style: styleToString({
+					position: 'absolute',
+					bottom: 0,
+					left: $dir === 'rtl' ? 'var(--melt-scroll-area-corner-width)' : 0,
+					right: $dir === 'ltr' ? 'var(--melt-scroll-area-corner-width)' : 0,
+					'--melt-scroll-area-thumb-width': `${getThumbSize($sizes)}px`,
+					visibility: !$isVisible ? 'hidden' : undefined,
+				}),
+				'data-state': $isVisible ? 'visible' : 'hidden',
+			};
+		},
+		action: (node: HTMLElement): MeltActionReturn<ScrollAreaEvents['scrollbar']> => {
+			const unsubAction = action(node)?.destroy;
+			rootState.scrollbarXEl.set(node);
+			rootState.scrollbarXEnabled.set(true);
+			return {
+				destroy() {
+					unsubAction?.();
+					rootState.scrollbarXEl.set(null);
+				},
+			};
+		},
+	});
+}
+
+/**
+ * Creates the vertical/y-axis scrollbar builder element.
+ */
+export function createScrollbarY(state: ScrollAreaState, createAction: CreateScrollbarAction) {
+	const action = createAction(state);
+	const { rootState, scrollbarState } = state;
+
+	return makeElement(name('scrollbar'), {
+		stores: [scrollbarState.sizes, rootState.options.dir, scrollbarState.isVisible],
+		returned: ([$sizes, $dir, $isVisible]) => {
+			return {
+				style: styleToString({
+					position: 'absolute',
+					top: 0,
+					right: $dir === 'ltr' ? 0 : undefined,
+					left: $dir === 'rtl' ? 0 : undefined,
+					bottom: 'var(--melt-scroll-area-corner-height)',
+					'--melt-scroll-area-thumb-height': `${getThumbSize($sizes)}px`,
+					visibility: !$isVisible ? 'hidden' : undefined,
+				}),
+				'data-state': $isVisible ? 'visible' : 'hidden',
+			};
+		},
+		action: (node: HTMLElement): MeltActionReturn<ScrollAreaEvents['scrollbar']> => {
+			const unsubAction = action(node)?.destroy;
+			rootState.scrollbarYEl.set(node);
+			rootState.scrollbarYEnabled.set(true);
+			return {
+				destroy() {
+					unsubAction?.();
+					rootState.scrollbarYEl.set(null);
+				},
+			};
+		},
+	});
+}
+
+export function getScrollbarActionByType(type: ScrollAreaType) {
+	switch (type) {
+		case 'always':
+			return createBaseScrollbarAction;
+		case 'auto':
+			return createAutoScrollbarAction;
+		case 'hover':
+			return createHoverScrollbarAction;
+		case 'scroll':
+			return createScrollScrollbarAction;
+		default:
+			return createBaseScrollbarAction;
+	}
+}
diff --git a/src/lib/builders/scroll-area/types.ts b/src/lib/builders/scroll-area/types.ts
new file mode 100644
index 00000000..13916d55
--- /dev/null
+++ b/src/lib/builders/scroll-area/types.ts
@@ -0,0 +1,38 @@
+import type { IdObj } from '$lib/internal/helpers/index.js';
+import type { TextDirection } from '$lib/internal/types.js';
+import type { ScrollAreaIdParts, createScrollArea } from './index.js';
+
+export type ScrollAreaType = 'auto' | 'always' | 'scroll' | 'hover';
+
+export type CreateScrollAreaProps = {
+	/**
+	 * Determines when the scrollbar should be visible
+	 *
+	 * @default 'hover'
+	 */
+	type?: ScrollAreaType;
+
+	/**
+	 * If the type is `"scroll"` or `"hover"`, this determines how long
+	 * (in milliseconds) the scrollbar should be visible after the user
+	 * either stops scrolling or stops hovering over the scroll area.
+	 *
+	 * @default 600
+	 */
+	hideDelay?: number;
+
+	/**
+	 * The reading direction of the scroll area.
+	 *
+	 * @default 'ltr'
+	 */
+	dir?: TextDirection;
+
+	/**
+	 * Optionally override the default ids assigned to the
+	 * elements.
+	 */
+	ids?: Partial<IdObj<ScrollAreaIdParts>>;
+};
+
+export type ScrollArea = ReturnType<typeof createScrollArea>;
diff --git a/src/lib/builders/separator/create.ts b/src/lib/builders/separator/create.ts
index d4d2aeab..c2133db1 100644
--- a/src/lib/builders/separator/create.ts
+++ b/src/lib/builders/separator/create.ts
@@ -21,7 +21,7 @@ export const createSeparator = (props?: CreateSeparatorProps) => {
 				'aria-orientation': ariaOrientation,
 				'aria-hidden': $decorative,
 				'data-orientation': $orientation,
-			};
+			} as const;
 		},
 	});
 
diff --git a/src/lib/builders/slider/create.ts b/src/lib/builders/slider/create.ts
index b2fa5e22..fa647551 100644
--- a/src/lib/builders/slider/create.ts
+++ b/src/lib/builders/slider/create.ts
@@ -125,7 +125,7 @@ export const createSlider = (props?: CreateSliderProps) => {
 					: `touch-action: ${$orientation === 'horizontal' ? 'pan-y' : 'pan-x'}`,
 
 				'data-melt-id': meltIds.root,
-			};
+			} as const;
 		},
 	});
 
@@ -164,7 +164,7 @@ export const createSlider = (props?: CreateSliderProps) => {
 
 			return {
 				style: styleToString(style),
-			};
+			} as const;
 		},
 	});
 
@@ -394,7 +394,7 @@ export const createSlider = (props?: CreateSliderProps) => {
 					'data-bounded': bounded ? true : undefined,
 					'data-value': tickValue,
 					style: styleToString(style),
-				};
+				} as const;
 			});
 		},
 	});
diff --git a/src/lib/builders/switch/create.ts b/src/lib/builders/switch/create.ts
index 4d1aa06e..74724849 100644
--- a/src/lib/builders/switch/create.ts
+++ b/src/lib/builders/switch/create.ts
@@ -74,7 +74,7 @@ export function createSwitch(props?: CreateSwitchProps) {
 		stores: [checked, nameStore, required, disabled, value],
 		returned: ([$checked, $name, $required, $disabled, $value]) => {
 			return {
-				type: 'checkbox' as const,
+				type: 'checkbox',
 				'aria-hidden': true,
 				hidden: true,
 				tabindex: -1,
diff --git a/src/lib/builders/table-of-contents/create.ts b/src/lib/builders/table-of-contents/create.ts
index 043d3d13..978d7345 100644
--- a/src/lib/builders/table-of-contents/create.ts
+++ b/src/lib/builders/table-of-contents/create.ts
@@ -355,7 +355,7 @@ export function createTableOfContents(args: CreateTableOfContentsArgs) {
 				return {
 					'data-id': id,
 					'data-active': active ? '' : undefined,
-				};
+				} as const;
 			};
 		},
 		action: (node: HTMLAnchorElement) => {
diff --git a/src/lib/builders/tabs/create.ts b/src/lib/builders/tabs/create.ts
index 8dac2383..96cdb5d7 100644
--- a/src/lib/builders/tabs/create.ts
+++ b/src/lib/builders/tabs/create.ts
@@ -50,7 +50,7 @@ export function createTabs(props?: CreateTabsProps) {
 		returned: ($orientation) => {
 			return {
 				'data-orientation': $orientation,
-			};
+			} as const;
 		},
 	});
 
@@ -62,7 +62,7 @@ export function createTabs(props?: CreateTabsProps) {
 				role: 'tablist',
 				'aria-orientation': $orientation,
 				'data-orientation': $orientation,
-			};
+			} as const;
 		},
 	});
 
@@ -90,7 +90,7 @@ export function createTabs(props?: CreateTabsProps) {
 				const isActive = sourceOfTruth === tabValue;
 
 				return {
-					type: 'button' as const,
+					type: 'button',
 					role: 'tab',
 					'data-state': isActive ? 'active' : 'inactive',
 					tabindex: isActive ? 0 : -1,
@@ -98,7 +98,7 @@ export function createTabs(props?: CreateTabsProps) {
 					'data-orientation': $orientation,
 					'data-disabled': disabledAttr(disabled),
 					disabled: disabledAttr(disabled),
-				};
+				} as const;
 			};
 		},
 		action: (node: HTMLElement): MeltActionReturn<TabsEvents['trigger']> => {
@@ -194,7 +194,7 @@ export function createTabs(props?: CreateTabsProps) {
 						? undefined
 						: true,
 					tabindex: 0,
-				};
+				} as const;
 			};
 		},
 	});
diff --git a/src/lib/builders/tags-input/create.ts b/src/lib/builders/tags-input/create.ts
index 12c1c6b3..5e5127f0 100644
--- a/src/lib/builders/tags-input/create.ts
+++ b/src/lib/builders/tags-input/create.ts
@@ -262,7 +262,7 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 				'data-disabled': disabledAttr($disabled),
 				disabled: disabledAttr($disabled),
 				placeholder: $placeholder,
-			};
+			} as const;
 		},
 		action: (node: HTMLInputElement): MeltActionReturn<TagsInputEvents['input']> => {
 			const getTagsInfo = (id: string) => {
@@ -497,7 +497,7 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 								margin: 0,
 						  })
 						: undefined,
-				};
+				} as const;
 			};
 		},
 		action: (node: HTMLDivElement): MeltActionReturn<TagsInputEvents['tag']> => {
@@ -583,7 +583,7 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 					'data-disabled': disabledAttr(disabled),
 					disabled: disabledAttr(disabled),
 					tabindex: -1,
-				};
+				} as const;
 			};
 		},
 		action: (node: HTMLElement): MeltActionReturn<TagsInputEvents['deleteTrigger']> => {
@@ -638,7 +638,7 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 								margin: 0,
 						  })
 						: undefined,
-				};
+				} as const;
 			};
 		},
 		action: (node: HTMLElement): MeltActionReturn<TagsInputEvents['edit']> => {
diff --git a/src/lib/builders/toast/create.ts b/src/lib/builders/toast/create.ts
index fd7d104a..2576e73a 100644
--- a/src/lib/builders/toast/create.ts
+++ b/src/lib/builders/toast/create.ts
@@ -111,9 +111,9 @@ export function createToaster<T = object>(props?: CreateToasterProps) {
 					role: 'alert',
 					'aria-describedby': toast.ids.description,
 					'aria-labelledby': toast.ids.title,
-					'aria-live': toast.type === 'foreground' ? ('assertive' as const) : ('polite' as const),
+					'aria-live': toast.type === 'foreground' ? 'assertive' : 'polite',
 					tabindex: -1,
-				};
+				} as const;
 			};
 		},
 		action: (node: HTMLElement): MeltActionReturn<ToastEvents['content']> => {
@@ -170,7 +170,7 @@ export function createToaster<T = object>(props?: CreateToasterProps) {
 				if (!toast) return null;
 				return {
 					id: toast.ids.title,
-				};
+				} as const;
 			};
 		},
 	});
@@ -184,17 +184,18 @@ export function createToaster<T = object>(props?: CreateToasterProps) {
 
 				return {
 					id: toast.ids.description,
-				};
+				} as const;
 			};
 		},
 	});
 
 	const close = makeElement(name('close'), {
 		returned: () => {
-			return (id: string) => ({
-				type: 'button' as const,
-				'data-id': id,
-			});
+			return (id: string) =>
+				({
+					type: 'button',
+					'data-id': id,
+				} as const);
 		},
 		action: (node: HTMLElement): MeltActionReturn<ToastEvents['close']> => {
 			function handleClose() {
diff --git a/src/lib/builders/toolbar/create.ts b/src/lib/builders/toolbar/create.ts
index 051ab1c4..e810e9eb 100644
--- a/src/lib/builders/toolbar/create.ts
+++ b/src/lib/builders/toolbar/create.ts
@@ -39,7 +39,7 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 			return {
 				role: 'toolbar',
 				'data-orientation': $orientation,
-			};
+			} as const;
 		},
 	});
 
diff --git a/src/lib/builders/tooltip/create.ts b/src/lib/builders/tooltip/create.ts
index cd20300d..48e101ee 100644
--- a/src/lib/builders/tooltip/create.ts
+++ b/src/lib/builders/tooltip/create.ts
@@ -19,7 +19,6 @@ import {
 	styleToString,
 	toWritableStores,
 	removeUndefined,
-	sleep,
 	portalAttr,
 } from '$lib/internal/helpers/index.js';
 
@@ -29,6 +28,7 @@ import { derived, writable, type Writable } from 'svelte/store';
 import { generateIds } from '../../internal/helpers/id.js';
 import type { TooltipEvents } from './events.js';
 import type { CreateTooltipProps } from './types.js';
+import { tick } from 'svelte';
 
 const defaults = {
 	positioning: {
@@ -142,7 +142,7 @@ export function createTooltip(props?: CreateTooltipProps) {
 				'aria-describedby': $contentId,
 				id: $triggerId,
 				'data-state': $open ? 'open' : 'closed',
-			};
+			} as const;
 		},
 		action: (node: HTMLElement): MeltActionReturn<TooltipEvents['trigger']> => {
 			const keydownHandler = (e: KeyboardEvent) => {
@@ -182,10 +182,7 @@ export function createTooltip(props?: CreateTooltipProps) {
 					if (clickedTrigger) return;
 					openTooltip('focus');
 				}),
-				addMeltEventListener(node, 'blur', async () => {
-					await sleep(0);
-					closeTooltip(true);
-				}),
+				addMeltEventListener(node, 'blur', () => closeTooltip(true)),
 				addMeltEventListener(node, 'keydown', keydownHandler),
 				addEventListener(document, 'keydown', keydownHandler)
 			);
@@ -207,7 +204,7 @@ export function createTooltip(props?: CreateTooltipProps) {
 				id: $contentId,
 				'data-portal': portalAttr($portal),
 				'data-state': $open ? 'open' : 'closed',
-			});
+			} as const);
 		},
 		action: (node: HTMLElement): MeltActionReturn<TooltipEvents['content']> => {
 			let unsubFloating = noop;
@@ -216,26 +213,18 @@ export function createTooltip(props?: CreateTooltipProps) {
 			const unsubDerived = effect(
 				[isVisible, positioning, portal],
 				([$isVisible, $positioning, $portal]) => {
+					unsubPortal();
+					unsubFloating();
 					const triggerEl = getEl('trigger');
-					if (!$isVisible || !triggerEl) {
-						unsubPortal();
-						unsubFloating();
-						return;
-					}
+					if (!$isVisible || !triggerEl) return;
 
-					const floatingReturn = useFloating(triggerEl, node, $positioning);
-					unsubFloating = floatingReturn.destroy;
-					if ($portal === null) {
+					tick().then(() => {
 						unsubPortal();
-						return;
-					}
-					const portalDest = getPortalDestination(node, $portal);
-					if (portalDest) {
-						const portalReturn = usePortal(node, portalDest);
-						if (portalReturn && portalReturn.destroy) {
-							unsubPortal = portalReturn.destroy;
-						}
-					}
+						unsubFloating();
+						const portalDest = getPortalDestination(node, $portal);
+						if (portalDest) unsubPortal = usePortal(node, portalDest).destroy;
+						unsubFloating = useFloating(triggerEl, node, $positioning).destroy;
+					});
 				}
 			);
 
@@ -272,14 +261,15 @@ export function createTooltip(props?: CreateTooltipProps) {
 
 	const arrow = makeElement(name('arrow'), {
 		stores: arrowSize,
-		returned: ($arrowSize) => ({
-			'data-arrow': true,
-			style: styleToString({
-				position: 'absolute',
-				width: `var(--arrow-size, ${$arrowSize}px)`,
-				height: `var(--arrow-size, ${$arrowSize}px)`,
-			}),
-		}),
+		returned: ($arrowSize) =>
+			({
+				'data-arrow': true,
+				style: styleToString({
+					position: 'absolute',
+					width: `var(--arrow-size, ${$arrowSize}px)`,
+					height: `var(--arrow-size, ${$arrowSize}px)`,
+				}),
+			} as const),
 	});
 
 	let isMouseInTooltipArea = false;
diff --git a/src/lib/builders/tree/create.ts b/src/lib/builders/tree/create.ts
index d7bacbb2..2194ffe0 100644
--- a/src/lib/builders/tree/create.ts
+++ b/src/lib/builders/tree/create.ts
@@ -96,7 +96,7 @@ export function createTreeView(args?: CreateTreeViewProps) {
 					'data-id': id,
 					tabindex,
 					'aria-expanded': hasChildren ? $expanded.includes(id) : undefined,
-				};
+				} as const;
 			};
 		},
 		action: (node: HTMLElement): MeltActionReturn<TreeEvents['item']> => {
@@ -252,14 +252,15 @@ export function createTreeView(args?: CreateTreeViewProps) {
 	const group = makeElement(name('group'), {
 		stores: [expanded],
 		returned: ([$expanded]) => {
-			return (opts: { id: string }) => ({
-				role: 'group',
-				'data-group-id': opts.id,
-				hidden: !forceVisible && !$expanded.includes(opts.id) ? true : undefined,
-				style: styleToString({
-					display: !forceVisible && !$expanded.includes(opts.id) ? 'none' : undefined,
-				}),
-			});
+			return (opts: { id: string }) =>
+				({
+					role: 'group',
+					'data-group-id': opts.id,
+					hidden: !forceVisible && !$expanded.includes(opts.id) ? true : undefined,
+					style: styleToString({
+						display: !forceVisible && !$expanded.includes(opts.id) ? 'none' : undefined,
+					}),
+				} as const);
 		},
 	});
 
diff --git a/src/lib/internal/actions/escape-keydown/action.ts b/src/lib/internal/actions/escape-keydown/action.ts
index 4181b555..04c6190c 100644
--- a/src/lib/internal/actions/escape-keydown/action.ts
+++ b/src/lib/internal/actions/escape-keydown/action.ts
@@ -3,6 +3,7 @@ import { isFunction, isHTMLElement, isReadable } from '$lib/internal/helpers/is.
 import { get, readable, type Readable } from 'svelte/store';
 import { effect, executeCallbacks, kbd, noop } from '../../helpers/index.js';
 import type { 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.
@@ -35,7 +36,7 @@ const documentEscapeKeyStore = readable<KeyboardEvent | undefined>(
 	}
 );
 
-export const useEscapeKeydown = (node: HTMLElement, config: EscapeKeydownConfig = {}) => {
+export const useEscapeKeydown = ((node, config = {}) => {
 	let unsub = noop;
 	function update(config: EscapeKeydownConfig = {}) {
 		unsub();
@@ -96,4 +97,4 @@ export const useEscapeKeydown = (node: HTMLElement, config: EscapeKeydownConfig
 			unsub();
 		},
 	};
-};
+}) satisfies Action<HTMLElement, EscapeKeydownConfig>;
diff --git a/src/lib/internal/actions/floating/action.ts b/src/lib/internal/actions/floating/action.ts
index a1d34801..4847a13e 100644
--- a/src/lib/internal/actions/floating/action.ts
+++ b/src/lib/internal/actions/floating/action.ts
@@ -14,7 +14,7 @@ import {
 } from '@floating-ui/dom';
 import type { FloatingConfig } from './types.js';
 import { isHTMLElement, noop } from '$lib/internal/helpers/index.js';
-import type { VirtualElement } from '@floating-ui/core';
+import type { Placement, VirtualElement } from '@floating-ui/core';
 
 const defaultConfig = {
 	strategy: 'absolute',
@@ -32,6 +32,12 @@ const ARROW_TRANSFORM = {
 	right: 'rotate(315deg)',
 };
 
+const SIDE_OPTIONS = ['top', 'right', 'bottom', 'left'] as const;
+const ALIGN_OPTIONS = ['start', 'center', 'end'] as const;
+
+type Side = (typeof SIDE_OPTIONS)[number];
+type Align = (typeof ALIGN_OPTIONS)[number];
+
 export function useFloating(
 	reference: HTMLElement | VirtualElement | undefined,
 	floating: HTMLElement | undefined,
@@ -100,6 +106,9 @@ export function useFloating(
 
 	function compute() {
 		if (!reference || !floating) return;
+		// if the reference is no longer in the document (e.g. it was removed), ignore it
+		if (isHTMLElement(reference) && !reference.ownerDocument.documentElement.contains(reference))
+			return;
 		const { placement, strategy } = options;
 
 		computePosition(reference, floating, {
@@ -110,6 +119,13 @@ export function useFloating(
 			const x = Math.round(data.x);
 			const y = Math.round(data.y);
 
+			// get the chosen side and align from the placement to apply as attributes
+			// to the floating element and arrow
+			const [side, align] = getSideAndAlignFromPlacement(data.placement);
+
+			floating.setAttribute('data-side', side);
+			floating.setAttribute('data-align', align);
+
 			Object.assign(floating.style, {
 				position: options.strategy,
 				top: `${y}px`,
@@ -121,6 +137,8 @@ export function useFloating(
 
 				const dir = data.placement.split('-')[0] as 'top' | 'bottom' | 'left' | 'right';
 
+				arrowEl.setAttribute('data-side', dir);
+
 				Object.assign(arrowEl.style, {
 					position: 'absolute',
 					left: x != null ? `${x}px` : '',
@@ -145,3 +163,8 @@ export function useFloating(
 		destroy: autoUpdate(reference, floating, compute),
 	};
 }
+
+function getSideAndAlignFromPlacement(placement: Placement) {
+	const [side, align = 'center'] = placement.split('-');
+	return [side as Side, align as Align] as const;
+}
diff --git a/src/lib/internal/actions/focus-trap/action.ts b/src/lib/internal/actions/focus-trap/action.ts
index 4003c4f8..8cd1e7f2 100644
--- a/src/lib/internal/actions/focus-trap/action.ts
+++ b/src/lib/internal/actions/focus-trap/action.ts
@@ -7,7 +7,7 @@ import type { FocusTrapConfig, FocusTrapReturn } from './types.js';
 import { writable, readonly } from 'svelte/store';
 import { createFocusTrap as _createFocusTrap } from 'focus-trap';
 
-export function createFocusTrap(config: FocusTrapConfig = {}): FocusTrapReturn {
+export function createFocusTrap(config: FocusTrapConfig = {}) {
 	let trap: undefined | FocusTrap;
 
 	const { immediate, ...focusTrapOptions } = config;
@@ -66,5 +66,5 @@ export function createFocusTrap(config: FocusTrapConfig = {}): FocusTrapReturn {
 		deactivate,
 		pause,
 		unpause,
-	};
+	} satisfies FocusTrapReturn;
 }
diff --git a/src/lib/internal/actions/interact-outside/action.ts b/src/lib/internal/actions/interact-outside/action.ts
index 4fcdbb2d..28638d38 100644
--- a/src/lib/internal/actions/interact-outside/action.ts
+++ b/src/lib/internal/actions/interact-outside/action.ts
@@ -5,9 +5,11 @@ import {
 	noop,
 } from '$lib/internal/helpers/index.js';
 import type { InteractOutsideConfig, InteractOutsideEvent } from './types.js';
+import type { Action } from 'svelte/action';
 
-export function useInteractOutside(node: HTMLElement, config: InteractOutsideConfig) {
+export const useInteractOutside = ((node, config) => {
 	let unsub = noop;
+	let unsubClick = noop;
 
 	let isPointerDown = false;
 	let isPointerDownInside = false;
@@ -15,6 +17,7 @@ export function useInteractOutside(node: HTMLElement, config: InteractOutsideCon
 
 	function update(config: InteractOutsideConfig) {
 		unsub();
+		unsubClick();
 		const { onInteractOutside, onInteractOutsideStart, enabled } = config;
 
 		if (!enabled) return;
@@ -41,10 +44,29 @@ export function useInteractOutside(node: HTMLElement, config: InteractOutsideCon
 		// Use pointer events if available, otherwise use mouse/touch events
 		if (typeof PointerEvent !== 'undefined') {
 			const onPointerUp = (e: PointerEvent) => {
-				if (shouldTriggerInteractOutside(e)) {
-					triggerInteractOutside(e);
+				unsubClick();
+
+				const handler = (e: InteractOutsideEvent) => {
+					if (shouldTriggerInteractOutside(e)) {
+						triggerInteractOutside(e);
+					}
+					resetPointerState();
+				};
+
+				/**
+				 * On touch devices, we need to wait for a click event because browsers implement
+				 * a delay between the time the user stops touching the display and when the
+				 * browser executes the click event. Without waiting for the click event, the
+				 * browser may execute events on other elements that should have been prevented.
+				 */
+				if (e.pointerType === 'touch') {
+					unsubClick = addEventListener(documentObj, 'click', handler, {
+						capture: true,
+						once: true,
+					});
+					return;
 				}
-				resetPointerState();
+				handler(e);
 			};
 
 			unsub = executeCallbacks(
@@ -94,9 +116,12 @@ export function useInteractOutside(node: HTMLElement, config: InteractOutsideCon
 
 	return {
 		update,
-		destroy: unsub,
+		destroy() {
+			unsub();
+			unsubClick();
+		},
 	};
-}
+}) satisfies Action<HTMLElement, InteractOutsideConfig>;
 
 function isValidEvent(e: InteractOutsideEvent, node: HTMLElement): boolean {
 	if ('button' in e && e.button > 0) return false;
diff --git a/src/lib/internal/actions/modal/action.ts b/src/lib/internal/actions/modal/action.ts
index 5c4fb7b4..8cd452f3 100644
--- a/src/lib/internal/actions/modal/action.ts
+++ b/src/lib/internal/actions/modal/action.ts
@@ -2,10 +2,10 @@ import { isElement, last, noop, sleep } 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 function useModal(node: HTMLElement, config: ModalConfig) {
+export const useModal = ((node, config) => {
 	let unsubInteractOutside = noop;
 
 	function removeNodeFromVisibleModals() {
@@ -69,4 +69,4 @@ export function useModal(node: HTMLElement, config: ModalConfig) {
 			unsubInteractOutside();
 		},
 	};
-}
+}) satisfies Action<HTMLElement, ModalConfig>;
diff --git a/src/lib/internal/actions/popper/action.ts b/src/lib/internal/actions/popper/action.ts
index f17366a3..1b692c76 100644
--- a/src/lib/internal/actions/popper/action.ts
+++ b/src/lib/internal/actions/popper/action.ts
@@ -22,7 +22,7 @@ const defaultConfig = {
 	portal: 'body',
 } satisfies PopperConfig;
 
-export const usePopper: Action<HTMLElement, PopperArgs> = (popperElement, args) => {
+export const usePopper = ((popperElement, args) => {
 	popperElement.dataset.escapee = '';
 	const { anchorElement, open, options } = args as PopperArgs;
 	if (!anchorElement || !open || !options) {
@@ -34,10 +34,7 @@ export const usePopper: Action<HTMLElement, PopperArgs> = (popperElement, args)
 	const callbacks: Callback[] = [];
 
 	if (opts.portal !== null) {
-		const portal = usePortal(popperElement, opts.portal);
-		if (portal?.destroy) {
-			callbacks.push(portal.destroy);
-		}
+		callbacks.push(usePortal(popperElement, opts.portal).destroy);
 	}
 
 	callbacks.push(useFloating(anchorElement, popperElement, opts.floating).destroy);
@@ -53,11 +50,7 @@ export const usePopper: Action<HTMLElement, PopperArgs> = (popperElement, args)
 			...opts.focusTrap,
 		});
 
-		const usedFocusTrap = useFocusTrap(popperElement);
-
-		if (usedFocusTrap?.destroy) {
-			callbacks.push(usedFocusTrap.destroy);
-		}
+		callbacks.push(useFocusTrap(popperElement).destroy);
 	}
 
 	if (opts.modal !== null) {
@@ -103,4 +96,4 @@ export const usePopper: Action<HTMLElement, PopperArgs> = (popperElement, args)
 			unsubscribe();
 		},
 	};
-};
+}) satisfies Action<HTMLElement, PopperArgs>;
diff --git a/src/lib/internal/actions/portal.ts b/src/lib/internal/actions/portal.ts
index 50e0e3c6..f2581099 100644
--- a/src/lib/internal/actions/portal.ts
+++ b/src/lib/internal/actions/portal.ts
@@ -4,7 +4,7 @@ import { isHTMLElement, noop } from '$lib/internal/helpers/index.js';
 
 export type PortalConfig = string | HTMLElement | undefined;
 
-export const usePortal: Action<HTMLElement, PortalConfig> = (el, target = 'body') => {
+export const usePortal = ((el, target = 'body') => {
 	let targetEl;
 
 	if (!isHTMLElement(target) && typeof target !== 'string') {
@@ -47,4 +47,4 @@ export const usePortal: Action<HTMLElement, PortalConfig> = (el, target = 'body'
 		update,
 		destroy,
 	};
-};
+}) satisfies Action<HTMLElement, PortalConfig>;
diff --git a/src/lib/internal/helpers/attr.ts b/src/lib/internal/helpers/attr.ts
index 621863a9..69a6c053 100644
--- a/src/lib/internal/helpers/attr.ts
+++ b/src/lib/internal/helpers/attr.ts
@@ -24,7 +24,7 @@ export const hiddenInputAttrs = {
  */
 export function portalAttr(portal: string | HTMLElement | null | undefined) {
 	if (portal !== null) {
-		return portal;
+		return '';
 	}
 	return undefined;
 }
diff --git a/src/lib/internal/helpers/index.ts b/src/lib/internal/helpers/index.ts
index ad3fb301..b360d341 100644
--- a/src/lib/internal/helpers/index.ts
+++ b/src/lib/internal/helpers/index.ts
@@ -5,6 +5,7 @@ export * from './dom.js';
 export * from './event.js';
 export * from './highlight.js';
 export * from './is.js';
+export * from './lifecycle.js';
 export * from './list.js';
 export * from './locale.js';
 export * from './object.js';
diff --git a/src/lib/internal/helpers/store/stateMachine.ts b/src/lib/internal/helpers/store/stateMachine.ts
new file mode 100644
index 00000000..8ea8cb00
--- /dev/null
+++ b/src/lib/internal/helpers/store/stateMachine.ts
@@ -0,0 +1,41 @@
+import { withGet } from '../withGet.js';
+
+export interface Machine<S> {
+	[k: string]: { [k: string]: S };
+}
+export type MachineState<T> = keyof T;
+export type MachineEvent<T> = {
+	[K in keyof T]: keyof T[K];
+}[keyof T];
+
+/**
+ * Creates a state machine with the given initial state and machine definition.
+ * @param initialState - The initial state of the machine.
+ * @param machine - The definition of the state machine, mapping states to possible transitions.
+ * @returns An object containing the state store and a dispatch function for state transitions.
+ */
+export function createStateMachine<M>(
+	initialState: MachineState<M>,
+	machine: M & Machine<MachineState<M>>
+) {
+	// init a store with the initial state
+	const state = withGet.writable(initialState);
+
+	function reducer(event: MachineEvent<M>) {
+		const $state = state.get();
+
+		// Get next state based on the current state & event,
+		// or keep current state if no transition is defined.
+		const nextState = machine[$state][event];
+		return nextState ?? $state;
+	}
+
+	const dispatch = (event: MachineEvent<M>) => {
+		state.set(reducer(event));
+	};
+
+	return {
+		state,
+		dispatch,
+	};
+}


Test patch:
diff --git a/e2e/dialog.spec.ts b/e2e/dialog.spec.ts
new file mode 100644
index 00000000..e3f94dfc
--- /dev/null
+++ b/e2e/dialog.spec.ts
@@ -0,0 +1,74 @@
+import { test, expect, type Page, Locator } from '@playwright/test';
+import { jsAvailable } from './utils.js';
+
+const locators = {
+	trigger: '[data-melt-dialog-trigger]',
+	overlay: '[data-melt-dialog-overlay][data-state="open"]',
+	content: '[data-melt-dialog-content][data-state="open"]',
+};
+
+async function nav(page: Page) {
+	await page.goto('/docs/builders/dialog');
+	await jsAvailable(page);
+}
+
+test.describe.configure({ mode: 'parallel' });
+
+test.describe('dialog', () => {
+	test.beforeEach(async ({ page }) => {
+		await nav(page);
+	});
+
+	test.describe('touch device', () => {
+		test("doesn't leak events when tapping overlay", async ({ page }) => {
+			const trigger = page.locator(locators.trigger).first();
+			const content = page.locator(locators.content).first();
+			const viewCodeButton = page.getByLabel('View code').first();
+
+			await trigger.click();
+			await expect(content).toBeVisible();
+
+			const [centerX, centerY] = await getElementCenterPosition(viewCodeButton);
+
+			// Make sure "View Code" Button is not under the dialog, but under the overlay.
+			const isButtonUnderneathContent = await isPositionInsideElement(content, centerX, centerY);
+			expect(isButtonUnderneathContent).toBe(false);
+
+			// Tap the overlay at the position of the "View code" button
+			await page.tap(locators.overlay, { position: { x: centerX, y: centerY } });
+
+			await expect(content).not.toBeVisible();
+
+			// Verify the state of the "View Code" is "unchecked".
+			const viewCodeState = await viewCodeButton.getAttribute('data-state');
+			expect(viewCodeState).toBe('unchecked');
+		});
+	});
+});
+
+/**
+ * Retrieves the bounding box of an element and throws an error if `null`.
+ */
+const getBoundingBox = async (locator: Locator) => {
+	const bounding = await locator.boundingBox();
+	if (!bounding) throw new Error(`Failed to get position of '${locator}'`);
+	return bounding;
+};
+
+/**
+ * Retrieves the center position (x, y coordinates) of an element represented by a Locator.
+ */
+const getElementCenterPosition = async (locator: Locator) => {
+	const bounding = await getBoundingBox(locator);
+	return [bounding.x + bounding.width / 2, bounding.y + bounding.height / 2] as const;
+};
+
+/**
+ * Verifies an x, y position is within the boundaries of an element.
+ */
+const isPositionInsideElement = async (locator: Locator, x: number, y: number) => {
+	const bounding = await getBoundingBox(locator);
+	const isXValid = bounding.x <= x && x <= bounding.x + bounding.width;
+	const isYValid = bounding.y <= y && y <= bounding.y + bounding.height;
+	return isXValid && isYValid;
+};
diff --git a/e2e/dropdown.spec.ts b/e2e/dropdown.spec.ts
index c34dfe3d..8ac3433f 100644
--- a/e2e/dropdown.spec.ts
+++ b/e2e/dropdown.spec.ts
@@ -16,7 +16,7 @@ async function nav(page: Page) {
 
 test.describe.configure({ mode: 'parallel' });
 
-test.describe('menu', () => {
+test.describe.skip('menu', () => {
 	test.beforeEach(async ({ page }) => {
 		await nav(page);
 	});
@@ -40,7 +40,7 @@ test.describe('menu', () => {
 	}
 });
 
-test.describe('first menu item', () => {
+test.describe.skip('first menu item', () => {
 	test.beforeEach(async ({ page }) => {
 		await nav(page);
 	});
diff --git a/e2e/utils.ts b/e2e/utils.ts
index 93e98950..2c0888cc 100644
--- a/e2e/utils.ts
+++ b/e2e/utils.ts
@@ -1,5 +1,10 @@
 import { Page } from '@playwright/test';
 
 export const jsAvailable = async (page: Page) => {
+	if (process.env.CI) {
+		return await page.evaluate(() => {
+			return new Promise((resolve) => setTimeout(resolve, 0));
+		});
+	}
 	await page.waitForSelector('[data-browser]');
 };
diff --git a/src/tests/calendar/Calendar.spec.ts b/src/tests/calendar/Calendar.spec.ts
index 9cca7573..b00bc355 100644
--- a/src/tests/calendar/Calendar.spec.ts
+++ b/src/tests/calendar/Calendar.spec.ts
@@ -1,14 +1,14 @@
-import { testKbd as kbd } from '../utils.js';
+import type { CreateCalendarProps } from '$lib/builders/index.js';
+import { CalendarDate, CalendarDateTime, toZoned, type DateValue } from '@internationalized/date';
 import { render } from '@testing-library/svelte';
 import { userEvent } from '@testing-library/user-event';
 import { axe } from 'jest-axe';
-import { describe } from 'vitest';
-import CalendarTest from './CalendarTest.svelte';
-import { CalendarDate, CalendarDateTime, toZoned, type DateValue } from '@internationalized/date';
-import { writable } from 'svelte/store';
 import { tick } from 'svelte';
+import { writable } from 'svelte/store';
+import { describe } from 'vitest';
+import { testKbd as kbd } from '../utils.js';
 import CalendarMultiTest from './CalendarMultiTest.svelte';
-import type { CreateCalendarProps } from '$lib/builders/index.js';
+import CalendarTest from './CalendarTest.svelte';
 
 const calendarDate = new CalendarDate(1980, 1, 20);
 const calendarDateTime = new CalendarDateTime(1980, 1, 20, 12, 30, 0, 0);
@@ -679,6 +679,75 @@ describe('Calendar', () => {
 		}
 	});
 
+	test('dynamically change numberOfMonths option', async () => {
+		const { queryByTestId, getByTestId, user } = setup();
+
+		let grid0 = queryByTestId('grid-0');
+		let grid1 = queryByTestId('grid-1');
+		expect(grid0).toBeInTheDocument();
+		expect(grid1).toBeNull();
+
+		const numberOfMonthsButton = getByTestId('numberOfMonths');
+		await user.click(numberOfMonthsButton);
+
+		grid0 = queryByTestId('grid-0');
+		grid1 = queryByTestId('grid-1');
+		expect(grid0).toBeInTheDocument();
+		expect(grid1).toBeInTheDocument();
+	});
+
+	test('dynamically change weekStartsOn option', async () => {
+		const { getByTestId, user } = setup();
+
+		const weekDaysCopy = [...narrowWeekdays];
+
+		let weekdayElement = getByTestId(`weekdays`);
+
+		for (let i = 0; i < weekDaysCopy.length; i++) {
+			expect(weekdayElement.children[i]).toHaveTextContent(weekDaysCopy[i]);
+		}
+
+		const weekStartsOnButton = getByTestId('weekStartsOn');
+		await user.click(weekStartsOnButton);
+		const first = weekDaysCopy.shift();
+		if (first) {
+			weekDaysCopy.push(first);
+		}
+
+		weekdayElement = getByTestId(`weekdays`);
+		for (let i = 0; i < weekDaysCopy.length; i++) {
+			expect(weekdayElement.children[i]).toHaveTextContent(weekDaysCopy[i]);
+		}
+	});
+
+	test('dynamically change fixedWeeks option', async () => {
+		const { getByTestId, queryByTestId, user } = setup();
+
+		const nextButton = getByTestId('next-button');
+
+		while (queryByTestId('week-6') !== null) {
+			await user.click(nextButton);
+		}
+
+		const fixedWeeksButton = getByTestId('fixedWeeks');
+		await user.click(fixedWeeksButton);
+
+		expect(queryByTestId('week-6')).not.toBeNull();
+	});
+
+	test('dynamically changing the locale option also update weekStartsOn', async () => {
+		const { getByTestId, user } = setup();
+
+		let weekdayElement = getByTestId(`weekday-0`);
+		expect(weekdayElement).toHaveTextContent('S');
+
+		const localeButton = getByTestId('locale');
+		await user.click(localeButton);
+		weekdayElement = getByTestId(`weekday-0`);
+		// L is italian for Lunedì which is Monday (which is different from S which is Sunday)
+		expect(weekdayElement).toHaveTextContent('L');
+	});
+
 	test('custom ids are applied when provided', async () => {
 		const ids = {
 			accessibleHeading: 'id-heading',
diff --git a/src/tests/calendar/CalendarTest.svelte b/src/tests/calendar/CalendarTest.svelte
index 7159efac..81b5246f 100644
--- a/src/tests/calendar/CalendarTest.svelte
+++ b/src/tests/calendar/CalendarTest.svelte
@@ -30,7 +30,13 @@
 	const {
 		elements: { calendar, heading, grid, cell, prevButton, nextButton },
 		states: { value: insideValue, months, headingValue, weekdays },
-		options: { weekdayFormat: weekdayFormatOption },
+		options: {
+			weekdayFormat: weekdayFormatOption,
+			numberOfMonths: numberOfMonthsOption,
+			fixedWeeks: fixedWeeksOption,
+			weekStartsOn: weekStartsOnOption,
+			locale: localeOption,
+		},
 	} = createCalendar(
 		removeUndefined({
 			value,
@@ -58,6 +64,10 @@
 		})
 	);
 
+	function cycleWeekStart() {
+		$weekStartsOnOption = (($weekStartsOnOption + 1) % 7) as typeof $weekStartsOnOption;
+	}
+
 	function cycleWeekdayFormat() {
 		weekdayFormatOption.update((prev) => {
 			switch (prev) {
@@ -95,7 +105,7 @@
 					{#each $months as month, i}
 						<table use:melt={$grid} class="w-full" data-testid="grid-{i}">
 							<thead aria-hidden="true">
-								<tr>
+								<tr data-testid="weekdays">
 									{#each $weekdays as day, idx}
 										<th class="text-sm font-semibold text-magnum-800">
 											<div
@@ -134,6 +144,28 @@
 	<button on:click={cycleWeekdayFormat} data-testid="cycle-weekday-format">
 		Cycle weekdayFormat
 	</button>
+	<button
+		data-testid="numberOfMonths"
+		on:click={() => {
+			$numberOfMonthsOption++;
+		}}>numberOfMonths</button
+	>
+	<br />
+	<button data-testid="weekStartsOn" on:click={cycleWeekStart}>weekStartsOn</button>
+	<br />
+	<button
+		data-testid="fixedWeeks"
+		on:click={() => {
+			$fixedWeeksOption = !$fixedWeeksOption;
+		}}>fixedWeeksOption</button
+	>
+	<br />
+	<button
+		data-testid="locale"
+		on:click={() => {
+			$localeOption = 'it';
+		}}>setLocaleToIt</button
+	>
 </main>
 
 <style lang="postcss">
diff --git a/src/tests/combobox/Combobox.spec.ts b/src/tests/combobox/Combobox.spec.ts
index a8165609..57532d02 100644
--- a/src/tests/combobox/Combobox.spec.ts
+++ b/src/tests/combobox/Combobox.spec.ts
@@ -315,6 +315,11 @@ describe('Combobox (forceVisible)', () => {
 		const { getByTestId, queryByTestId } = render(ComboboxForceVisibleTest, { options });
 		const input = getByTestId('input');
 		const getMenu = () => queryByTestId('menu');
+		const getFirstItem = () => {
+			const firstItem = getMenu()?.querySelector('[data-melt-combobox-option]');
+			if (!firstItem) throw new Error('No option found');
+			return firstItem;
+		};
 
 		expect(input).not.toHaveValue(options[0].label);
 
@@ -323,10 +328,7 @@ describe('Combobox (forceVisible)', () => {
 		expect(getMenu()).not.toBeNull();
 		expect(getMenu()).toBeVisible();
 
-		const firstItem = getMenu()?.querySelector('[data-melt-combobox-option]');
-		if (!firstItem) throw new Error('No option found');
-
-		await user.click(firstItem);
+		await user.click(getFirstItem());
 
 		expect(getMenu()).toBeNull();
 		expect(input).toHaveValue(options[0].label);
@@ -334,7 +336,7 @@ describe('Combobox (forceVisible)', () => {
 		await user.click(input);
 		expect(getMenu()).not.toBeNull();
 		expect(getMenu()).toBeVisible();
-		expect(firstItem).toHaveAttribute('data-selected');
+		expect(getFirstItem()).toHaveAttribute('data-selected');
 	});
 
 	test('Applies custom ids when provided', async () => {
diff --git a/src/tests/date-field/DateField.spec.ts b/src/tests/date-field/DateField.spec.ts
index 1e89ce5e..34e5b186 100644
--- a/src/tests/date-field/DateField.spec.ts
+++ b/src/tests/date-field/DateField.spec.ts
@@ -1,5 +1,5 @@
 import { testKbd as kbd } from '../utils.js';
-import { render } from '@testing-library/svelte';
+import { render, waitFor } from '@testing-library/svelte';
 import { userEvent } from '@testing-library/user-event';
 import { axe } from 'jest-axe';
 import { describe } from 'vitest';
@@ -125,13 +125,14 @@ describe('DateField', () => {
 		});
 		expect(queryByTestId('dayPeriod')).not.toBeNull();
 	});
+
 	test('focuses first segment on click', async () => {
 		const { getByTestId, user } = setup();
 
 		const firstSegment = getByTestId('month');
 		await user.click(firstSegment);
 
-		expect(firstSegment).toHaveFocus();
+		await waitFor(() => expect(firstSegment).toHaveFocus());
 	});
 
 	test('increments segment on arrow up', async () => {
@@ -193,16 +194,16 @@ describe('DateField', () => {
 
 		await user.click(monthSegment);
 		await user.keyboard(kbd.ARROW_RIGHT);
-		expect(daySegment).toHaveFocus();
+		await waitFor(() => expect(daySegment).toHaveFocus());
 
 		await user.keyboard(kbd.ARROW_RIGHT);
-		expect(yearSegment).toHaveFocus();
+		await waitFor(() => expect(yearSegment).toHaveFocus());
 
 		await user.keyboard(kbd.ARROW_LEFT);
-		expect(daySegment).toHaveFocus();
+		await waitFor(() => expect(daySegment).toHaveFocus());
 
 		await user.keyboard(kbd.ARROW_LEFT);
-		expect(monthSegment).toHaveFocus();
+		await waitFor(() => expect(monthSegment).toHaveFocus());
 	});
 
 	test('navigates segments using tab', async () => {
@@ -214,10 +215,10 @@ describe('DateField', () => {
 
 		await user.click(monthSegment);
 		await user.keyboard(kbd.TAB);
-		expect(daySegment).toHaveFocus();
+		await waitFor(() => expect(daySegment).toHaveFocus());
 
 		await user.keyboard(kbd.TAB);
-		expect(yearSegment).toHaveFocus();
+		await waitFor(() => expect(yearSegment).toHaveFocus());
 	});
 
 	test('disabled prop prevents interaction', async () => {
@@ -229,16 +230,17 @@ describe('DateField', () => {
 		const yearSegment = getByTestId('year');
 
 		await user.click(monthSegment);
-		expect(monthSegment).not.toHaveFocus();
+		await waitFor(() => expect(monthSegment).not.toHaveFocus());
 		await user.keyboard(kbd.ARROW_RIGHT);
-		expect(daySegment).not.toHaveFocus();
+		await waitFor(() => expect(daySegment).not.toHaveFocus());
 		await user.keyboard(kbd.ARROW_RIGHT);
+		await waitFor(() => expect(yearSegment).not.toHaveFocus());
 		expect(yearSegment).not.toHaveFocus();
 
 		await user.click(daySegment);
-		expect(daySegment).not.toHaveFocus();
+		await waitFor(() => expect(daySegment).not.toHaveFocus());
 		await user.click(yearSegment);
-		expect(yearSegment).not.toHaveFocus();
+		await waitFor(() => expect(yearSegment).not.toHaveFocus());
 	});
 
 	test('readonly prop prevents modifying segments', async () => {
@@ -250,11 +252,12 @@ describe('DateField', () => {
 
 		for (const segment of segments) {
 			const el = getByTestId(segment);
+
 			expect(el).toHaveTextContent(String(calendarDateOther[segment]));
 			await user.click(el);
-			expect(el).toHaveFocus();
+			await waitFor(() => expect(el).toHaveFocus());
 			await user.keyboard(kbd.ARROW_UP);
-			expect(el).toHaveTextContent(String(calendarDateOther[segment]));
+			await waitFor(() => expect(el).toHaveTextContent(String(calendarDateOther[segment])));
 		}
 	});
 
@@ -268,17 +271,17 @@ describe('DateField', () => {
 		const monthSegment = getByTestId('month');
 		expect(monthSegment).toHaveTextContent(String(calendarDateOther['month']));
 		await user.click(monthSegment);
-		expect(monthSegment).toHaveFocus();
+		await waitFor(() => expect(monthSegment).toHaveFocus());
 		await user.keyboard(kbd.ARROW_UP);
-		expect(monthSegment).toHaveTextContent(String(calendarDateOther['month']));
+		await waitFor(() => expect(monthSegment).toHaveTextContent(String(calendarDateOther['month'])));
 
 		// day should change
 		const daySegment = getByTestId('day');
-		expect(daySegment).toHaveTextContent(String(calendarDateOther['day']));
+		await waitFor(() => expect(daySegment).toHaveTextContent(String(calendarDateOther['day'])));
 		await user.click(daySegment);
-		expect(daySegment).toHaveFocus();
+		await waitFor(() => expect(daySegment).toHaveFocus());
 		await user.keyboard(kbd.ARROW_UP);
-		expect(daySegment).toHaveTextContent(String(calendarDateOther['day'] + 1));
+		await waitFor(() => expect(daySegment).toHaveTextContent(String(calendarDateOther['day'] + 1)));
 	});
 
 	test('if selected date unavailable, mark field as invalid', async () => {
@@ -586,7 +589,7 @@ describe('DateField', () => {
 		expect(secondSegment).toHaveTextContent('00');
 	});
 
-	test('displays correct timezone with ZonedDateTime value - now', async () => {
+	test.skip('displays correct timezone with ZonedDateTime value - now', async () => {
 		const { getByTestId } = setup({
 			defaultValue: now('America/Los_Angeles'),
 		});
@@ -669,14 +672,11 @@ describe('DateField', () => {
 	});
 });
 
-function isDaylightSavingsTime(): boolean {
+function isDaylightSavingsTime() {
 	const now = new Date();
-	const january = new Date(now.getFullYear(), 0, 1);
-	const july = new Date(now.getFullYear(), 6, 1);
-	const timezoneOffset = now.getTimezoneOffset();
-	const isDaylightSavingsTime =
-		timezoneOffset < Math.max(january.getTimezoneOffset(), july.getTimezoneOffset());
-	return isDaylightSavingsTime;
+	const januaryOffset = new Date(now.getFullYear(), 0, 1).getTimezoneOffset();
+	const julyOffset = new Date(now.getFullYear(), 6, 1).getTimezoneOffset();
+	return januaryOffset !== julyOffset;
 }
 
 function thisTimeZone(date: string): string {
diff --git a/src/tests/date-picker/DatePicker.spec.ts b/src/tests/date-picker/DatePicker.spec.ts
index 2ff9089e..e7f973b4 100644
--- a/src/tests/date-picker/DatePicker.spec.ts
+++ b/src/tests/date-picker/DatePicker.spec.ts
@@ -560,4 +560,66 @@ describe('DatePicker', () => {
 
 		expect(trigger).toBeDisabled();
 	});
+
+	test('dynamically change numberOfMonths option even if numberOfMonths is specified as an option', async () => {
+		const { queryByTestId, getByTestId, user } = setup({
+			numberOfMonths: 1,
+		});
+
+		let grid0 = queryByTestId('grid-0');
+		let grid1 = queryByTestId('grid-1');
+		expect(grid0).toBeInTheDocument();
+		expect(grid1).toBeNull();
+
+		const numberOfMonthsButton = getByTestId('numberOfMonths');
+		await user.click(numberOfMonthsButton);
+
+		grid0 = queryByTestId('grid-0');
+		grid1 = queryByTestId('grid-1');
+		expect(grid0).toBeInTheDocument();
+		expect(grid1).toBeInTheDocument();
+	});
+
+	test('dynamically change weekStartsOn option even if weekStartsOn option is set', async () => {
+		const { getByTestId, user } = setup({
+			weekStartsOn: 0,
+		});
+
+		const weekDaysCopy = [...narrowWeekdays];
+
+		let weekdayElement = getByTestId(`weekdays`);
+
+		for (let i = 0; i < weekDaysCopy.length; i++) {
+			expect(weekdayElement.children[i]).toHaveTextContent(weekDaysCopy[i]);
+		}
+
+		const weekStartsOnButton = getByTestId('weekStartsOn');
+		await user.click(weekStartsOnButton);
+		const first = weekDaysCopy.shift();
+		if (first) {
+			weekDaysCopy.push(first);
+		}
+
+		weekdayElement = getByTestId(`weekdays`);
+		for (let i = 0; i < weekDaysCopy.length; i++) {
+			expect(weekdayElement.children[i]).toHaveTextContent(weekDaysCopy[i]);
+		}
+	});
+
+	test('dynamically change fixedWeeks option even if fixedWeeks option is set', async () => {
+		const { getByTestId, queryByTestId, user } = setup({
+			fixedWeeks: false,
+		});
+
+		const nextButton = getByTestId('next-button');
+
+		while (queryByTestId('week-6') !== null) {
+			await user.click(nextButton);
+		}
+
+		const fixedWeeksButton = getByTestId('fixedWeeks');
+		await user.click(fixedWeeksButton);
+
+		expect(queryByTestId('week-6')).not.toBeNull();
+	});
 });
diff --git a/src/tests/date-picker/DatePickerTest.svelte b/src/tests/date-picker/DatePickerTest.svelte
index 86596a9b..56a2ece7 100644
--- a/src/tests/date-picker/DatePickerTest.svelte
+++ b/src/tests/date-picker/DatePickerTest.svelte
@@ -27,6 +27,7 @@
 	export let placeholder: CreateDatePickerProps['placeholder'] = undefined;
 	export let weekStartsOn: CreateDatePickerProps['weekStartsOn'] = undefined;
 	export let weekdayFormat: CreateDatePickerProps['weekdayFormat'] = undefined;
+	export let fixedWeeks: CreateDatePickerProps['fixedWeeks'] = undefined;
 	export let onOutsideClick: CreateDatePickerProps['onOutsideClick'] = undefined;
 
 	const {
@@ -45,7 +46,13 @@
 			validation,
 		},
 		states: { value: insideValue, months, headingValue, weekdays, segmentContents },
-		options: { locale: insideLocale, weekdayFormat: weekdayFormatOption },
+		options: {
+			locale: insideLocale,
+			weekdayFormat: weekdayFormatOption,
+			numberOfMonths: numberOfMonthsOption,
+			fixedWeeks: fixedWeeksOption,
+			weekStartsOn: weekStartsOnOption,
+		},
 	} = createDatePicker(
 		removeUndefined({
 			value,
@@ -72,9 +79,14 @@
 			weekdayFormat,
 			popoverIds,
 			onOutsideClick,
+			fixedWeeks,
 		})
 	);
 
+	function cycleWeekStart() {
+		$weekStartsOnOption = (($weekStartsOnOption + 1) % 7) as typeof $weekStartsOnOption;
+	}
+
 	function cycleWeekdayFormat() {
 		weekdayFormatOption.update((prev) => {
 			switch (prev) {
@@ -138,7 +150,7 @@
 				{@const { weeks } = month}
 				<table use:melt={$grid} class="w-full" data-testid="grid-{i}">
 					<thead aria-hidden="true">
-						<tr>
+						<tr data-testid="weekdays">
 							{#each $weekdays as day, idx}
 								<th class="text-sm font-semibold text-magnum-800">
 									<div
@@ -152,8 +164,8 @@
 						</tr>
 					</thead>
 					<tbody>
-						{#each weeks as days}
-							<tr>
+						{#each weeks as days, i}
+							<tr data-testid="week-{i + 1}">
 								{#each days as date}
 									<td role="gridcell">
 										<div
@@ -174,6 +186,21 @@
 		<button on:click={cycleWeekdayFormat} data-testid="cycle-weekday-format">
 			Cycle weekdayFormat
 		</button>
+		<button
+			data-testid="numberOfMonths"
+			on:click={() => {
+				$numberOfMonthsOption++;
+			}}>numberOfMonths</button
+		>
+		<br />
+		<button data-testid="weekStartsOn" on:click={cycleWeekStart}>weekStartsOn</button>
+		<br />
+		<button
+			data-testid="fixedWeeks"
+			on:click={() => {
+				$fixedWeeksOption = !$fixedWeeksOption;
+			}}>fixedWeeksOption</button
+		>
 	</div>
 </main>
 
diff --git a/src/tests/dialog/Dialog.spec.ts b/src/tests/dialog/Dialog.spec.ts
index c9dedae1..384a138a 100644
--- a/src/tests/dialog/Dialog.spec.ts
+++ b/src/tests/dialog/Dialog.spec.ts
@@ -230,6 +230,30 @@ describe('Dialog', () => {
 		await waitFor(() => expect(content).not.toBeVisible());
 	});
 
+	it('Respects the `closeOnEscape` prop', async () => {
+		const { getByTestId, user, trigger } = setup({
+			closeOnEscape: false,
+		});
+		expect(trigger).toBeVisible();
+		const content = getByTestId('content');
+		expect(content).not.toBeVisible();
+		await user.click(trigger);
+		expect(content).toBeVisible();
+		await user.keyboard(kbd.ESCAPE);
+		expect(content).toBeVisible();
+	});
+
+	it("Doesn't close on escape if child intercepts event", async () => {
+		const { getByTestId, user, trigger } = setup();
+		await user.click(trigger);
+		const content = getByTestId('content');
+		expect(content).toBeVisible();
+		const input = getByTestId('input-keydown-interceptor');
+		input.focus();
+		await user.keyboard(kbd.ESCAPE);
+		expect(content).toBeVisible();
+	});
+
 	it('Applies custom ids when provided', async () => {
 		const ids = {
 			content: 'id-content',
@@ -277,4 +301,34 @@ describe('Dialog', () => {
 		await user.pointer({ target: overlay, offset: 2, keys: '[/MouseLeft]' });
 		expect(content).not.toBeVisible();
 	});
+
+	it("Doesn't deactivate focus trap on escape provided `closeOnEscape` false", async () => {
+		const { getByTestId, user, trigger } = setup({
+			closeOnEscape: false,
+		});
+		const content = getByTestId('content');
+		expect(content).not.toBeVisible();
+		await user.click(trigger);
+		expect(content).toBeVisible();
+		await user.keyboard(kbd.ESCAPE);
+		expect(content).toBeVisible();
+		expect(getByTestId('content')).toHaveFocus();
+		await user.tab({ shift: true });
+		expect(getByTestId('floating-closer')).not.toHaveFocus();
+	});
+
+	it("Doesn't deactivate focus trap on outside click provided `closeOnOutsideClick` false", async () => {
+		const { getByTestId, user, trigger } = setup({
+			closeOnOutsideClick: false,
+		});
+		const content = getByTestId('content');
+		expect(content).not.toBeVisible();
+		await user.click(trigger);
+		expect(content).toBeVisible();
+		await user.click(getByTestId('overlay'));
+		expect(content).toBeVisible();
+		expect(getByTestId('content')).toHaveFocus();
+		await user.tab({ shift: true });
+		expect(getByTestId('floating-closer')).not.toHaveFocus();
+	});
 });
diff --git a/src/tests/dialog/DialogNestedTest.svelte b/src/tests/dialog/DialogNestedTest.svelte
index 110398c8..ac5f2820 100644
--- a/src/tests/dialog/DialogNestedTest.svelte
+++ b/src/tests/dialog/DialogNestedTest.svelte
@@ -22,8 +22,8 @@
 
 <main>
 	<button use:melt={$trigger} data-testid="trigger">Open</button>
-	<div use:melt={$portalled} data-testid="portalled">
-		{#if $open}
+	{#if $open}
+		<div use:melt={$portalled} data-testid="portalled">
 			<div use:melt={$overlay} data-testid="overlay" transition:fade />
 			<div use:melt={$content} data-testid="content">
 				<h2 use:melt={$title}>Title</h2>
@@ -31,8 +31,8 @@
 
 				<button use:melt={$close} data-testid="closer">Close</button>
 				<button use:melt={$triggerA} data-testid="triggerA">Close2</button>
-				<div use:melt={$portalledA} data-testid="portalledA">
-					{#if $openA}
+				{#if $openA}
+					<div use:melt={$portalledA} data-testid="portalledA">
 						<div use:melt={$overlayA} data-testid="overlayA" />
 						<div use:melt={$contentA} data-testid="contentA" transition:fade>
 							<h2 use:melt={$titleA}>Title</h2>
@@ -41,11 +41,11 @@
 							<button use:melt={$closeA} data-testid="closerA">Close</button>
 							<button use:melt={$closeA} data-testid="lastA">Close2</button>
 						</div>
-					{/if}
-				</div>
+					</div>
+				{/if}
 			</div>
-		{/if}
-	</div>
+		</div>
+	{/if}
 </main>
 
 <style>
diff --git a/src/tests/dialog/DialogTest.svelte b/src/tests/dialog/DialogTest.svelte
index 0c0b213a..60d8e2ea 100644
--- a/src/tests/dialog/DialogTest.svelte
+++ b/src/tests/dialog/DialogTest.svelte
@@ -1,5 +1,6 @@
 <script lang="ts">
 	import { createDialog, melt, type CreateDialogProps } from '$lib/index.js';
+	import { kbd } from '$lib/internal/helpers/keyboard.js';
 
 	type $$Props = CreateDialogProps;
 
@@ -19,6 +20,11 @@
 		<div use:melt={$content} data-testid="content">
 			<h2 use:melt={$title} data-testid="title">Title</h2>
 			<p use:melt={$description} data-testid="description">Description</p>
+			<input
+				data-testid="input-keydown-interceptor"
+				type="text"
+				on:keydown={(e) => e.key === kbd.ESCAPE && e.stopPropagation()}
+			/>
 
 			<button use:melt={$close} data-testid="closer">Close</button>
 			<button use:melt={$close} data-testid="last">Close</button>
diff --git a/src/tests/popover/Popover.spec.ts b/src/tests/popover/Popover.spec.ts
index 676c4f24..7d1f5664 100644
--- a/src/tests/popover/Popover.spec.ts
+++ b/src/tests/popover/Popover.spec.ts
@@ -135,4 +135,33 @@ describe('Popover (Default)', () => {
 		expect(trigger.id).toBe(ids.trigger);
 		expect(content.id).toBe(ids.content);
 	});
+
+	it("Doesn't deactivate focus trap on escape provided `closeOnEscape` false", async () => {
+		const { getByTestId, user, trigger } = setup({
+			closeOnEscape: false,
+		});
+		const content = getByTestId('content');
+		expect(content).not.toBeVisible();
+		await user.click(trigger);
+		expect(content).toBeVisible();
+		await user.keyboard(kbd.ESCAPE);
+		expect(content).toBeVisible();
+		expect(getByTestId('content')).toHaveFocus();
+		await user.tab({ shift: true });
+		expect(getByTestId('trigger')).not.toHaveFocus();
+	});
+
+	it("Doesn't deactivate focus trap on outside click provided `closeOnOutsideClick` false", async () => {
+		const { getByTestId, user, trigger } = setup({
+			closeOnOutsideClick: false,
+		});
+		const content = getByTestId('content');
+		expect(content).not.toBeVisible();
+		await user.click(trigger);
+		expect(content).toBeVisible();
+		await user.click(getByTestId('outside'));
+		expect(content).toBeVisible();
+		await user.tab();
+		expect(getByTestId('closeFocus')).not.toHaveFocus();
+	});
 });
diff --git a/src/tests/portal/PopoverTooltip.svelte b/src/tests/portal/PopoverTooltip.svelte
new file mode 100644
index 00000000..c73ea7f3
--- /dev/null
+++ b/src/tests/portal/PopoverTooltip.svelte
@@ -0,0 +1,35 @@
+<script lang="ts">
+	import { createPopover, createTooltip, melt } from '$lib/index.js';
+	import { Settings2 } from '$icons/index.js';
+
+	const {
+		elements: { trigger, content, arrow, close },
+	} = createPopover();
+
+	const {
+		elements: { trigger: ttTrigger, content: ttContent },
+	} = createTooltip({
+		openDelay: 0,
+		closeDelay: 0,
+	});
+</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={$ttTrigger} data-testid="tooltip-trigger"> Open tooltip </button>
+	<div use:melt={$ttContent} data-testid="tooltip-content">Tooltip content</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/Portal.spec.ts b/src/tests/portal/Portal.spec.ts
new file mode 100644
index 00000000..89a3c56e
--- /dev/null
+++ b/src/tests/portal/Portal.spec.ts
@@ -0,0 +1,69 @@
+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 { sleep } from '$lib/internal/helpers/sleep.js';
+
+function setupPopoverTooltip() {
+	const user = userEvent.setup();
+	const returned = render(PopoverTooltip);
+	const popoverTrigger = returned.getByTestId('popover-trigger');
+	const popoverContent = returned.getByTestId('popover-content');
+	const tooltipTrigger = returned.getByTestId('tooltip-trigger');
+	const tooltipContent = returned.getByTestId('tooltip-content');
+	const outside = returned.getByTestId('outside');
+	const elements = { popoverTrigger, popoverContent, tooltipTrigger, tooltipContent, outside };
+	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();
+	});
+
+	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();
+	});
+
+	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();
+		await sleep(100);
+		expect(elements.outside).toBeVisible();
+		await user.click(elements.outside);
+		await waitFor(() => expect(elements.tooltipContent).not.toBeVisible());
+		await waitFor(() => expect(elements.popoverContent).not.toBeVisible());
+	});
+});
diff --git a/src/tests/range-calendar/RangeCalendar.spec.ts b/src/tests/range-calendar/RangeCalendar.spec.ts
index 5f9744a6..3bb62e3e 100644
--- a/src/tests/range-calendar/RangeCalendar.spec.ts
+++ b/src/tests/range-calendar/RangeCalendar.spec.ts
@@ -501,6 +501,75 @@ describe('Range Calendar', () => {
 		}
 	});
 
+	test('dynamically change numberOfMonths option', async () => {
+		const { queryByTestId, getByTestId, user } = setup();
+
+		let grid0 = queryByTestId('grid-0');
+		let grid1 = queryByTestId('grid-1');
+		expect(grid0).toBeInTheDocument();
+		expect(grid1).toBeNull();
+
+		const numberOfMonthsButton = getByTestId('numberOfMonths');
+		await user.click(numberOfMonthsButton);
+
+		grid0 = queryByTestId('grid-0');
+		grid1 = queryByTestId('grid-1');
+		expect(grid0).toBeInTheDocument();
+		expect(grid1).toBeInTheDocument();
+	});
+
+	test('dynamically change weekStartsOn option', async () => {
+		const { getByTestId, user } = setup();
+
+		const weekDaysCopy = [...narrowWeekdays];
+
+		let weekdayElement = getByTestId(`weekdays`);
+
+		for (let i = 0; i < weekDaysCopy.length; i++) {
+			expect(weekdayElement.children[i]).toHaveTextContent(weekDaysCopy[i]);
+		}
+
+		const weekStartsOnButton = getByTestId('weekStartsOn');
+		await user.click(weekStartsOnButton);
+		const first = weekDaysCopy.shift();
+		if (first) {
+			weekDaysCopy.push(first);
+		}
+
+		weekdayElement = getByTestId(`weekdays`);
+		for (let i = 0; i < weekDaysCopy.length; i++) {
+			expect(weekdayElement.children[i]).toHaveTextContent(weekDaysCopy[i]);
+		}
+	});
+
+	test('dynamically change fixedWeeks option', async () => {
+		const { getByTestId, queryByTestId, user } = setup();
+
+		const nextButton = getByTestId('next-button');
+
+		while (queryByTestId('week-6') !== null) {
+			await user.click(nextButton);
+		}
+
+		const fixedWeeksButton = getByTestId('fixedWeeks');
+		await user.click(fixedWeeksButton);
+
+		expect(queryByTestId('week-6')).not.toBeNull();
+	});
+
+	test('dynamically changing the locale option also update weekStartsOn', async () => {
+		const { getByTestId, user } = setup();
+
+		let weekdayElement = getByTestId(`weekday-0`);
+		expect(weekdayElement).toHaveTextContent('S');
+
+		const localeButton = getByTestId('locale');
+		await user.click(localeButton);
+		weekdayElement = getByTestId(`weekday-0`);
+		// L is italian for Lunedì which is Monday (which is different from S which is Sunday)
+		expect(weekdayElement).toHaveTextContent('L');
+	});
+
 	test('custom ids are applied when provided', async () => {
 		const ids = {
 			accessibleHeading: 'id-heading',
diff --git a/src/tests/range-calendar/RangeCalendarTest.svelte b/src/tests/range-calendar/RangeCalendarTest.svelte
index f9160fe1..4961f63b 100644
--- a/src/tests/range-calendar/RangeCalendarTest.svelte
+++ b/src/tests/range-calendar/RangeCalendarTest.svelte
@@ -27,7 +27,13 @@
 	const {
 		elements: { calendar, heading, grid, cell, prevButton, nextButton },
 		states: { value: insideValue, months, headingValue, weekdays },
-		options: { weekdayFormat: weekdayFormatOption },
+		options: {
+			weekdayFormat: weekdayFormatOption,
+			numberOfMonths: numberOfMonthsOption,
+			fixedWeeks: fixedWeeksOption,
+			locale: localeOption,
+			weekStartsOn: weekStartsOnOption,
+		},
 	} = createRangeCalendar(
 		removeUndefined({
 			value,
@@ -64,6 +70,10 @@
 			}
 		});
 	}
+
+	function cycleWeekStart() {
+		$weekStartsOnOption = (($weekStartsOnOption + 1) % 7) as typeof $weekStartsOnOption;
+	}
 </script>
 
 <main class="flex h-full">
@@ -91,7 +101,7 @@
 					{@const { weeks } = month}
 					<table use:melt={$grid} class="w-full" data-testid="grid-{i}">
 						<thead aria-hidden="true">
-							<tr>
+							<tr data-testid="weekdays">
 								{#each $weekdays as day, idx}
 									<th class="text-sm font-semibold text-magnum-800">
 										<div
@@ -129,6 +139,28 @@
 	<button on:click={cycleWeekdayFormat} data-testid="cycle-weekday-format">
 		Cycle weekdayFormat
 	</button>
+	<button
+		data-testid="numberOfMonths"
+		on:click={() => {
+			$numberOfMonthsOption++;
+		}}>numberOfMonths</button
+	>
+	<br />
+	<button data-testid="weekStartsOn" on:click={cycleWeekStart}>weekStartsOn</button>
+	<br />
+	<button
+		data-testid="fixedWeeks"
+		on:click={() => {
+			$fixedWeeksOption = !$fixedWeeksOption;
+		}}>fixedWeeksOption</button
+	>
+	<br />
+	<button
+		data-testid="locale"
+		on:click={() => {
+			$localeOption = 'it';
+		}}>setLocaleToIt</button
+	>
 </main>
 
 <style lang="postcss">
diff --git a/src/tests/scroll-area/ScrollArea.spec.ts b/src/tests/scroll-area/ScrollArea.spec.ts
new file mode 100644
index 00000000..4b7a0c77
--- /dev/null
+++ b/src/tests/scroll-area/ScrollArea.spec.ts
@@ -0,0 +1,96 @@
+import { render, waitFor, fireEvent } from '@testing-library/svelte';
+import { describe, it } from 'vitest';
+import { userEvent } from '@testing-library/user-event';
+import ScrollAreaTest from './ScrollAreaTest.svelte';
+import type { CreateScrollAreaProps } from '$lib/index.js';
+
+function setup(
+	props?: CreateScrollAreaProps & {
+		height?: string;
+		width?: string;
+	}
+) {
+	const user = userEvent.setup();
+	const returned = render(ScrollAreaTest, { props });
+	const root = returned.getByTestId('root');
+	const scrollbarX = returned.getByTestId('scrollbar-x');
+	const scrollbarY = returned.getByTestId('scrollbar-y');
+	const content = returned.getByTestId('content');
+	const thumbX = returned.getByTestId('thumb-x');
+	const thumbY = returned.getByTestId('thumb-y');
+
+	return {
+		user,
+		elements: {
+			scrollbarX,
+			scrollbarY,
+			content,
+			thumbX,
+			thumbY,
+			root,
+		},
+		...returned,
+	};
+}
+
+describe('Scroll Area', () => {
+	it('Displays the scrollbars when `type` is "always"', async () => {
+		const { elements } = setup({
+			type: 'always',
+		});
+
+		expect(elements.scrollbarX).toBeVisible();
+		expect(elements.scrollbarY).toBeVisible();
+	});
+
+	it('Displays the x scrollbar when `type` is "auto" and content overflows', async () => {
+		const { elements } = setup({
+			type: 'always',
+			width: '10px',
+		});
+
+		expect(elements.scrollbarX).toBeVisible();
+	});
+
+	it('Displays the y scrollbar when `type` is "auto" and content overflows', async () => {
+		const { elements } = setup({
+			type: 'always',
+			height: '10px',
+		});
+
+		expect(elements.scrollbarY).toBeVisible();
+	});
+
+	it("Doesn't display the scrollbars when `type` is 'auto' and content doesn't overflow", async () => {
+		const { elements } = setup({
+			type: 'auto',
+			width: '5000px',
+			height: '5000px',
+		});
+
+		await waitFor(() => expect(elements.scrollbarX).not.toBeVisible());
+		await waitFor(() => expect(elements.scrollbarY).not.toBeVisible());
+	});
+
+	// I hate vitest/testing library sometimes.
+	it.skip('Displays the scrollbars on hover', async () => {
+		const { elements } = setup({
+			type: 'hover',
+			width: '10px',
+			height: '10px',
+		});
+
+		await waitFor(() => expect(elements.scrollbarX).not.toBeVisible());
+		await waitFor(() => expect(elements.scrollbarY).not.toBeVisible());
+
+		await fireEvent.pointerEnter(elements.root);
+
+		await waitFor(() => expect(elements.scrollbarX).toBeVisible());
+		await waitFor(() => expect(elements.scrollbarY).toBeVisible());
+
+		await fireEvent.pointerLeave(elements.root);
+
+		await waitFor(() => expect(elements.scrollbarX).not.toBeVisible());
+		await waitFor(() => expect(elements.scrollbarY).not.toBeVisible());
+	});
+});
diff --git a/src/tests/scroll-area/ScrollAreaTest.svelte b/src/tests/scroll-area/ScrollAreaTest.svelte
new file mode 100644
index 00000000..b8226e6e
--- /dev/null
+++ b/src/tests/scroll-area/ScrollAreaTest.svelte
@@ -0,0 +1,99 @@
+<script lang="ts">
+	import { createScrollArea, melt, type CreateScrollAreaProps } from '$lib/index.js';
+	import { removeUndefined } from '../utils.js';
+
+	const flavors = [
+		'Vanilla',
+		'Chocolate',
+		'Strawberry',
+		'Mint Chocolate Chip',
+		'Cookies and Cream',
+		'Rocky Road',
+		'Pistachio',
+		'Neapolitan',
+		'Butter Pecan',
+		'Salted Caramel',
+		'Coffee',
+		'Mango',
+		'Raspberry Ripple',
+		'Lemon Sorbet',
+		'Green Tea',
+		'Coconut',
+		'Black Cherry',
+		'Banana',
+		'Almond Fudge',
+		'Cinnamon',
+		'Blueberry Cheesecake',
+		'Tiramisu',
+		'Red Velvet',
+		'Matcha',
+		'Peanut Butter Cup',
+		'Cookie Dough',
+		'Rum Raisin',
+		'Birthday Cake',
+		'Lychee',
+		'Honey Lavender',
+	];
+
+	type $$Props = CreateScrollAreaProps & {
+		height?: string;
+		width?: string;
+	};
+
+	export let height = '288px';
+	export let width = '490px';
+
+	export let type: $$Props['type'] = 'hover';
+
+	const {
+		elements: { root, content, viewport, corner, scrollbarY, thumbY, scrollbarX, thumbX },
+	} = createScrollArea(
+		removeUndefined({
+			type,
+			...$$restProps,
+		})
+	);
+</script>
+
+<div
+	use:melt={$root}
+	class="relative overflow-hidden rounded-md border bg-white text-magnum-900 shadow-lg"
+	data-testid="root"
+	style:width
+	style:height
+>
+	<div use:melt={$viewport} class="h-full w-full rounded-[inherit]" data-testid="viewport">
+		<div use:melt={$content} data-testid="content">
+			<div class="w-[300px] p-4" data-testid="inner-content">
+				<h4 class="mb-4 font-semibold leading-none">Endless Flavors</h4>
+				<div data-testid="hover">Hover me please</div>
+				{#each flavors as flavor (flavor)}
+					<div class="text-sm">
+						{flavor}
+					</div>
+					<div role="separator" class="my-2 h-px w-full bg-magnum-600" />
+				{/each}
+			</div>
+		</div>
+	</div>
+	<div
+		use:melt={$scrollbarY}
+		data-testid="scrollbar-y"
+		class="flex h-full w-2.5 border-l border-l-transparent bg-magnum-800/10 p-px"
+	>
+		<div
+			use:melt={$thumbY}
+			data-testid="thumb-y"
+			class="relative flex-1 rounded-full bg-magnum-600"
+		/>
+	</div>
+
+	<div
+		use:melt={$scrollbarX}
+		data-testid="scrollbar-x"
+		class="flex h-full w-2.5 border-l border-l-transparent bg-magnum-800/10 p-px"
+	>
+		<div use:melt={$thumbX} data-testid="thumb-x" class="relative rounded-full bg-magnum-600" />
+	</div>
+	<div use:melt={$corner} />
+</div>
diff --git a/src/tests/slider/Slider.spec.ts b/src/tests/slider/Slider.spec.ts
index df42592f..d6f99ba7 100644
--- a/src/tests/slider/Slider.spec.ts
+++ b/src/tests/slider/Slider.spec.ts
@@ -1,4 +1,4 @@
-import { kbd } from '$lib/internal/helpers/index.js';
+import { testKbd as kbd } from '../utils.js';
 import { act, render } from '@testing-library/svelte';
 import { userEvent } from '@testing-library/user-event';
 import { axe } from 'jest-axe';
@@ -89,7 +89,7 @@ describe('Slider (Default)', () => {
 		const range = getByTestId('range');
 
 		await act(() => thumb.focus());
-		await user.keyboard(`{${key}}`);
+		await user.keyboard(key);
 
 		expectPercentage({ percentage: 31, thumb, range });
 	});
@@ -102,7 +102,7 @@ describe('Slider (Default)', () => {
 		const range = getByTestId('range');
 
 		await act(() => thumb.focus());
-		await user.keyboard(`{${key}}`);
+		await user.keyboard(key);
 
 		expectPercentage({ percentage: 29, thumb, range });
 	});
@@ -115,7 +115,7 @@ describe('Slider (Default)', () => {
 		const range = getByTestId('range');
 
 		await act(() => thumb.focus());
-		await user.keyboard(`{${kbd.HOME}}`);
+		await user.keyboard(kbd.HOME);
 
 		expectPercentage({ percentage: 0, thumb, range });
 	});
@@ -127,7 +127,7 @@ describe('Slider (Default)', () => {
 		const range = getByTestId('range');
 
 		await act(() => thumb.focus());
-		await user.keyboard(`{${kbd.END}}`);
+		await user.keyboard(kbd.END);
 
 		expectPercentage({ percentage: 100, thumb, range });
 	});
@@ -172,7 +172,7 @@ describe('Slider (Range)', () => {
 			const range = getByTestId('range');
 
 			await act(() => thumb0.focus());
-			await user.keyboard(`{${key}}`);
+			await user.keyboard(key);
 
 			expectPercentages({ percentages: [21, 80], thumbs: [thumb0, thumb1], range });
 		}
@@ -189,7 +189,7 @@ describe('Slider (Range)', () => {
 			const range = getByTestId('range');
 
 			await act(() => thumb1.focus());
-			await user.keyboard(`{${key}}`);
+			await user.keyboard(key);
 
 			expectPercentages({ percentages: [20, 81], thumbs: [thumb0, thumb1], range });
 		}
@@ -206,7 +206,7 @@ describe('Slider (Range)', () => {
 			const range = getByTestId('range');
 
 			await act(() => thumb0.focus());
-			await user.keyboard(`{${key}}`);
+			await user.keyboard(key);
 
 			expectPercentages({ percentages: [19, 80], thumbs: [thumb0, thumb1], range });
 		}
@@ -223,7 +223,7 @@ describe('Slider (Range)', () => {
 			const range = getByTestId('range');
 
 			await act(() => thumb1.focus());
-			await user.keyboard(`{${key}}`);
+			await user.keyboard(key);
 
 			expectPercentages({ percentages: [20, 79], thumbs: [thumb0, thumb1], range });
 		}
@@ -242,9 +242,9 @@ describe('Slider (Range)', () => {
 			const range = getByTestId('range');
 
 			await act(() => thumb0.focus());
-			await user.keyboard(`{${key}}`);
-			await user.keyboard(`{${key}}`);
-			await user.keyboard(`{${key}}`);
+			await user.keyboard(key);
+			await user.keyboard(key);
+			await user.keyboard(key);
 
 			expectPercentages({ percentages: [51, 52], thumbs: [thumb0, thumb1], range });
 			expect(thumb1).toHaveFocus();
@@ -264,9 +264,9 @@ describe('Slider (Range)', () => {
 			const range = getByTestId('range');
 
 			await act(() => thumb1.focus());
-			await user.keyboard(`{${key}}`);
-			await user.keyboard(`{${key}}`);
-			await user.keyboard(`{${key}}`);
+			await user.keyboard(key);
+			await user.keyboard(key);
+			await user.keyboard(key);
 
 			expectPercentages({ percentages: [48, 49], thumbs: [thumb0, thumb1], range });
 			expect(thumb0).toHaveFocus();
@@ -282,7 +282,7 @@ describe('Slider (Range)', () => {
 		const range = getByTestId('range');
 
 		await act(() => thumb0.focus());
-		await user.keyboard(`{${kbd.HOME}}`);
+		await user.keyboard(kbd.HOME);
 
 		expectPercentages({ percentages: [0, 80], thumbs: [thumb0, thumb1], range });
 	});
@@ -296,7 +296,7 @@ describe('Slider (Range)', () => {
 		const range = getByTestId('range');
 
 		await act(() => thumb1.focus());
-		await user.keyboard(`{${kbd.END}}`);
+		await user.keyboard(kbd.END);
 
 		expectPercentages({ percentages: [20, 100], thumbs: [thumb0, thumb1], range });
 	});
@@ -310,7 +310,7 @@ describe('Slider (Range)', () => {
 		const range = getByTestId('range');
 
 		await act(() => thumb1.focus());
-		await user.keyboard(`{${kbd.HOME}}`);
+		await user.keyboard(kbd.HOME);
 
 		expectPercentages({ percentages: [0, 20], thumbs: [thumb0, thumb1], range });
 		expect(thumb0).toHaveFocus();
@@ -325,7 +325,7 @@ describe('Slider (Range)', () => {
 		const range = getByTestId('range');
 
 		await act(() => thumb0.focus());
-		await user.keyboard(`{${kbd.END}}`);
+		await user.keyboard(kbd.END);
 
 		expectPercentages({ percentages: [80, 100], thumbs: [thumb0, thumb1], range });
 		expect(thumb1).toHaveFocus();
@@ -361,7 +361,7 @@ describe('Slider (Small min, max, step)', () => {
 		const range = getByTestId('range');
 
 		await act(() => thumb.focus());
-		await user.keyboard(`{${key}}`);
+		await user.keyboard(key);
 
 		expectPercentage({ percentage: 51, thumb, range });
 	});
@@ -379,7 +379,7 @@ describe('Slider (Small min, max, step)', () => {
 		const range = getByTestId('range');
 
 		await act(() => thumb.focus());
-		await user.keyboard(`{${key}}`);
+		await user.keyboard(key);
 
 		expectPercentage({ percentage: 49, thumb, range });
 	});
@@ -414,7 +414,7 @@ describe('Slider (negative min)', () => {
 		const range = getByTestId('range');
 
 		await act(() => thumb.focus());
-		await user.keyboard(`{${key}}`);
+		await user.keyboard(key);
 
 		expectPercentage({ percentage: 51, thumb, range });
 	});
@@ -432,7 +432,7 @@ describe('Slider (negative min)', () => {
 		const range = getByTestId('range');
 
 		await act(() => thumb.focus());
-		await user.keyboard(`{${key}}`);
+		await user.keyboard(key);
 
 		expectPercentage({ percentage: 49, thumb, range });
 	});
@@ -581,10 +581,10 @@ describe('Slider (min=0, max=100, step=30)', () => {
 		expect(thumb).toBeInTheDocument();
 
 		await act(() => thumb.focus());
-		await user.keyboard(`{${kbd.ARROW_RIGHT}}`);
-		await user.keyboard(`{${kbd.ARROW_RIGHT}}`);
-		await user.keyboard(`{${kbd.ARROW_RIGHT}}`);
-		await user.keyboard(`{${kbd.ARROW_RIGHT}}`);
+		await user.keyboard(kbd.ARROW_RIGHT);
+		await user.keyboard(kbd.ARROW_RIGHT);
+		await user.keyboard(kbd.ARROW_RIGHT);
+		await user.keyboard(kbd.ARROW_RIGHT);
 
 		expectPercentage({ percentage: 90, thumb, range: getByTestId('range') });
 	});
diff --git a/src/tests/switch/Switch.spec.ts b/src/tests/switch/Switch.spec.ts
index 72fa80d3..f2a31754 100644
--- a/src/tests/switch/Switch.spec.ts
+++ b/src/tests/switch/Switch.spec.ts
@@ -3,7 +3,7 @@ import { axe } from 'jest-axe';
 import { describe, it } from 'vitest';
 import SwitchTest from './SwitchTest.svelte';
 import { userEvent } from '@testing-library/user-event';
-import { kbd } from '$lib/internal/helpers/index.js';
+import { testKbd as kbd } from '../utils.js';
 import { writable } from 'svelte/store';
 import { tick } from 'svelte';
 
@@ -35,11 +35,11 @@ describe('Switch', () => {
 		expect(switchEl).toHaveFocus();
 
 		for (const key of keys) {
-			await userEvent.keyboard(`{${key}}`);
+			await userEvent.keyboard(key);
 			expect(switchEl).toHaveAttribute('data-state', 'checked');
 			expect(switchEl).toHaveAttribute('aria-checked', 'true');
 
-			await userEvent.keyboard(`{${key}}`);
+			await userEvent.keyboard(key);
 			expect(switchEl).toHaveAttribute('data-state', 'unchecked');
 			expect(switchEl).toHaveAttribute('aria-checked', 'false');
 		}
diff --git a/src/tests/tabs/Tabs.spec.ts b/src/tests/tabs/Tabs.spec.ts
index 488ec116..8da38cad 100644
--- a/src/tests/tabs/Tabs.spec.ts
+++ b/src/tests/tabs/Tabs.spec.ts
@@ -1,4 +1,4 @@
-import { kbd } from '$lib/internal/helpers/keyboard.js';
+import { testKbd as kbd } from '../utils.js';
 import { render } from '@testing-library/svelte';
 import { userEvent } from '@testing-library/user-event';
 import { axe } from 'jest-axe';
@@ -34,7 +34,7 @@ describe('Tabs', () => {
 
 		for (const tab of tabs) {
 			expect(getByTestId(`${tab}-trigger`)).toHaveFocus();
-			await userEvent.keyboard(`{${kbd.ARROW_RIGHT}}`);
+			await userEvent.keyboard(kbd.ARROW_RIGHT);
 		}
 
 		const lastTrigger = getByTestId(`${tabs[tabs.length - 1]}-trigger`);
@@ -42,7 +42,7 @@ describe('Tabs', () => {
 
 		for (const tab of [...tabs].reverse()) {
 			expect(getByTestId(`${tab}-trigger`)).toHaveFocus();
-			await userEvent.keyboard(`{${kbd.ARROW_LEFT}}`);
+			await userEvent.keyboard(kbd.ARROW_LEFT);
 		}
 	});
 });
diff --git a/src/tests/tags-input/TagsInput.spec.ts b/src/tests/tags-input/TagsInput.spec.ts
index dd58ce49..2ffd97f3 100644
--- a/src/tests/tags-input/TagsInput.spec.ts
+++ b/src/tests/tags-input/TagsInput.spec.ts
@@ -1,4 +1,4 @@
-import { kbd } from '$lib/internal/helpers/index.js';
+import { testKbd as kbd } from '../utils.js';
 import { queryByText, render } from '@testing-library/svelte';
 import { userEvent } from '@testing-library/user-event';
 import { axe } from 'jest-axe';
@@ -33,7 +33,7 @@ describe('TagsInput', () => {
 		const input = await findByRole('textbox');
 		const user = userEvent.setup();
 		const newTag = 'new tag';
-		await user.type(input, `${newTag}{${kbd.ENTER}}`);
+		await user.type(input, `${newTag}${kbd.ENTER}`);
 		expect(input).not.toHaveValue();
 		expect(queryByText(newTag)).toBeInTheDocument();
 	});
@@ -49,7 +49,7 @@ describe('TagsInput', () => {
 
 		const input = await findByRole('textbox');
 		const user = userEvent.setup();
-		await user.type(input, `${newTag}{${kbd.ENTER}}`);
+		await user.type(input, `${newTag}${kbd.ENTER}`);
 		expect(input).toHaveValue();
 	});
 
@@ -68,7 +68,7 @@ describe('TagsInput', () => {
 
 		const input = await findByRole('textbox');
 		const user = userEvent.setup();
-		await user.type(input, `${newTag}   {${kbd.ENTER}}`);
+		await user.type(input, `${newTag}   ${kbd.ENTER}`);
 		expect(input).toHaveValue();
 	});
 
@@ -82,7 +82,7 @@ describe('TagsInput', () => {
 
 		const input = await findByRole('textbox');
 		const user = userEvent.setup();
-		await user.type(input, `${newTag}   {${kbd.ENTER}}`);
+		await user.type(input, `${newTag}   ${kbd.ENTER}`);
 		expect(input).not.toHaveValue();
 		expect(queryByText(newTag)).toBeInTheDocument();
 	});
@@ -99,7 +99,7 @@ describe('TagsInput', () => {
 
 		const input = await findByRole('textbox');
 		const user = userEvent.setup();
-		await user.type(input, `${newTag}   {${kbd.ENTER}}`);
+		await user.type(input, `${newTag}   ${kbd.ENTER}`);
 		expect(input).toHaveValue();
 	});
 
@@ -115,10 +115,10 @@ describe('TagsInput', () => {
 
 		const input = await findByRole('textbox');
 		const user = userEvent.setup();
-		await user.type(input, `${newTag}{${kbd.ENTER}}`);
+		await user.type(input, `${newTag}${kbd.ENTER}`);
 		expect(input).toHaveValue();
 		user.clear(input);
-		await user.type(input, `${allowedTag}{${kbd.ENTER}}`);
+		await user.type(input, `${allowedTag}${kbd.ENTER}`);
 		expect(input).not.toHaveValue();
 		expect(queryByText(allowedTag)).toBeInTheDocument();
 	});
@@ -135,10 +135,10 @@ describe('TagsInput', () => {
 
 		const input = await findByRole('textbox');
 		const user = userEvent.setup();
-		await user.type(input, `${newTag}   {${kbd.ENTER}}`);
+		await user.type(input, `${newTag}   ${kbd.ENTER}`);
 		expect(input).toHaveValue();
 		user.clear(input);
-		await user.type(input, `${allowedTag}    {${kbd.ENTER}}`);
+		await user.type(input, `${allowedTag}    ${kbd.ENTER}`);
 		expect(input).not.toHaveValue();
 		expect(queryByText(allowedTag)).toBeInTheDocument();
 	});
@@ -157,10 +157,10 @@ describe('TagsInput', () => {
 
 		const input = await findByRole('textbox');
 		const user = userEvent.setup();
-		await user.type(input, `${newTag}   {${kbd.ENTER}}`);
+		await user.type(input, `${newTag}   ${kbd.ENTER}`);
 		expect(input).toHaveValue();
 		user.clear(input);
-		await user.type(input, `${allowedTag}    {${kbd.ENTER}}`);
+		await user.type(input, `${allowedTag}    ${kbd.ENTER}`);
 		expect(input).not.toHaveValue();
 		expect(queryByText(allowedTag)).toBeInTheDocument();
 	});
diff --git a/src/tests/toolbar/Toolbar.spec.ts b/src/tests/toolbar/Toolbar.spec.ts
index 1f93f658..cd013dc1 100644
--- a/src/tests/toolbar/Toolbar.spec.ts
+++ b/src/tests/toolbar/Toolbar.spec.ts
@@ -1,4 +1,4 @@
-import { kbd } from '$lib/internal/helpers/keyboard.js';
+import { testKbd as kbd } from '../utils.js';
 import { render } from '@testing-library/svelte';
 import { userEvent } from '@testing-library/user-event';
 import { axe } from 'jest-axe';
@@ -113,8 +113,8 @@ describe('Toolbar', () => {
 		const user = userEvent.setup();
 		const { getByTestId } = render(ToolbarTest, props);
 
-		const nextKey = `{${props.orientation === horizontal ? kbd.ARROW_RIGHT : kbd.ARROW_DOWN}}`;
-		const prevKey = `{${props.orientation === horizontal ? kbd.ARROW_LEFT : kbd.ARROW_UP}}`;
+		const nextKey = props.orientation === horizontal ? kbd.ARROW_RIGHT : kbd.ARROW_DOWN;
+		const prevKey = props.orientation === horizontal ? kbd.ARROW_LEFT : kbd.ARROW_UP;
 
 		const reversedItems = [...items].reverse();
 
@@ -180,8 +180,8 @@ describe('Toolbar', () => {
 			const user = userEvent.setup();
 			const { getByTestId } = render(ToolbarTest, { loop: true, ...props });
 
-			const nextKey = `{${props.orientation === horizontal ? kbd.ARROW_RIGHT : kbd.ARROW_DOWN}}`;
-			const prevKey = `{${props.orientation === horizontal ? kbd.ARROW_LEFT : kbd.ARROW_UP}}`;
+			const nextKey = props.orientation === horizontal ? kbd.ARROW_RIGHT : kbd.ARROW_DOWN;
+			const prevKey = props.orientation === horizontal ? kbd.ARROW_LEFT : kbd.ARROW_UP;
 
 			await user.tab();
 			const firstItem = getByTestId(items[0]);
@@ -243,8 +243,8 @@ describe('Toolbar', () => {
 			const user = userEvent.setup();
 			const { getByTestId } = render(ToolbarTest, { loop: false, ...props });
 
-			const nextKey = `{${props.orientation === horizontal ? kbd.ARROW_RIGHT : kbd.ARROW_DOWN}}`;
-			const prevKey = `{${props.orientation === horizontal ? kbd.ARROW_LEFT : kbd.ARROW_UP}}`;
+			const nextKey = props.orientation === horizontal ? kbd.ARROW_RIGHT : kbd.ARROW_DOWN;
+			const prevKey = props.orientation === horizontal ? kbd.ARROW_LEFT : kbd.ARROW_UP;
 
 			const firstItem = getByTestId(items[0]);
 			const lastItem = getByTestId(items[items.length - 1]);
@@ -312,9 +312,9 @@ describe('Toolbar', () => {
 		const firstItem = getByTestId(items[0]);
 		const lastItem = getByTestId(items[items.length - 1]);
 		expect(firstItem).toHaveFocus();
-		await user.keyboard(`{${kbd.END}}`);
+		await user.keyboard(kbd.END);
 		expect(lastItem).toHaveFocus();
-		await user.keyboard(`{${kbd.HOME}}`);
+		await user.keyboard(kbd.HOME);
 		expect(firstItem).toHaveFocus();
 	});
 
@@ -326,7 +326,7 @@ describe('Toolbar', () => {
 			const curr = getByTestId(item);
 			curr.focus();
 			expect(curr).toHaveFocus();
-			await user.keyboard(`{${kbd.SPACE}}`);
+			await user.keyboard(kbd.SPACE);
 			expect(curr).toHaveAttribute('aria-pressed', 'true');
 			expect(curr).toHaveAttribute('data-state', 'on');
 		}
@@ -335,7 +335,7 @@ describe('Toolbar', () => {
 			const curr = getByTestId(item);
 			curr.focus();
 			expect(curr).toHaveFocus();
-			await user.keyboard(`{${kbd.SPACE}}`);
+			await user.keyboard(kbd.SPACE);
 			expect(curr).toHaveAttribute('aria-checked', 'true');
 			expect(curr).toHaveAttribute('data-state', 'on');
 		}