Headless UI stellt ungestaltete React-Interaktionskomponenten für Dialoge, Listenfelder und Umschalter bereit. Diese Anleitung zeigt, wie sich deren integriertes Fokus-, Auswahl- und Popup-Verhalten mit gewöhnlichem CSS kombinieren lässt. Als Beispiel dient ein Ablauf zum Teilen von Dokumenten.
Öffnen Sie den Dialog zum Teilen, prüfen Sie eine E-Mail-Adresse, wählen Sie mit der Tastatur eine Berechtigung aus, fügen Sie eine lokale Einladung hinzu und schalten Sie den Status des öffentlichen Links um.
Natives @headlessui/react 2.2.10 mit React 19. Alle Farben, Schriftarten, Abstände und Layouts sind eigens für dieses Beispiel erstellte CSS und keine offiziellen Headless UI-Design-Tokens. · Implementierungsreferenz
02
Designreferenz für das Headless UI-Designsystem
Headless UI verfügt über keine offizielle visuelle Design-Tokens-Skala. Alle nachfolgenden Zahlen- und Farbwerte gehören zu diesem OpenDesign-Beispiel; die verlinkte Dokumentation beschreibt Verhalten und Zustands-APIs. Getestetes Paket: @headlessui/react 2.2.10.
Headless UI-Designsystem: Verhalten zusammenstellen, bevor der Bildschirm gestaltet wird
Verhalten zusammenstellen, bevor der Bildschirm gestaltet wird
Dialog, Listbox und Switch bilden jeweils eine andere Interaktion ab. Der Dialog steuert eine klar begrenzte Aufgabe und ihren Fokuskontext; die Listbox stellt eine bestätigte Auswahl dar; der Switch bildet eine unabhängige boolesche Einstellung ab. Sie lassen sich kombinieren, ohne ihren Zustand zusammenzuführen: Der Einladungsentwurf, die ausgewählte Berechtigung und die Einstellung für den öffentlichen Link bleiben getrennt.
Das Beispiel verwendet die Komponente API aus React mit direkten Imports wie DialogPanel und ListboxButton. Der Dialog enthält eine Listbox. Daher müssen Positionierung und Schließen des Pop-ups auch innerhalb eines bereits geöffneten Overlays funktionieren. Beginnen Sie mit dieser Zusammensetzung, bevor Sie den Dokumentbereich und die Zugriffsübersicht ergänzen. Das bereitgestellte CSS zeigt eine vollständige Gestaltung dieser nativen Elemente.
Anatomie von Dialog, Listbox und Switch im Headless UI-Designsystem
Grenzen des Overlays bewusst festlegen
DialogBackdrop ist ein Geschwisterelement des Containers für die feste Positionierung. DialogPanel ist der begrenzte weiße Bereich innerhalb dieses Containers und füllt nicht den gesamten Viewport aus. DialogTitle benennt die Aufgabe, und Description erklärt, was beim Schließen geschieht. Das native Eingabefeld hat data-autofocus; Headless UI verwaltet das Fokusverhalten des umgebenden Dialogs.
Eine Listbox wählt einen Wert aus; ein Switch ändert einen booleschen Wert
ListboxButton zeigt die ausgewählte Rolle an. ListboxOptions verwendet anchor="bottom" und eine Breite, die sich an --button-width orientiert, damit das Pop-up bündig mit seinem Auslöser abschließt. Jede ListboxOption hat einen stabilen Zeichenfolgenwert. Die nativen Attribute data-focus und data-selected steuern zwei unterschiedliche CSS-Darstellungen.
Der Switch für den öffentlichen Link befindet sich in Field mit Label und nicht in einem unbeschrifteten, eingefärbten Feld. Sein aktivierter Zustand ändert sowohl das barrierefreie Steuerelement als auch den sichtbaren Erklärungssatz. Die lokale Einladung ist unabhängig von der Einstellung für den öffentlichen Link.
Headless UI-Designsystem: Auswahl, Schließen und lokale Bestätigung getrennt behandeln
Das hier verwendete Zustandsmodell
Beim Öffnen werden ein neuer Empfängerentwurf und die Rolle „Darf ansehen“ initialisiert. Abbrechen, Escape und ein Klick außerhalb schließen den Dialog; keine dieser Aktionen fügt einen Empfänger hinzu. Beim Absenden wird die E-Mail-Adresse geprüft und eine bereits vorhandene Adresse abgewiesen. Anschließend wird die Einladung im Arbeitsspeicher hinzugefügt und der Dialog geschlossen. Die Bestätigung weist ausdrücklich darauf hin, dass keine E-Mail versendet wurde.
Die Listbox für Berechtigungen ändert die Rolle im Entwurf, nicht den Zugriff eines bestehenden Empfängers. Der Zustand des öffentlichen Links ändert einen separaten booleschen Wert. So wird der häufige Fehler vermieden, jedes Schließen des Dialogs als Speicherung von Berechtigungen zu behandeln.
Ungültige Eingabe: Dialog geöffnet lassen und den Fehler mit der E-Mail-Adresse verknüpfen.
Lokale Einladung angenommen: Liste aktualisieren und das Ergebnis der Vorschau ansagen.
Headless UI-Designsystem: Auswahl und Tastaturfokus unabhängig voneinander gestalten
Auswahl und Tastaturfokus unabhängig voneinander gestalten
ListboxOption stellt data-focus und data-selected für unterschiedliche Zustände bereit. Die fokussierte Option erhält die dezente Akzentgestaltung, während die bestätigte Berechtigung mit einem Häkchen markiert wird. So können Tastaturnutzende durch die Alternativen navigieren, ohne die bestehende Auswahl aus dem Blick zu verlieren.
Dieselbe Unterscheidung gilt außerhalb des Pop-ups: Die Fehlerrolle gehört zum ungültigen Empfängerfeld, während das aktivierte Erscheinungsbild von Switch den Zugriff über den öffentlichen Link kennzeichnet. Beschränken Sie CSS auf den Komponentenstatus, den es beschreibt. Ein breit gefasster Selektor, der alle fokussierten oder aktivierten Elemente identisch einfärbt, würde diese unterschiedlichen Bedeutungen verwischen.
Headless UI-Designsystem: Dokumentidentität und Berechtigungen verständlich darstellen
Reproduzierbare typografische Einstellungen
Der Dokumenttitel hat eine Schriftgröße von 28px und einen Zeilenabstand von 1.25, der Dialogtitel eine Schriftgröße von 20px und einen Zeilenabstand von 1.3. Der Fließtext verwendet die Systemschrift der Plattform mit 16px/1.5. Feldbeschriftungen und Hilfetexte sind 14px groß. Diese exakten Werte stehen in style.css; sie müssen nicht aus einem Screenshot abgeleitet werden.
Für die E-Mail-Adresse in der Zugriffsliste wird overflow-wrap:anywhere verwendet, damit sie die Spalte nicht verbreitert. Die Berechtigung wird in einer separaten Zeile angezeigt. Im Dialog sind der vollständige Dokumenttitel und die ausgewählte Berechtigung sichtbar, sobald die Einladung hinzugefügt wird.
Headless UI-Designsystem: vollständige Overlay-Positionierung und responsives CSS
Das Layout ist nicht hinter einer Voraussetzung verborgen
Das bereitgestellte Stylesheet enthält das vollständige Overlay: feste Positionierung mit Insets, Deckkraft des Backdrops, z-index, maximale Breite, vertikales Scrollen und Innenabstand. Unter 700px werden die Dokumentbereiche untereinander angeordnet und der Innenabstand des Dialogs verringert. Ein Tailwind-Build wird nicht vorausgesetzt.
Halten Sie den Positionierungscontainer und DialogPanel voneinander getrennt. Der Container erstreckt sich über den verfügbaren Viewport und sorgt für einen Randabstand; das Panel stellt die in der Breite begrenzte Inhaltsfläche bereit. Geben Sie längeren Dialoginhalten einen scrollbaren Bereich, damit Empfängerfeld, Berechtigungsauswahl und Schließen-Aktionen auch bei schmalen Ansichten erreichbar bleiben.
Headless UI-Designsystem: nativen Status und Fokusverhalten nutzen
Tastaturbedienung überprüfen
Öffnen Sie den Dialog und prüfen Sie, ob der Fokus zunächst auf dem Empfänger liegt. Navigieren Sie mit der Tabulatortaste zum Berechtigungstrigger und wählen Sie mit den Pfeiltasten und Enter eine Rolle aus. Escape schließt das aktive Pop-up oder den Dialog entsprechend dem aktuellen Fokuskontext der nativen Komponente. Nach dem Schließen des Dialogs sollte der Fokus zum Freigabe-Trigger zurückkehren.
Die E-Mail-Adresse verwendet aria-invalid und aria-describedby für ihren benutzerdefinierten Fehler. Das Häkchen in einer Option ist dekorativ, da Listbox die Auswahlsemantik bereitstellt. Das Steuerelement für den öffentlichen Link hat ein sichtbares Label, und der erklärende Satz bleibt nach dem Umschalten erhalten. Die Einstellung für reduzierte Bewegung CSS entfernt den dekorativen Übergang des Switch-Daumens.
Headless UI-Designsystem: eine React-API-Generation durchgängig verwenden
Eine React-API-Generation durchgängig verwenden
Der vollständige React-Eintrag bindet App ein; dessen Stylesheet liefert die Geometrie des Overlays und die Selektoren für die Zustände. DialogPanel, DialogBackdrop, ListboxButton und ListboxOptions werden im geprüften Beispiel mit Version 2.2.10 direkt importiert. Ältere Schreibweisen für zusammengesetzte Komponenten sollten gezielt migriert und nicht mit dieser Zusammensetzung vermischt werden.
ListboxOptions verwendet anchor="bottom" und die native Variable --button-width, um die Optionen am Trigger auszurichten. Diese Details sind Teil der Integration und keine optionale Dekoration: Wird der Positionierungscontainer entfernt oder die Breite durch einen unabhängigen festen Wert ersetzt, kann ein verschachteltes Pop-up fehlerhaft dargestellt werden, obwohl das geschlossene Feld weiterhin korrekt aussieht.
Installieren Sie @headlessui/react im React-Projekt.
Speichern Sie App.jsx und style.css aus den Beispielen und ersetzen Sie anschließend main.jsx wie gezeigt.
Starten Sie den Entwicklungsserver und testen Sie das Schließen des Dialogs, die Berechtigungsauswahl und die Behandlung doppelter E-Mail-Adressen.
Headless UI-Designsystem: eine echte Berechtigungsinteraktion prüfen
Abnahmekriterien
Eine aussagekräftige Prüfung beginnt mit einem Dokument und einem bestehenden Empfänger, nicht mit einer leeren Komponentengalerie. Fügen Sie eine gültige Einladung mit der Berechtigung „Darf kommentieren“ hinzu und öffnen Sie den Dialog erneut, um dieselbe E-Mail-Adresse einzugeben. Prüfen Sie, ob der ursprüngliche Eintrag erhalten bleibt und für das Duplikat eine konkrete Fehlermeldung erscheint. Aktivieren Sie den öffentlichen Link und prüfen Sie die dazugehörige, unabhängige Erklärung.
Das Beispiel demonstriert ausschließlich den lokalen UI-Status. Es bietet weder eine Empfängersuche noch ein Backend für die Freigabe. Für die Anbindung eines echten Dienstes sind ein Statusmodell für ausstehende und fehlgeschlagene Anfragen sowie eine explizite Richtlinie für gleichzeitige Berechtigungsänderungen erforderlich; die nativen Primitives legen keine Autorisierungsregeln fest.
Headless UI-Designsystem: Komponentenbeispiele
App.jsx — native Freigabezusammensetzung
Vollständig kontrollierter Dialog, Berechtigungs-Listbox, beschrifteter Switch und lokale Validierung.
jsx
import React, { useState } from 'react';
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle, Description, Listbox, ListboxButton, ListboxOptions, ListboxOption, Switch, Field, Label } from '@headlessui/react';
import './style.css';
const roles = ['Can view', 'Can comment', 'Can edit'];
export default function App() {
const [open,setOpen]=useState(false),[role,setRole]=useState(roles[0]),[publicLink,setPublicLink]=useState(false),[email,setEmail]=useState(''),[error,setError]=useState(''),[notice,setNotice]=useState('');
const [people,setPeople]=useState([{email:'[email protected]',role:'Can edit'}]);
const start=()=>{setEmail('');setError('');setRole(roles[0]);setOpen(true)};
const submit=e=>{e.preventDefault();if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)){setError('Enter a complete email address.');return;}if(people.some(p=>p.email===email)){setError('This person already appears in the access list.');return;}setPeople([...people,{email,role}]);setOpen(false);setNotice('Invitation added to this local example only. No email was sent.');};
return <main className='sharing-shell'>
<header><div><span className='eyebrow'>HEADLESS UI / CUSTOM SURFACE</span><h1>Quarterly brief</h1><p className='muted'>Research · Updated today</p></div><button className='primary' onClick={start}>Share document</button></header>
<div className='document-grid'><article className='paper'><span className='tag'>WORKING DOCUMENT</span><h2>Make the next decision clearer.</h2><p>Three research priorities for the coming quarter, shared with the people who need to review them.</p><div className='rule'/><h3>01 / Understand the handoff</h3><p>Compare where context is lost between research notes and the next product decision.</p><h3>02 / Review access deliberately</h3><p>Keep editing rights separate from the ability to read a document.</p></article>
<aside className='access'><h2>People with access</h2><p className='muted'>The list below is local demonstration state.</p>{people.map((p,i)=><div className='person' key={p.email}><span className='avatar'>{i===0?'AK':'+'}</span><div><strong>{p.email}</strong><small>{p.role}</small></div></div>)}<Field className='link-setting'><Label>Public link</Label><Switch checked={publicLink} onChange={setPublicLink} className='switch'><span /></Switch></Field><p className='link-detail'>{publicLink?'Anyone with the example link could view this document. This changes only the local preview.':'Restricted to the named people in this example.'}</p><p role='status' className='notice'>{notice}</p></aside></div>
<Dialog open={open} onClose={()=>setOpen(false)} className='dialog-root'><DialogBackdrop className='backdrop'/><div className='dialog-position'><DialogPanel className='dialog-panel'><DialogTitle as='h2'>Share quarterly brief</DialogTitle><Description className='muted'>Choose one recipient and a permission. Closing discards this invitation draft.</Description><form noValidate onSubmit={submit}><label className='field-label' htmlFor='recipient'>Recipient email</label><input id='recipient' data-autofocus type='email' value={email} onChange={e=>{setEmail(e.target.value);setError('')}} aria-invalid={!!error} aria-describedby={error?'email-error':undefined} placeholder='[email protected]'/>{error&&<p id='email-error' role='alert' className='error'>{error}</p>}<Listbox value={role} onChange={setRole}><Label className='field-label'>Permission</Label><ListboxButton className='role-button'>{role}<span aria-hidden='true'>⌄</span></ListboxButton><ListboxOptions anchor='bottom' className='role-options'>{roles.map(value=><ListboxOption key={value} value={value} className='role-option'><span>{value}</span><span className='selected' aria-hidden='true'>✓</span></ListboxOption>)}</ListboxOptions></Listbox><p className='muted permission-help'>{role==='Can edit'?'May change document content in this example.':role==='Can comment'?'May add comments without editing content.':'May read the document without changing it.'}</p><div className='dialog-actions'><button type='button' onClick={()=>setOpen(false)}>Cancel</button><button className='primary' type='submit'>Add invitation</button></div></form></DialogPanel></div></Dialog>
<footer>Native Dialog, Listbox, and Switch behavior. Original CSS below supplies all visual values; Headless UI does not ship this palette.</footer>
</main>;
}
Es werden weder E-Mails versendet noch Anfragen gesendet.
Escape und Abbrechen verwerfen die nicht gespeicherte Einladung.
style.css — vollständiges originales visuelles System
Alle visuellen Designwerte, Zustandsselektoren, die Positionierung des Overlays und die responsiven Regeln, die App.jsx benötigt. Dies sind Entscheidungen für das Beispiel, keine offiziellen Headless UI-Standardwerte.
Die vollständigen Style- und Komponentendateien sind oben enthalten. Erfordert eine Node-Version, die von der ausgewählten Vite-Version unterstützt wird. Die Demo bündelt dasselbe native Komponentenpaket lokal.
text
# Terminal
npm create vite@latest sharing-demo -- --template react
cd sharing-demo
npm install
npm install @headlessui/[email protected]
# Save the complete examples as src/App.jsx and src/style.css.
# Replace src/main.jsx with:
import React from 'react';
import {createRoot} from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(<App/>);
# Terminal
npm run dev
Ein Prompt für dein Projekt
Agenten-Prompt
Verwenden Sie das bereitgestellte Headless UI App.jsx und style.css. Behalten Sie die native Zusammensetzung aus Dialog, Listbox und Switch bei, trennen Sie Berechtigungsentwürfe von bestätigten Zugriffsrechten und behalten Sie alle Selektoren für Fokus- und Auswahlzustände bei. Ersetzen Sie das lokale Hinzufügen von Einladungen durch unsere autorisierte API, einschließlich Status für ausstehende und fehlgeschlagene Anfragen; speichern Sie niemals in onClose. Bezeichnen Sie die benutzerdefinierte CSS-Farbpalette nicht als offizielles Headless UI-Theme.
05
Headless UI-Designsystem: Quellen und Geltungsbereich
Originale visuelle Konfiguration von OpenDesign und React-Beispiel mit dem nativen Verhalten von Headless UI 2.2.10. Die Werte von CSS sind ausdrücklich Entscheidungen auf Anwendungsebene; Headless UI stellt dieses Theme nicht bereit. Einladungen und Berechtigungsänderungen sind nur im lokalen Vorschauzustand vorhanden.