diff --git a/pages/src/localdev.ts b/pages/src/localdev.ts
index 587420c019..bd760b283a 100644
--- a/pages/src/localdev.ts
+++ b/pages/src/localdev.ts
@@ -34,6 +34,7 @@ import {
ESLAnimate,
ESLAnimateMixin,
ESLShare,
+ ESLModal,
ESLRelatedTarget
} from '../../src/modules/all';
@@ -104,6 +105,8 @@ ESLTooltip.register();
ESLAnimate.register();
ESLAnimateMixin.register();
+ESLModal.register();
+
ESLShare.config(() => fetch('/assets/share/config.json').then((response) => response.json()));
ESLShare.register();
diff --git a/src/modules/all.less b/src/modules/all.less
index 05fab970e1..09d2e82d69 100644
--- a/src/modules/all.less
+++ b/src/modules/all.less
@@ -25,3 +25,5 @@
@import "./esl-animate/core.less";
@import "./esl-share/core.less";
+
+@import "./esl-modal/core.less";
diff --git a/src/modules/all.ts b/src/modules/all.ts
index a6dde9383a..309a029019 100644
--- a/src/modules/all.ts
+++ b/src/modules/all.ts
@@ -40,6 +40,9 @@ export * from './esl-tooltip/core';
// Animate
export * from './esl-animate/core';
+// Modal
+export * from './esl-modal/core';
+
// Related Target Mixin
export * from './esl-related-target/core';
diff --git a/src/modules/esl-modal/README.md b/src/modules/esl-modal/README.md
new file mode 100644
index 0000000000..d57c95d106
--- /dev/null
+++ b/src/modules/esl-modal/README.md
@@ -0,0 +1,30 @@
+# [ESL](../../../) Modal
+
+Version: *1.0.0-beta*.
+
+Authors: *Anastasiya Lesun*.
+
+
+
+**ESLModal** - a custom element based on `ESLToggleable` instance.
+
+`ESLModal` opens in overlay on top of the main content when `esl:show` DOM event is dispatched on the appropriate modal item.
+By default, modal window before its opening is moved to the `document.body` (determined by `body-inject` attribute) and blocks all other workflows on the main page until modal is closed (supports 'none' | 'native' | 'pseudo' locks using `scroll-lock-strategy` attribute).
+Modal opening can be taken up together with backdrop appearance (depends on `no-backdrop` attribute).
+
+### ESLModal Attributes | Properties:
+- `no-backdrop` (boolean) - disable modal backdrop
+- `body-inject` (boolean) - provide element movement to body before its opening
+- `scroll-lock-strategy` ('none' | 'native' | 'pseudo') - define scroll lock type
+
+### Example
+```html
+
+
+ ...
+
+
+ ...
+
+
+```
diff --git a/src/modules/esl-modal/core.less b/src/modules/esl-modal/core.less
new file mode 100644
index 0000000000..913090db65
--- /dev/null
+++ b/src/modules/esl-modal/core.less
@@ -0,0 +1 @@
+@import "./core/esl-modal.less";
diff --git a/src/modules/esl-modal/core.ts b/src/modules/esl-modal/core.ts
new file mode 100644
index 0000000000..28207faf11
--- /dev/null
+++ b/src/modules/esl-modal/core.ts
@@ -0,0 +1,3 @@
+export type {ESLModalTagShape} from './core/esl-modal.shape';
+
+export * from './core/esl-modal';
diff --git a/src/modules/esl-modal/core/esl-modal.less b/src/modules/esl-modal/core/esl-modal.less
new file mode 100644
index 0000000000..57aeb53dd8
--- /dev/null
+++ b/src/modules/esl-modal/core/esl-modal.less
@@ -0,0 +1,64 @@
+esl-modal {
+ display: none;
+}
+
+.esl-modal {
+ position: fixed;
+ z-index: 1000000;
+ left: 0;
+ top: 0;
+ width: 100vw;
+ height: var(--100vh, 100vh);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ overflow-y: scroll;
+
+ &:not(.open) {
+ display: none;
+ }
+
+ &::before, &::after {
+ content: '';
+ flex: 1 0 30px;
+ }
+
+ .esl-modal-container {
+ flex: 0 0 auto;
+ position: relative;
+ padding: 50px;
+ max-width: 100%;
+ width: 80%;
+ background-color: #fff;
+ overflow: auto;
+ }
+
+ .close-btn {
+ position: absolute;
+ right: 15px;
+ top: 15px;
+ width: 20px;
+ height: 20px;
+ font-size: 19px;
+ line-height: 19px;
+
+ &::before {
+ content: '\2715';
+ }
+ }
+}
+
+.esl-modal-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: -1;
+ transition: all .3s ease-in;
+
+ &.active {
+ background: rgba(0, 0, 0, .4);
+ z-index: 999999;
+ }
+}
diff --git a/src/modules/esl-modal/core/esl-modal.shape.ts b/src/modules/esl-modal/core/esl-modal.shape.ts
new file mode 100644
index 0000000000..b385889dae
--- /dev/null
+++ b/src/modules/esl-modal/core/esl-modal.shape.ts
@@ -0,0 +1,24 @@
+import type {ESLToggleableTagShape} from '../../esl-toggleable/core/esl-toggleable.shape';
+import type {ESLModal, ScrollLockStrategies} from './esl-modal';
+
+/**
+ * Tag declaration interface of {@link ESLModal} element
+ * Used for TSX declaration
+ */
+export interface ESLModalTagShape extends ESLToggleableTagShape {
+ /** Disable modal backdrop */
+ 'no-backdrop'?: boolean;
+ /** Provides modal movement to body before its opening */
+ 'body-inject'?: boolean;
+ /** Defines a scroll lock strategy when the modal is open (default 'pseudo') */
+ 'scroll-lock-strategy'?: ScrollLockStrategies;
+}
+
+declare global {
+ namespace JSX {
+ export interface IntrinsicElements {
+ /** {@link ESLModal} custom tag */
+ 'esl-modal': ESLModalTagShape;
+ }
+ }
+}
diff --git a/src/modules/esl-modal/core/esl-modal.ts b/src/modules/esl-modal/core/esl-modal.ts
new file mode 100644
index 0000000000..23c13d36a9
--- /dev/null
+++ b/src/modules/esl-modal/core/esl-modal.ts
@@ -0,0 +1,116 @@
+import {ExportNs} from '../../esl-utils/environment/export-ns';
+import {ESLToggleable} from '../../esl-toggleable/core/esl-toggleable';
+import {prop, boolAttr, attr, memoize, listen} from '../../esl-utils/decorators';
+import {hasAttr, setAttr} from '../../esl-utils/dom/attr';
+import {getKeyboardFocusableElements, handleFocusChain} from '../../esl-utils/dom/focus';
+import {lockScroll, unlockScroll} from '../../esl-utils/dom/scroll/utils';
+import {TAB} from '../../esl-utils/dom/keys';
+
+import type {ScrollLockOptions} from '../../esl-utils/dom/scroll/utils';
+import type {ESLToggleableActionParams} from '../../esl-toggleable/core/esl-toggleable';
+
+export type ScrollLockStrategies = ScrollLockOptions['strategy'];
+
+export interface ModalActionParams extends ESLToggleableActionParams { }
+
+@ExportNs('Modal')
+export class ESLModal extends ESLToggleable {
+ public static override is = 'esl-modal';
+
+ @attr({defaultValue: '[data-modal-close]'})
+ public override closeTrigger: string;
+
+ /** Define option to lock scroll {@see ScrollLockOptions} */
+ @attr({defaultValue: 'pseudo'})
+ public scrollLockStrategy: ScrollLockStrategies;
+
+ @boolAttr() public noBackdrop: boolean;
+ @boolAttr() public bodyInject: boolean;
+
+ @prop(true) public override closeOnEsc: boolean;
+ @prop(true) public override closeOnOutsideAction: boolean;
+
+ @memoize()
+ protected static get $backdrop(): any {
+ const $backdrop = document.createElement('esl-modal-backdrop');
+ $backdrop.classList.add('esl-modal-backdrop');
+ return $backdrop;
+ }
+
+ public override connectedCallback(): void {
+ super.connectedCallback();
+ if (!hasAttr(this, 'role')) setAttr(this, 'role', 'dialog');
+ if (!hasAttr(this, 'tabindex')) setAttr(this, 'tabIndex', '-1');
+ }
+
+ public override onShow(params: ModalActionParams): void {
+ this.bodyInject && this.inject();
+ this.activator = params.activator;
+ this.showBackdrop();
+ super.onShow(params);
+ this.focus();
+ lockScroll(document.documentElement, this.lockOptions);
+ }
+
+ public override onHide(params: ModalActionParams): void {
+ unlockScroll(document.documentElement, this.lockOptions);
+ super.onHide(params);
+ this.hideBackdrop();
+ this.activator?.focus();
+ this.bodyInject && this.extract();
+ }
+
+ protected inject(): void {
+ if (this.parentNode === document.body) return;
+ document.body.appendChild(this);
+ }
+
+ protected extract(): void {
+ if (this.parentNode !== document.body) return;
+ document.body.removeChild(this);
+ }
+
+
+ protected showBackdrop(): void {
+ if (this.noBackdrop) return;
+ if (!document.body.contains(ESLModal.$backdrop)) document.body.appendChild(ESLModal.$backdrop);
+ ESLModal.$backdrop.classList.add('active');
+ }
+
+ protected hideBackdrop(): void {
+ if (this.noBackdrop) return;
+ ESLModal.$backdrop.classList.remove('active');
+ }
+
+ public get lockOptions(): ScrollLockOptions {
+ return {
+ strategy: this.scrollLockStrategy,
+ initiator: this
+ };
+ }
+
+ public get $boundaryFocusable(): {first: HTMLElement, last: HTMLElement} {
+ const $focusableEls = getKeyboardFocusableElements(this) as HTMLElement[];
+ return {first: $focusableEls[0], last: $focusableEls[$focusableEls.length - 1]};
+ }
+
+ @listen({inherit: true})
+ protected override _onKeyboardEvent(e: KeyboardEvent): void {
+ super._onKeyboardEvent(e);
+ if (e.key === TAB) this._onTabKey(e);
+ }
+
+ protected _onTabKey(e: KeyboardEvent): boolean | undefined {
+ const {first, last} = this.$boundaryFocusable;
+ return handleFocusChain(e, first, last);
+ }
+}
+
+declare global {
+ export interface ESLLibrary {
+ Modal: typeof ESLModal;
+ }
+ export interface HTMLElementTagNameMap {
+ 'esl-modal': ESLModal;
+ }
+}