G Gentelella v4.0.0

Overlays

Modals, toasts, popover menus, and slide-out panels — four helpers that handle backdrop clicks, Escape, focus traps, and ARIA roles. No third-party UI library, no Bootstrap JS.

Last updated May 25, 2026

Modals, toasts, popover menus, and slide-out panels. Four helpers — no third-party UI library, no Bootstrap JS.

All overlays handle the boring parts for you: backdrop click, Escape key, focus trap, focus return, ARIA roles. You provide content; they provide behavior.

showModal — backdropped dialog

import { showModal } from './v4/modal.js';

showModal({
  title: 'Delete project?',
  body: 'This action cannot be undone.',
  actions: [
    { label: 'Cancel', variant: 'outline' },
    { label: 'Delete', variant: 'danger', action: () => deleteProject(id) }
  ]
});

Options

OptionTypeDefaultPurpose
titlestringHeader text
bodystring | HTMLElement''Modal body. String is set via innerHTML; HTMLElement is appended
actionsAction[][]Footer buttons (see below)
size'sm' | 'md' | 'lg''md'Dialog width
onClose() => voidCalled when the modal closes (any reason)

Action shape

type Action = {
  label: string;
  variant?: 'primary' | 'outline' | 'ghost' | 'danger';  // default 'outline'
  action?: (ctx: { dialog, body, close }) => false | void;  // return false to keep modal open
  closeOnAction?: boolean;  // default true
}

If action() returns false, the modal stays open (use this for validation failures). Otherwise the modal closes after the handler runs.

Body as HTML

showModal({
  title: 'Edit user',
  body: `
    <form id="edit-user-form" class="form-stack">
      <label class="field">
        <span class="label">Name</span>
        <input class="input" name="name" value="Jane Smith">
      </label>
      <label class="field">
        <span class="label">Email</span>
        <input class="input" name="email" type="email" value="[email protected]">
      </label>
    </form>
  `,
  actions: [
    { label: 'Cancel', variant: 'outline' },
    { label: 'Save', variant: 'primary', action: ({ body, close }) => {
        const form = body.querySelector('#edit-user-form');
        if (!form.reportValidity()) return false;   // keep open if invalid
        saveUser(new FormData(form));
        // modal auto-closes
    }}
  ]
});

Body as DOM element

const content = document.createElement('div');
content.innerHTML = '<p>Dynamic content</p>';

showModal({ title: 'Hello', body: content });

Programmatic close

import { closeModal, isModalOpen } from './v4/modal.js';

if (isModalOpen()) closeModal();

Accessibility

  • role="dialog" + aria-modal="true"
  • aria-label set from the title
  • Focus moves into the dialog on open (first focusable element)
  • Tab is trapped inside while open
  • Focus restored to the trigger on close
  • Backdrop click + Escape + close button all dismiss

showToast — transient notifications

import { showToast } from './v4/toast.js';

showToast('Saved');
showToast('Failed to save', { variant: 'error' });
showToast('Heads up', { variant: 'info', duration: 5000 });

Options

OptionTypeDefaultPurpose
variant'default' | 'success' | 'error' | 'info''default'Color and icon
durationnumber (ms)2600Auto-dismiss delay; click also dismisses

Stacks at the top-right of the viewport. Each toast self-removes on the transitionend after duration. Click dismisses early.

ARIA: the host container has role="status" and aria-live="polite" so screen readers announce new toasts without interrupting.

Returns

The toast element, in case you want to dismiss early:

const t = showToast('Uploading…', { duration: 60000 });
upload().finally(() => t.remove());

openMenu — popover menu

Right-click-style context menu anchored to a trigger button.

import { openMenu } from './v4/menus.js';

button.addEventListener('click', () => {
  openMenu(button, [
    { label: 'Edit',      action: () => editItem() },
    { label: 'Duplicate', action: () => duplicateItem() },
    '-',  // separator
    { label: 'Delete',    action: () => deleteItem() }
  ]);
});

Item shape

type MenuItem =
  | { label: string; action?: () => void; disabled?: boolean; icon?: string }
  | '-'  // separator

One menu open at a time. Outside-click, Escape, or scroll closes it.

DEFAULT_CARD_MENU

Common card-options menu (Refresh, Move up, Move down, Hide card) — drop in for any card without writing your own list:

import { openMenu, DEFAULT_CARD_MENU } from './v4/menus.js';

btn.addEventListener('click', () => openMenu(btn, DEFAULT_CARD_MENU));

The Refresh item briefly shimmers the card and re-emits themechange so any embedded chart redraws. Move up / Move down reorder the card within its parent. Hide card removes it with a fade and shows an undo toast.

Programmatic close

import { closeMenu } from './v4/menus.js';
closeMenu();

openPanel — slide-out side panel

For larger drill-down content that doesn’t deserve a modal. Slides in from the right.

import { openPanel } from './v4/menus.js';

button.addEventListener('click', () => {
  openPanel(button, `
    <div class="panel-header">
      <h2>Notifications</h2>
      <button class="panel-close">×</button>
    </div>
    <div class="panel-body">
      <!-- content -->
    </div>
  `, { width: '380px' });
});

Options

OptionDefaultPurpose
width'320px'Panel width
onCloseCalled when the panel closes

Outside-click and Escape dismiss. The Notifications and Messages panels in the topbar use this.

Choosing between them

Use caseHelper
Confirmation prompt with OK/CancelshowModal
Form in a popupshowModal
”Saved” / “Failed” feedbackshowToast
Card-options ⋯ menuopenMenu
Right-click contextopenMenu
Notifications draweropenPanel
User account menuopenMenu

What you don’t have to do

These helpers handle:

  • Backdrop element creation + removal
  • Click-outside-to-close detection
  • Escape key handling
  • Body scroll lock (modals)
  • Focus trap (modals)
  • Focus return to the trigger
  • ARIA roles + labels
  • Transition timing (no premature DOM removal)

You write content. They handle the boring 200 lines per overlay.

Custom overlays

If you need something the helpers don’t cover (e.g. a tooltip), use the same patterns:

  • Append to document.body
  • Add a single global Escape listener with a cleanup function
  • Use aria-live (toast) or role="dialog" (modal) appropriately
  • Capture and restore focus

src/v4/modal.js is the cleanest reference — under 200 lines, no dependencies.

Where to look