Lumenv0.2

Components/Overlays

Dialog

A modal moment. Focus is trapped inside; escape or a click outside closes it.

Default

Props

PropTypeDefaultDescription
openbooleanWhether the dialog is shown
onClose() => voidCalled on escape or backdrop click
titlestringAccessible heading
childrenReactNodeBody content

Installation

Paste the source into components/dialog.tsx. Traps focus and closes on escape or backdrop click — no dependencies required.

'use client';

import { useEffect, useRef } from 'react';
import type { ReactNode } from 'react';

interface DialogProps {
  open: boolean;
  onClose: () => void;
  title: string;
  children?: ReactNode;
}

export function Dialog({ open, onClose, title, children }: DialogProps) {
  const panelRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!open) return;

    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        onClose();
        return;
      }
      if (e.key === 'Tab') {
        const panel = panelRef.current;
        if (!panel) return;
        const focusable = panel.querySelectorAll<HTMLElement>(
          'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])'
        );
        if (focusable.length === 0) return;
        const first = focusable[0];
        const last = focusable[focusable.length - 1];
        if (e.shiftKey && document.activeElement === first) {
          e.preventDefault();
          last.focus();
        } else if (!e.shiftKey && document.activeElement === last) {
          e.preventDefault();
          first.focus();
        }
      }
    };

    document.addEventListener('keydown', onKeyDown);
    panelRef.current?.focus();
    return () => document.removeEventListener('keydown', onKeyDown);
  }, [open, onClose]);

  if (!open) return null;

  return (
    <div
      onClick={onClose}
      style={{
        position: 'fixed',
        inset: 0,
        backgroundColor: 'rgba(26, 24, 20, 0.32)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        zIndex: 100,
        padding: '24px',
      }}
    >
      <div
        ref={panelRef}
        role="dialog"
        aria-modal="true"
        aria-label={title}
        tabIndex={-1}
        onClick={(e) => e.stopPropagation()}
        style={{
          width: '100%',
          maxWidth: '420px',
          backgroundColor: 'var(--color-surface)',
          border: '0.5px solid var(--color-border)',
          borderRadius: 'var(--radius-lg)',
          padding: '24px',
          outline: 'none',
        }}
      >
        <h2
          style={{
            margin: '0 0 8px',
            fontFamily: 'var(--font-sans)',
            fontSize: '18px',
            fontWeight: 540,
            letterSpacing: '-0.01em',
            color: 'var(--color-text)',
          }}
        >
          {title}
        </h2>
        {children && (
          <div
            style={{
              fontFamily: 'var(--font-sans)',
              fontSize: '14px',
              fontWeight: 420,
              color: 'var(--color-text-muted)',
              lineHeight: 1.6,
            }}
          >
            {children}
          </div>
        )}
      </div>
    </div>
  );
}

Built from Lumen tokens. Edit the tokens