Modal

Ein Modal ist ein popover-Element. Jeder Button, dessen popovertarget zur id des Modals passt, öffnet es — ganz ohne Skript, samt Hintergrund, Schließen bei Klick nach außen, Escape und Fokusverwaltung, die alle dem Browser gehören.

Deshalb ist die id Pflicht, und deshalb wirft die Komponente ohne sie: die id ist die ganze Verkabelung.

Einfaches Modal

Jedes Modal auf dieser Seite geht wirklich auf — probier es aus.

fragment(
  button({ popovertarget: 'demo-basic' }, 'Modal öffnen'),
  modal({ id: 'demo-basic', title: 'Website neu bauen?' },
    'Das führt sitelo build aus und veröffentlicht dist/ erneut.',
  ),
)

Ein Schließen-Button ist jeder Button, der mit popovertargetaction="hide" auf dieselbe id zeigt.

fragment(
  button({ color: 'danger', popovertarget: 'demo-confirm' }, 'Seite löschen…'),
  modal({
    id: 'demo-confirm',
    title: 'Diese Seite löschen?',
    footer: stack({ direction: 'row', gap: 'sm' },
      button({
        variant: 'ghost',
        color: 'neutral',
        popovertarget: 'demo-confirm',
        popovertargetaction: 'hide',
      }, 'Abbrechen'),
      button({ color: 'danger' }, 'Löschen'),
    ),
  }, 'Das lässt sich nicht rückgängig machen. Das erzeugte HTML verschwindet beim nächsten Build.'),
)

Größen

fragment(
  stack({ direction: 'row', gap: 'sm', wrap: true },
    button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-sm' }, 'Klein'),
    button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-md' }, 'Mittel'),
    button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-lg' }, 'Groß'),
  ),
  modal({ id: 'demo-sm', size: 'sm', title: 'Klein' }, 'size: sm — etwa 24rem.'),
  modal({ id: 'demo-md', title: 'Mittel' }, 'Der Standard — etwa 32rem.'),
  modal({ id: 'demo-lg', size: 'lg', title: 'Groß' }, 'size: lg — etwa 48rem.'),
)

Formulare in einem Modal

fragment(
  button({ variant: 'soft', popovertarget: 'demo-form' }, 'Neue Seite…'),
  modal({
    id: 'demo-form',
    title: 'Neue Seite',
    footer: stack({ direction: 'row', gap: 'sm' },
      button({ variant: 'ghost', color: 'neutral', popovertarget: 'demo-form', popovertargetaction: 'hide' }, 'Abbrechen'),
      button({ type: 'submit' }, 'Anlegen'),
    ),
  },
    stack({ gap: 'md' },
      textField({ label: 'Titel', name: 'modal-title', placeholder: 'Über' }),
      selectField({ label: 'Endung', name: 'modal-ext', options: ['.ht.js', '.ht.ts', '.ht.jsx'] }),
    ),
  ),
)

Ohne Schließen-Button

closable: false lässt das × in der Ecke weg. Escape und ein Klick nach außen schließen es weiterhin — ein Popover lässt sich nicht wirklich blockierend machen, und das ist ohnehin meist das richtige Verhalten.

fragment(
  button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-bare' }, 'Ohne Schließen-Button'),
  modal({ id: 'demo-bare', title: 'Drück Escape', closable: false },
    'Oder klick irgendwo außerhalb dieses Dialogs.',
  ),
)

Langer Inhalt

Der Körper scrollt; Kopf und Fuß bleiben stehen.

fragment(
  button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-long' }, 'Langes Modal'),
  modal({
    id: 'demo-long',
    title: 'Release Notes',
    footer: button({ popovertarget: 'demo-long', popovertargetaction: 'hide' }, 'Schließen'),
  },
    stack({ gap: 'md' },
      ...Array.from({ length: 12 }, (unused, index) =>
        text({ variant: 'small', tone: 'muted' }, 'Änderung ' + (index + 1) + ' — etwas wurde behoben.'),
      ),
    ),
  ),
)

Scrollen im Hintergrund

Die Seite hinter einem offenen Modal scrollt nicht. Das ist das Einzige, was die Popover-API dir überlässt, und es passiert hier in CSS — kein Skript, nichts zu initialisieren. Übergib lockScroll: false, damit der Hintergrund wie gewohnt scrollt.

Browser-Unterstützung

Die Popover-API gibt es in jedem aktuellen Browser. In einem zu alten, der sie nicht kennt, rendert das Modal inline in der Seite statt darüber: sichtbar und benutzbar, nur eben nicht überlagert. Nichts verschwindet.

Props

PropTypStandardBeschreibung
idstring—Pflicht. Worauf das popovertarget eines Auslösers zeigt.
titleChild—Überschrift und zugänglicher Name des Dialogs.
size'sm' | 'md' | 'lg''md'Maximale Breite.
footerChild—Untere Reihe, auf einem eigenen getönten Band.
closablebooleantrueDas × in der Kopfzeile zeigen.
closeLabelstring'Close'Zugänglicher Name dieses Buttons.
lockScrollbooleantrueDie Seite dahinter am Scrollen hindern, solange es offen ist.

closeButton({ target }) rendert dieses × für sich, für eine Kopfzeile, die du selbst baust.