The Overlay component is the foundation for building overlay interactions like Modal, Drawer, Popover, and more. It provides all the essential features needed for accessible and user-friendly overlay experiences.
import { Overlay } from 'frontile';
The most basic usage of the Overlay component.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { Overlay } from 'frontile';
import { Button } from 'frontile';
export default class BasicOverlay extends Component {
@tracked isOpen = false;
@action toggle() {
this.isOpen = !this.isOpen;
}
<template>
<div class='flex flex-col gap-4'>
<Button @onPress={{this.toggle}}>
Open Overlay
</Button>
<Overlay @isOpen={{this.isOpen}} @onClose={{this.toggle}}>
<div class='bg-content1 p-8 rounded-lg shadow-lg max-w-md'>
<h2 class='text-lg font-semibold mb-4'>Overlay Content</h2>
<p class='mb-4'>This is the content inside the overlay. You can put
any content here.</p>
<div class='flex gap-2'>
<Button @intent='primary' @onPress={{this.toggle}}>
Close
</Button>
<Button>
Another Action
</Button>
</div>
</div>
</Overlay>
</div>
</template>
}
Control the appearance of the backdrop behind the overlay.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { Overlay } from 'frontile';
import { Button } from 'frontile';
export default class BackdropTypes extends Component {
@tracked fadedOpen = false;
@tracked blurredOpen = false;
@tracked noneOpen = false;
@action toggleFaded() {
this.fadedOpen = !this.fadedOpen;
}
@action toggleBlurred() {
this.blurredOpen = !this.blurredOpen;
}
@action toggleNone() {
this.noneOpen = !this.noneOpen;
}
<template>
<div class='flex flex-col gap-4'>
<div class='flex gap-2'>
<Button @onPress={{this.toggleFaded}}>
Faded Backdrop
</Button>
<Button @onPress={{this.toggleBlurred}}>
Blurred Backdrop
</Button>
<Button @onPress={{this.toggleNone}}>
No Backdrop
</Button>
</div>
<Overlay
@isOpen={{this.fadedOpen}}
@onClose={{this.toggleFaded}}
@backdrop='faded'
>
<div class='bg-content1 p-6 rounded-lg shadow-lg'>
<h3 class='font-semibold mb-2'>Faded Backdrop</h3>
<p class='mb-4'>Standard semi-transparent backdrop</p>
<Button @onPress={{this.toggleFaded}}>Close</Button>
</div>
</Overlay>
<Overlay
@isOpen={{this.blurredOpen}}
@onClose={{this.toggleBlurred}}
@backdrop='blur'
>
<div class='bg-content1 p-6 rounded-lg shadow-lg'>
<h3 class='font-semibold mb-2'>Blurred Backdrop</h3>
<p class='mb-4'>Backdrop with blur effect</p>
<Button @onPress={{this.toggleBlurred}}>Close</Button>
</div>
</Overlay>
<Overlay
@isOpen={{this.noneOpen}}
@onClose={{this.toggleNone}}
@backdrop='none'
>
<div class='bg-content1 p-6 rounded-lg shadow-lg border'>
<h3 class='font-semibold mb-2'>No Backdrop</h3>
<p class='mb-4'>Overlay without backdrop</p>
<Button @onPress={{this.toggleNone}}>Close</Button>
</div>
</Overlay>
</div>
</template>
}
By default, overlays are rendered in a portal. You can render them in-place instead.
This container shows the difference between portal and in-place rendering.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { Overlay } from 'frontile';
import { Button } from 'frontile';
export default class RenderInPlace extends Component {
@tracked portalOpen = false;
@tracked inPlaceOpen = false;
@action togglePortal() {
this.portalOpen = !this.portalOpen;
}
@action toggleInPlace() {
this.inPlaceOpen = !this.inPlaceOpen;
}
<template>
<div class='flex flex-col gap-4'>
<div class='flex gap-2'>
<Button @onPress={{this.togglePortal}}>
Portal Overlay (Default)
</Button>
<Button @onPress={{this.toggleInPlace}}>
In-Place Overlay
</Button>
</div>
<div
class='relative border-2 border-dashed border-neutral-soft p-4 min-h-48'
>
<p class='text-sm text-neutral mb-4'>This container shows the difference
between portal and in-place rendering.</p>
<Overlay
@isOpen={{this.inPlaceOpen}}
@onClose={{this.toggleInPlace}}
@renderInPlace={{true}}
>
<div
class='absolute inset-4 bg-content1 p-4 rounded shadow-lg border'
>
<h3 class='font-semibold mb-2'>In-Place Overlay</h3>
<p class='text-sm mb-4'>This overlay is rendered within its parent
container.</p>
<Button @size='sm' @onPress={{this.toggleInPlace}}>Close</Button>
</div>
</Overlay>
</div>
<Overlay @isOpen={{this.portalOpen}} @onClose={{this.togglePortal}}>
<div class='bg-content1 p-6 rounded-lg shadow-lg'>
<h3 class='font-semibold mb-2'>Portal Overlay</h3>
<p class='mb-4'>This overlay is rendered in a portal (outside the
normal DOM tree).</p>
<Button @onPress={{this.togglePortal}}>Close</Button>
</div>
</Overlay>
</div>
</template>
}
Control when and how the overlay can be closed.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { Overlay } from 'frontile';
import { Button } from 'frontile';
export default class CustomCloseBehavior extends Component {
@tracked normalOpen = false;
@tracked noEscapeOpen = false;
@tracked noOutsideClickOpen = false;
@tracked confirmCloseOpen = false;
@action toggleNormal() {
this.normalOpen = !this.normalOpen;
}
@action toggleNoEscape() {
this.noEscapeOpen = !this.noEscapeOpen;
}
@action toggleNoOutsideClick() {
this.noOutsideClickOpen = !this.noOutsideClickOpen;
}
@action toggleConfirmClose() {
this.confirmCloseOpen = !this.confirmCloseOpen;
}
@action handleConfirmClose() {
if (confirm('Are you sure you want to close this overlay?')) {
this.confirmCloseOpen = false;
}
}
<template>
<div class='flex flex-col gap-4'>
<div class='grid grid-cols-2 gap-2'>
<Button @onPress={{this.toggleNormal}}>
Normal Overlay
</Button>
<Button @onPress={{this.toggleNoEscape}}>
No Escape Key
</Button>
<Button @onPress={{this.toggleNoOutsideClick}}>
No Outside Click
</Button>
<Button @onPress={{this.toggleConfirmClose}}>
Confirm Close
</Button>
</div>
<Overlay @isOpen={{this.normalOpen}} @onClose={{this.toggleNormal}}>
<div class='bg-content1 p-6 rounded-lg shadow-lg'>
<h3 class='font-semibold mb-2'>Normal Overlay</h3>
<p class='mb-4'>Can be closed with Escape key, outside click, or
button.</p>
<Button @onPress={{this.toggleNormal}}>Close</Button>
</div>
</Overlay>
<Overlay
@isOpen={{this.noEscapeOpen}}
@onClose={{this.toggleNoEscape}}
@closeOnEscapeKey={{false}}
>
<div class='bg-content1 p-6 rounded-lg shadow-lg'>
<h3 class='font-semibold mb-2'>No Escape Key</h3>
<p class='mb-4'>Cannot be closed with Escape key. Try pressing Escape!</p>
<Button @onPress={{this.toggleNoEscape}}>Close</Button>
</div>
</Overlay>
<Overlay
@isOpen={{this.noOutsideClickOpen}}
@onClose={{this.toggleNoOutsideClick}}
@closeOnOutsideClick={{false}}
>
<div class='bg-content1 p-6 rounded-lg shadow-lg'>
<h3 class='font-semibold mb-2'>No Outside Click</h3>
<p class='mb-4'>Cannot be closed by clicking outside. Try clicking the
backdrop!</p>
<Button @onPress={{this.toggleNoOutsideClick}}>Close</Button>
</div>
</Overlay>
<Overlay
@isOpen={{this.confirmCloseOpen}}
@onClose={{this.handleConfirmClose}}
>
<div class='bg-content1 p-6 rounded-lg shadow-lg'>
<h3 class='font-semibold mb-2'>Confirm Close</h3>
<p class='mb-4'>Shows confirmation dialog before closing.</p>
<Button @onPress={{this.handleConfirmClose}}>Close</Button>
</div>
</Overlay>
</div>
</template>
}
Demonstrate focus trapping and restoration features.
Open an overlay and use Tab/Shift+Tab to see focus behavior. Focus should return to the trigger button when closed.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { Overlay } from 'frontile';
import { Button } from 'frontile';
import { Input } from 'frontile';
export default class FocusManagement extends Component {
@tracked focusTrapOpen = false;
@tracked noFocusTrapOpen = false;
@action toggleFocusTrap() {
this.focusTrapOpen = !this.focusTrapOpen;
}
@action toggleNoFocusTrap() {
this.noFocusTrapOpen = !this.noFocusTrapOpen;
}
<template>
<div class='flex flex-col gap-4'>
<div class='flex gap-2'>
<Button @onPress={{this.toggleFocusTrap}} data-test-trigger>
Focus Trap Enabled
</Button>
<Button @onPress={{this.toggleNoFocusTrap}}>
Focus Trap Disabled
</Button>
</div>
<p class='text-sm text-neutral'>
Open an overlay and use Tab/Shift+Tab to see focus behavior. Focus
should return to the trigger button when closed.
</p>
<Overlay @isOpen={{this.focusTrapOpen}} @onClose={{this.toggleFocusTrap}}>
<div class='bg-content1 p-6 rounded-lg shadow-lg max-w-md'>
<h3 class='font-semibold mb-4'>Focus Trap Enabled</h3>
<p class='mb-4 text-sm'>Focus is trapped within this overlay. Try
pressing Tab to cycle through focusable elements.</p>
<div class='space-y-3'>
<Input @label='First Input' />
<Input @label='Second Input' />
<div class='flex gap-2'>
<Button @intent='primary' @onPress={{this.toggleFocusTrap}}>
Close
</Button>
<Button>
Another Button
</Button>
</div>
</div>
</div>
</Overlay>
<Overlay
@isOpen={{this.noFocusTrapOpen}}
@onClose={{this.toggleNoFocusTrap}}
@disableFocusTrap={{true}}
>
<div class='bg-content1 p-6 rounded-lg shadow-lg max-w-md'>
<h3 class='font-semibold mb-4'>Focus Trap Disabled</h3>
<p class='mb-4 text-sm'>Focus is not trapped. You can Tab to elements
outside the overlay.</p>
<div class='space-y-3'>
<Input @label='Input Field' />
<Button @onPress={{this.toggleNoFocusTrap}}>
Close
</Button>
</div>
</div>
</Overlay>
</div>
</template>
}
Customize overlay animations and transition duration.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { Overlay } from 'frontile';
import { Button } from 'frontile';
export default class AnimationsAndTransitions extends Component {
@tracked fastOpen = false;
@tracked slowOpen = false;
@tracked noAnimationOpen = false;
@action toggleFast() {
this.fastOpen = !this.fastOpen;
}
@action toggleSlow() {
this.slowOpen = !this.slowOpen;
}
@action toggleNoAnimation() {
this.noAnimationOpen = !this.noAnimationOpen;
}
<template>
<div class='flex flex-col gap-4'>
<div class='flex gap-2'>
<Button @onPress={{this.toggleFast}}>
Fast Animation (100ms)
</Button>
<Button @onPress={{this.toggleSlow}}>
Slow Animation (800ms)
</Button>
<Button @onPress={{this.toggleNoAnimation}}>
No Animation
</Button>
</div>
<Overlay
@isOpen={{this.fastOpen}}
@onClose={{this.toggleFast}}
@transitionDuration={{100}}
>
<div class='bg-content1 p-6 rounded-lg shadow-lg'>
<h3 class='font-semibold mb-2'>Fast Animation</h3>
<p class='mb-4'>This overlay opens and closes quickly (100ms).</p>
<Button @onPress={{this.toggleFast}}>Close</Button>
</div>
</Overlay>
<Overlay
@isOpen={{this.slowOpen}}
@onClose={{this.toggleSlow}}
@transitionDuration={{800}}
>
<div class='bg-content1 p-6 rounded-lg shadow-lg'>
<h3 class='font-semibold mb-2'>Slow Animation</h3>
<p class='mb-4'>This overlay has a longer transition duration (800ms).</p>
<Button @onPress={{this.toggleSlow}}>Close</Button>
</div>
</Overlay>
<Overlay
@isOpen={{this.noAnimationOpen}}
@onClose={{this.toggleNoAnimation}}
@disableTransitions={{true}}
>
<div class='bg-content1 p-6 rounded-lg shadow-lg'>
<h3 class='font-semibold mb-2'>No Animation</h3>
<p class='mb-4'>This overlay appears instantly without transitions.</p>
<Button @onPress={{this.toggleNoAnimation}}>Close</Button>
</div>
</Overlay>
</div>
</template>
}
The Overlay component has two different click-to-close mechanisms that work together:
@closeOnOutsideClick (default: true): Closes when clicking the backdrop/outside area@closeOnOverlayElementClick (default: true): Closes when clicking the overlay element itselfDefault Behavior: Try clicking on the overlay element (the outer container) vs the inner content card.
Disabled: Only the backdrop (outside area) will close the overlay.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { Overlay } from 'frontile';
import { Button } from 'frontile';
export default class OverlayElementClick extends Component {
@tracked defaultOpen = false;
@tracked disabledOpen = false;
@action toggleDefault() {
this.defaultOpen = !this.defaultOpen;
}
@action toggleDisabled() {
this.disabledOpen = !this.disabledOpen;
}
<template>
<div class='flex flex-col gap-4'>
<div class='flex gap-2'>
<Button @onPress={{this.toggleDefault}}>
Default Behavior
</Button>
<Button @onPress={{this.toggleDisabled}}>
Overlay Element Click Disabled
</Button>
</div>
<div class='text-sm space-y-2'>
<p><strong>Default Behavior:</strong>
Try clicking on the overlay element (the outer container) vs the inner
content card.</p>
<p><strong>Disabled:</strong>
Only the backdrop (outside area) will close the overlay.</p>
</div>
<Overlay @isOpen={{this.defaultOpen}} @onClose={{this.toggleDefault}}>
<div class='bg-content1 p-6 rounded-lg shadow-lg max-w-md'>
<h3 class='font-semibold mb-2'>Default Behavior</h3>
<p class='mb-4 text-sm'>
<code>@closeOnOverlayElementClick={{true}}</code>
(default)
</p>
<p class='mb-4'>Clicking anywhere on the overlay element will close
it, but clicking this inner content card won't.</p>
<Button @onPress={{this.toggleDefault}}>Close</Button>
</div>
</Overlay>
<Overlay
@isOpen={{this.disabledOpen}}
@onClose={{this.toggleDisabled}}
@closeOnOverlayElementClick={{false}}
>
<div class='bg-content1 p-6 rounded-lg shadow-lg max-w-md'>
<h3 class='font-semibold mb-2'>Overlay Element Click Disabled</h3>
<p class='mb-4 text-sm'>
<code>@closeOnOverlayElementClick={{false}}</code>
</p>
<p class='mb-4'>Only clicking the backdrop (outside area) will not
close this overlay because the overlay content element is on top of
backdrop.</p>
<Button @onPress={{this.toggleDisabled}}>Close</Button>
</div>
</Overlay>
</div>
</template>
}
Why is
@closeOnOverlayElementClicktrueby default?This option is set to
trueby default to make "outside click" functionality work intuitively. Most overlay content is wrapped with an inner element (like a card or dialog), which prevents accidental closure when clicking on the actual content. The overlay element itself acts as part of the "outside" area, allowing users to click anywhere around the content to dismiss the overlay.
FormSelect uses Power Select, which defaults to @renderInPlace={{false}} and so inserts
its dropdown outside the overlay. The focus trap then treats the dropdown as outside the
overlay and blocks access to it, including its search input.
Pass @renderInPlace={{true}} on the FormSelect so the dropdown stays inside the trap:
<FormSelect @renderInPlace={{true}} />
Disabling the overlay's focus trap with @disableFocusTrap={{true}} also works, but it
removes the trap for everything else in the overlay too — prefer the first option.
Overlay is the primitive under Modal, Drawer and Popover, and supplies their shared focus
and keyboard behavior. It sets tabindex="0" on the content element but no role — a
semantic role is the consuming component's job, which is why Modal and Drawer add
role="dialog" themselves. If you build directly on Overlay, give it a role and an
accessible name.
| Behavior | Detail |
|---|---|
| Focus on open | Moves into the overlay; ember-focus-trap keeps it there |
| Focus trap | Disable with @disableFocusTrap={{true}}, or tune via @focusTrapOptions |
| Focus on close | Returns to the previously focused element, unless @preventFocusRestore |
Escape |
Closes, unless @closeOnEscapeKey={{false}} |
| Backdrop click | Closes, unless @closeOnOutsideClick={{false}} |
| Body scroll | Blocked while open, unless @blockScroll={{false}} |
The overlay needs at least one focusable element inside it. A focus trap with nothing to focus leaves the keyboard stranded, and nothing warns you at runtime.
Frontile does not set aria-modal, aria-labelledby or aria-describedby at this level.
Element: <span class="hljs-title class_">HTMLDivElement</span>
| Name | Type | Default | Description |
|---|---|---|---|
isOpen
*
|
boolean
|
- | Whether it is open or not |
backdrop
|
enum
|
- |
How the area behind the overlay is rendered: none omits the backdrop
entirely, transparent keeps it clickable but invisible, faded dims the
page, and blur blurs it.
|
backdropTransition
|
Object
|
- | Transition classes for the backdrop, overriding the defaults used when it fades in and out. |
blockScroll
|
boolean
|
true
|
|
class
|
string
|
- | Custom class name for the content element, merged with the default ones using Tailwind Merge. |
closeOnEscapeKey
|
boolean
|
true
|
Whether to close when the escape key is pressed |
closeOnOutsideClick
|
boolean
|
true
|
Whether to close when the area outside (the backdrop) is clicked |
closeOnOverlayElementClick
|
boolean
|
true
|
Whether to close when the overlay element is clicked, used for modal and drawer components. This is set to true by default to allow "outside click" functionality to work properly. Most overlay content is wrapped with an inner element, preventing accidental closure. |
customContentModifier
|
ModifierLike<{ Element: HTMLElement; }>
|
- | An extra modifier applied to the content element, for behavior the overlay doesn't provide itself. |
didClose
|
function
|
- | A function that will be called when closing is finished executing, this includes waiting for animations/transitions to finish. |
disableFlexContent
|
boolean
|
false
|
Opt out of the flex layout applied to the content element, for overlays that need to lay their content out themselves. |
disableFocusTrap
|
boolean
|
false
|
Whether the focus trap is disabled or not |
disableTransitions
|
boolean
|
false
|
Disable css transitions |
focusTrapOptions
|
any
|
{ clickOutsideDeactivates: true, allowOutsideClick: true }
|
Focus trap options |
onClose
|
function
|
- | A function that will be called when closed |
onOpen
|
function
|
- | A function that will be called when opened |
preventAutoFocus
|
boolean
|
false
|
When focusTrap is disabled, by default Oberlay will be auto focused. This option prevents that. |
preventFocusRestore
|
boolean
|
false
|
|
renderInPlace
|
boolean
|
false
|
Whether to render in place or in the specified/default destination |
target
|
enum
|
- |
The target where to render the portal.
There are 3 options: 1) For element id, string must be prefixed with |
transition
|
Object
|
{name:'overlay-transition--fade'}
|
Transition options |
transitionDuration
|
number
|
200
|
Duration of the animation |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |