diff --git a/packages/bits-ui/src/lib/bits/index.ts b/packages/bits-ui/src/lib/bits/index.ts
index 3af6da940..9122e0fdd 100644
--- a/packages/bits-ui/src/lib/bits/index.ts
+++ b/packages/bits-ui/src/lib/bits/index.ts
@@ -33,5 +33,6 @@ export * as Toggle from "./toggle/index.js";
export * as ToggleGroup from "./toggle-group/index.js";
export * as Toolbar from "./toolbar/index.js";
export * as Tooltip from "./tooltip/index.js";
+export * as EscapeLayer from "./utilities/escape-layer/index.js";
export * as PreventTextSelectionOverflowLayer from "./utilities/prevent-text-selection-overflow-layer/index.js";
export * as DismissableLayer from "./utilities/dismissable-layer/index.js";
diff --git a/packages/bits-ui/src/lib/bits/utilities/escape-layer/escape-layer.svelte b/packages/bits-ui/src/lib/bits/utilities/escape-layer/escape-layer.svelte
new file mode 100644
index 000000000..baba6f377
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/utilities/escape-layer/escape-layer.svelte
@@ -0,0 +1,15 @@
+
+
+{@render children?.()}
diff --git a/packages/bits-ui/src/lib/bits/utilities/escape-layer/escape-layer.svelte.ts b/packages/bits-ui/src/lib/bits/utilities/escape-layer/escape-layer.svelte.ts
new file mode 100644
index 000000000..fdd30553c
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/utilities/escape-layer/escape-layer.svelte.ts
@@ -0,0 +1,57 @@
+import { onDestroy } from "svelte";
+import type { EscapeBehaviorType, EscapeLayerProps } from "./types.js";
+import type { ReadonlyBox, ReadonlyBoxedValues } from "$lib/internal/box.svelte.js";
+import { type EventCallback, addEventListener } from "$lib/internal/events.js";
+import { kbd } from "$lib/internal/kbd.js";
+
+const layers = new Map>();
+
+type EscapeLayerStateProps = ReadonlyBoxedValues>>;
+
+export class EscapeLayerState {
+ #onEscapeProp: ReadonlyBox>;
+ #behaviorType: ReadonlyBox;
+
+ constructor(props: EscapeLayerStateProps) {
+ this.#behaviorType = props.behaviorType;
+ this.#onEscapeProp = props.onEscape;
+ layers.set(this, this.#behaviorType);
+ const unsubEvents = this.#addEventListener();
+ onDestroy(() => {
+ unsubEvents();
+ layers.delete(this);
+ });
+ }
+
+ #addEventListener() {
+ return addEventListener(document, "keydown", this.#onkeydown, { passive: false });
+ }
+
+ #onkeydown = (e: KeyboardEvent) => {
+ if (e.key !== kbd.ESCAPE || !isResponsibleEscapeLayer(this)) return;
+ e.preventDefault();
+ const behaviorType = this.#behaviorType.value;
+ if (behaviorType !== "close" && behaviorType !== "defer-otherwise-close") return;
+ this.#onEscapeProp.value(e);
+ };
+}
+
+export function escapeLayerState(props: EscapeLayerStateProps) {
+ return new EscapeLayerState(props);
+}
+
+function isResponsibleEscapeLayer(instance: EscapeLayerState) {
+ const layersArr = [...layers];
+ /**
+ * We first check if we can find a top layer with `close` or `ignore`.
+ * If that top layer was found and matches the provided node, then the node is
+ * responsible for the escape. Otherwise, we know that all layers defer so
+ * the first layer is the responsible one.
+ */
+ const topMostLayer = layersArr.findLast(
+ ([_, { value: behaviorType }]) => behaviorType === "close" || behaviorType === "ignore"
+ );
+ if (topMostLayer) return topMostLayer[0] === instance;
+ const [firstLayerNode] = layersArr[0]!;
+ return firstLayerNode === instance;
+}
diff --git a/packages/bits-ui/src/lib/bits/utilities/escape-layer/index.ts b/packages/bits-ui/src/lib/bits/utilities/escape-layer/index.ts
new file mode 100644
index 000000000..18e7fd7dd
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/utilities/escape-layer/index.ts
@@ -0,0 +1,3 @@
+export { default as Root } from "./escape-layer.svelte";
+
+export type { EscapeLayerProps as Props } from "./types.js";
diff --git a/packages/bits-ui/src/lib/bits/utilities/escape-layer/types.ts b/packages/bits-ui/src/lib/bits/utilities/escape-layer/types.ts
new file mode 100644
index 000000000..59f21dc40
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/utilities/escape-layer/types.ts
@@ -0,0 +1,27 @@
+import type { Snippet } from "svelte";
+
+export type EscapeBehaviorType =
+ | "close"
+ | "defer-otherwise-close"
+ | "defer-otherwise-ignore"
+ | "ignore";
+
+export type EscapeLayerProps = {
+ children?: Snippet;
+
+ /**
+ * Callback fired when escape is pressed.
+ */
+ onEscape?: (e: KeyboardEvent) => void;
+
+ /**
+ * Escape behavior type.
+ * `close`: Closes the element immediately.
+ * `defer-otherwise-close`: Delegates the action to the parent element. If no parent is found, it closes the element.
+ * `defer-otherwise-ignore`: Delegates the action to the parent element. If no parent is found, nothing is done.
+ * `ignore`: Prevents the element from closing and also blocks the parent element from closing in response to an escape key press.
+ *
+ * @defaultValue `close`
+ */
+ behaviorType?: EscapeBehaviorType;
+};