Système de composants sans interface · Nouvelle ressource

Système de design Headless UI.

Headless UI fournit des composants d’interaction React sans style prédéfini pour les boîtes de dialogue, les listes de sélection et les interrupteurs. Ce guide porte sur la composition de leur comportement natif en matière de focus, de sélection et de fenêtres contextuelles avec du CSS ordinaire, à partir d’un flux de partage de documents comme exemple fonctionnel.

Guide OpenDesign9 chapitres27 entrées de référenceGuide de l’application d’origine
01

Aperçu de l’interface du système de design Headless UI

Ouvrez la boîte de dialogue de partage, validez une adresse e-mail, choisissez une autorisation au clavier, ajoutez une invitation locale et activez ou désactivez le lien public.

Partage de documents avec des primitives headless nativesOuvrir l’exemple complet
@headlessui/react 2.2.10 natif avec React 19. Toutes les couleurs, la typographie, les espacements et la mise en page sont propres à cet exemple CSS ; ce ne sont pas des jetons officiels de Headless UI. · Référence de mise en œuvre
02

Référence de conception du système de design Headless UI

Headless UI ne possède aucune échelle officielle de jetons visuels. Chaque valeur numérique ou couleur ci-dessous appartient à cet exemple OpenDesign ; la documentation liée décrit les comportements et les API d’état. Version testée du package : @headlessui/react 2.2.10.

Rôles de couleur d’origine pour CSS

Source
Nom / règleValeur / configurationUtilisation
--canvas#f5f5f4 Espace de travail du document.
--surface#ffffff Feuille, panneau d’autorisations et boîte de dialogue.
--ink#1c1917 Texte principal.
--muted#57534e Texte secondaire.
--line#d6d3d1 Bordures des panneaux et des champs de saisie.
--accent#4338ca Action principale et contour de focus.
--accent-soft#eef2ff Option sélectionnée et arrière-plan de l’avatar.
--error#b91c1c Texte et bordure d’adresse e-mail invalide.

Espacement et géométrie d’origine

Source
Nom / règleValeur / configurationUtilisation
--space-1 / --space-24px / 8pxPetits incréments.
--space-3 / --space-412px / 16pxEspacement entre les contrôles et marge intérieure sur mobile.
--space-6 / --space-824px / 32pxMarge intérieure des panneaux et grands espacements.
--radius-control8pxBoutons et champs de saisie.
--radius-panel16pxFeuille et boîte de dialogue.
.dialog-panel widthmin(100%, 460px)Voile délimité sur les petits écrans.
.dialog-panel max-heightcalc(100% - 16px)Préserve l’espace à l’intérieur du conteneur de positionnement fixe.

Choix typographiques et adaptatifs d’origine

Source
Nom / règleValeur / configurationUtilisation
Body16px / 1.5 system-uiAucune police externe requise.
Screen title28px / 1.25Identité du document.
Document headline32px; 26px below 700pxHiérarchie du contenu éditorial.
Dialog heading20px / 1.3Indique la tâche de partage.
Labels and helper copy14pxContexte du champ.
Error and access description13pxRetour persistant.
Breakpoint700pxLes panneaux du document et des autorisations s’empilent.

Contrats d’interaction natifs

Source
Nom / règleValeur / configurationUtilisation
Dialog open / onCloseControlled boolean / dismissal callbackLa touche Échap et les interactions à l’extérieur demandent la fermeture ; elles n’enregistrent pas les modifications.
DialogPanelContent boundaryLe voile et le conteneur de positionnement se trouvent en dehors de cette limite.
Listbox value / onChangeControlled permission stringLa sélection au clavier met à jour l’autorisation choisie.
data-focus / data-selectedNative state attributesCSS distingue le focus clavier de la sélection confirmée.
Switch checked / onChangeControlled booleandata-checked applique le style de son état visuel.
03

Guide complet du système de design Headless UI

Un guide pratique pour choisir, appliquer et évaluer ce système dans une interface réelle.

DESIGN-headless-ui.md

Des conseils complets, un point de départ pour la mise en œuvre et des liens vers les sources.

Télécharger le guide

Système de design Headless UI : composer le comportement avant de styliser l’écran

Composer le comportement avant de styliser l’écran

Dialog, Listbox et Switch modélisent chacun une interaction différente. Dialog gère une tâche délimitée et le contexte de focus qui lui est associé ; Listbox représente un choix confirmé ; Switch représente un réglage booléen indépendant. Les combiner ne signifie pas fusionner leurs états : le brouillon d’invitation, l’autorisation choisie et le réglage du lien public restent distincts.

Extrait de ce guide OpenDesign

L’exemple utilise le composant API de React avec des importations directes, telles que DialogPanel et ListboxButton. Sa boîte de dialogue contient une liste de sélection : le positionnement et la fermeture du menu contextuel doivent donc fonctionner à l’intérieur d’une fenêtre déjà ouverte. Commencez par composer ces éléments avant d’ajouter le panneau du document et le récapitulatif des autorisations. Le CSS fourni présente un style complet pour ces éléments natifs.

Système de design Headless UI : anatomie de Dialog, Listbox et Switch

Délimiter intentionnellement les éléments superposés

DialogBackdrop est un élément frère du conteneur de positionnement fixe. DialogPanel est le panneau blanc délimité à l’intérieur de ce conteneur ; il ne remplit pas la fenêtre. DialogTitle nomme la tâche et Description explique ce qui se passe à la fermeture. Le champ de saisie natif comporte data-autofocus, et Headless UI gère le comportement du focus dans la boîte de dialogue.

Consultez la spécification associée

Listbox sélectionne une valeur ; Switch modifie une valeur booléenne

ListboxButton affiche le rôle sélectionné. ListboxOptions utilise anchor="bottom" et une largeur basée sur --button-width, afin que le menu contextuel soit aligné sur son déclencheur. Chaque ListboxOption possède une valeur de chaîne stable. Les attributs natifs data-focus et data-selected contrôlent deux traitements visuels distincts de CSS.

Le Switch du lien public se trouve dans Field avec Label, plutôt que dans une pastille colorée sans libellé. Son état activé modifie le contrôle accessible et la phrase explicative visible. L’invitation locale est indépendante du réglage du lien public.

Consultez la spécification associée

Système de design Headless UI : séparer sélection, fermeture et confirmation locale

Modèle d’état utilisé ici

À l’ouverture, un nouveau brouillon de destinataire est créé et le rôle Can view est sélectionné. Annuler, appuyer sur Échap ou cliquer à l’extérieur ferme la boîte de dialogue ; aucune de ces actions n’ajoute de destinataire. Lors de l’envoi, l’adresse e-mail est validée et toute adresse déjà présente est refusée ; l’invitation est ensuite ajoutée en mémoire et la boîte de dialogue se ferme. La confirmation précise qu’aucun e-mail n’a été envoyé.

La liste de sélection des autorisations modifie le rôle du brouillon, et non les autorisations d’un destinataire existant. L’état du lien public modifie une valeur booléenne distincte. On évite ainsi l’erreur fréquente qui consiste à traiter chaque fermeture comme un enregistrement des autorisations.

  • Saisie invalide : laissez la boîte de dialogue ouverte et associez l’erreur à l’adresse e-mail.
  • Destinataire en double : ne modifiez pas la liste des accès existante.
  • Invitation locale acceptée : mettez à jour la liste et signalez clairement le résultat, valable uniquement dans l’aperçu.

Système de design Headless UI : styliser la sélection et le positionnement au clavier séparément

Styliser la sélection et le positionnement au clavier séparément

ListboxOption expose data-focus et data-selected pour distinguer différentes situations. L’option active reçoit le traitement d’accentuation pâle, tandis que la permission sélectionnée est marquée d’une coche. Au clavier, il est ainsi possible de parcourir les options sans perdre de vue le choix déjà effectué.

La même distinction s’applique en dehors de la fenêtre contextuelle : le rôle d’erreur concerne le champ du destinataire invalide, tandis que l’apparence cochée de Switch correspond à l’accès par lien public. Limitez CSS à l’état du composant qu’il décrit. Un sélecteur trop général qui appliquerait la même couleur à tous les éléments actifs ou cochés masquerait ces différentes significations.

css
:root { --accent: #4338ca; --accent-soft: #eef2ff; --error: #b91c1c; }
.role-option[data-focus] { background: var(--accent-soft); }
.role-option[data-selected] .selected { visibility: visible; }

Référence du code

Rendre l’identité du document et les permissions lisibles — système de design Headless UI

Paramètres typographiques reproductibles

Le titre du document est en 28px avec une hauteur de ligne de 1.25, tandis que le titre de la boîte de dialogue est en 20px avec une hauteur de ligne de 1.3. Le texte courant est en 16px/1.5, dans la police système de la plateforme. Les libellés des champs et le texte d’aide sont en 14px. Ces valeurs exactes figurent dans style.css ; il n’est pas nécessaire de les déduire d’une capture d’écran.

Dans la liste des accès, le courriel utilise overflow-wrap:anywhere pour empêcher une adresse de forcer l’élargissement de sa colonne. La permission reste sur une ligne distincte. Dans la boîte de dialogue, le titre complet du document et la permission sélectionnée sont visibles au moment de l’ajout de l’invitation.

Positionnement complet de la fenêtre superposée et comportement adaptatif — système de design Headless UI CSS

La mise en page ne dépend pas d’un prérequis masqué

La feuille de style fournie comprend toute la fenêtre superposée : positionnement fixe avec inset, opacité de l’arrière-plan, z-index, largeur maximale, défilement vertical et marges intérieures. Sous 700px, les panneaux du document s’empilent et les marges intérieures de la boîte de dialogue diminuent. Aucune compilation Tailwind n’est requise.

Distinguez le conteneur de positionnement de DialogPanel. Le conteneur couvre la fenêtre disponible et ajoute une marge intérieure sur les bords ; le panneau fournit une surface de contenu aux dimensions limitées. Prévoyez une zone défilante pour les boîtes de dialogue au contenu long afin que le champ du destinataire, le choix de permission et les actions de fermeture restent accessibles sur les écrans étroits.

css
.dialog-position { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; padding: 16px; overflow-y: auto; }
.dialog-panel { width: min(100%, 460px); max-height: calc(100% - 16px); overflow-y: auto; }

Référence du code

Utiliser le comportement natif des états et du focus — système de design Headless UI

Vérification au clavier

Ouvrez la boîte de dialogue et vérifiez que le focus est initialement placé sur le destinataire. Appuyez sur Tab pour atteindre le déclencheur de permission, puis utilisez les touches fléchées et Entrée pour choisir un rôle. Échap ferme la fenêtre contextuelle active ou la boîte de dialogue, selon le contexte de focus du composant natif. Après la fermeture de la boîte de dialogue, le focus doit revenir au déclencheur de partage.

Le champ de courriel utilise aria-invalid et aria-describedby pour afficher une erreur personnalisée. La coche d’une option est décorative, car Listbox fournit les indications de sélection. Le contrôle de lien public possède un libellé visible, et la phrase explicative reste affichée après son activation ou sa désactivation. La préférence de réduction des animations CSS supprime la transition décorative du curseur du commutateur.

Consultez la spécification associée

Headless UI Utiliser un seul système de design pour générer React API de manière cohérente

Utiliser systématiquement une seule génération de React API

L’entrée React complète monte App, dont la feuille de style fournit la géométrie de la fenêtre superposée et les sélecteurs d’état. Dans l’exemple vérifié en version 2.2.10, DialogPanel, DialogBackdrop, ListboxButton et ListboxOptions sont importés directement. Les anciennes syntaxes des composants composés doivent être migrées délibérément, plutôt que mélangées à cette composition.

ListboxOptions utilise anchor="bottom" et la variable native --button-width pour aligner les options sur le déclencheur. Ces éléments font partie de l’intégration et ne sont pas de simples détails décoratifs : supprimer le conteneur de positionnement ou remplacer la largeur par une valeur fixe sans rapport peut casser une fenêtre contextuelle imbriquée, même si le champ fermé semble toujours correct.

  1. Installez @headlessui/react dans le projet React.
  2. Enregistrez App.jsx et style.css à partir des exemples, puis remplacez main.jsx comme indiqué.
  3. Lancez le serveur de développement et testez la fermeture de la boîte de dialogue, le choix d’une permission et la gestion des courriels en double.

Examiner une interaction réelle de permission — système de design Headless UI

Critères d’acceptation

Pour effectuer une vérification utile, commencez par un document auquel un destinataire a déjà accès, plutôt que par une galerie de composants vide. Ajoutez une invitation valide avec la permission « Peut commenter », puis rouvrez la boîte de dialogue et essayez la même adresse. Vérifiez que la ligne existante est conservée et qu’un message d’erreur précis s’affiche pour le doublon. Activez le lien public et vérifiez son texte explicatif indépendant.

L’exemple illustre uniquement l’état local de l’interface. Il ne fournit ni recherche de destinataires ni service de partage. La connexion à un service réel nécessite un modèle de réponse avec états de chargement et d’erreur, ainsi qu’une politique explicite pour les changements de permission simultanés ; les primitives natives ne définissent pas les règles d’autorisation.

Exemples de composants du système de design Headless UI

App.jsx — composition native du partage

Dialog contrôlé complet, Listbox pour les permissions, Switch avec libellé et validation locale.

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>;
}

Référence du code

  • Aucun courriel ni aucune requête n’est envoyé.
  • Échap et Annuler abandonnent l’invitation non enregistrée.

style.css — système visuel original complet

Tous les jetons visuels, sélecteurs d’état, règles de positionnement de la fenêtre superposée et règles adaptatives nécessaires à App.jsx. Il s’agit de choix propres à l’exemple, et non des valeurs par défaut officielles de Headless UI.

css
:root{--canvas:#f5f5f4;--surface:#fff;--ink:#1c1917;--muted:#57534e;--line:#d6d3d1;--accent:#4338ca;--accent-soft:#eef2ff;--error:#b91c1c;--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-6:24px;--space-8:32px;--radius-control:8px;--radius-panel:16px;font:16px/1.5 system-ui,sans-serif;color:var(--ink);background:var(--canvas)}*{box-sizing:border-box}body{margin:0}button,input{font:inherit}button{cursor:pointer;border:1px solid var(--line);border-radius:var(--radius-control);padding:10px 16px;background:var(--surface);color:var(--ink);font-weight:600}button:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.primary{background:var(--accent);color:white;border-color:var(--accent)}.sharing-shell{max-width:1120px;margin:auto;padding:32px}.sharing-shell>header{display:flex;align-items:center;justify-content:space-between;gap:24px;margin-bottom:32px}.eyebrow{font-size:11px;letter-spacing:.1em;color:var(--accent);font-weight:700}h1{font-size:28px;line-height:1.25;margin:8px 0}h2{font-size:20px;line-height:1.3;margin:0 0 12px}h3{font-size:16px;margin:24px 0 8px}p{margin:0 0 16px}.muted{color:var(--muted);font-size:14px}.document-grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(260px,1fr);gap:24px}.paper,.access{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-panel);padding:32px}.paper h2{font-size:32px;max-width:15ch;margin-top:24px}.paper p{max-width:48ch}.tag{font-size:11px;font-weight:600;border:1px solid var(--line);padding:4px 8px;border-radius:4px}.rule{height:1px;background:var(--line);margin:24px 0}.person{display:flex;gap:12px;padding:16px 0;border-bottom:1px solid var(--line)}.person strong{display:block;font-size:14px;overflow-wrap:anywhere}.person small{color:var(--muted)}.avatar{flex:none;width:32px;height:32px;border-radius:50%;background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;font-size:12px}.link-setting{display:flex;align-items:center;justify-content:space-between;margin-top:24px;font-weight:600}.switch{padding:3px;width:44px;height:26px;border:0;border-radius:999px;background:#a8a29e}.switch span{display:block;width:20px;height:20px;border-radius:50%;background:white;transition:transform .15s}.switch[data-checked]{background:var(--accent)}.switch[data-checked] span{transform:translateX(18px)}.link-detail{margin-top:12px;font-size:13px;color:var(--muted)}.notice{font-size:13px;color:var(--accent)}footer{font-size:12px;color:var(--muted);margin-top:24px}.dialog-root{position:fixed;inset:0;z-index:20}.backdrop{position:fixed;inset:0;background:rgb(28 25 23/.45)}.dialog-position{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:16px;overflow-y:auto}.dialog-panel{width:min(100%,460px);max-height:calc(100% - 16px);overflow-y:auto;background:var(--surface);padding:32px;border-radius:var(--radius-panel);box-shadow:0 24px 64px rgb(0 0 0/.22)}.field-label{display:block;font-size:14px;font-weight:600;margin:20px 0 8px}input[type=email]{display:block;width:100%;padding:10px 12px;border:1px solid var(--line);border-radius:var(--radius-control);background:white;color:var(--ink)}input[aria-invalid=true]{border-color:var(--error)}.error{font-size:13px;color:var(--error);margin:8px 0}.role-button{display:flex;justify-content:space-between;width:100%;text-align:left}.role-options{z-index:30;width:var(--button-width);--anchor-gap:4px;background:var(--surface);border:1px solid var(--line);padding:4px;border-radius:8px;box-shadow:0 8px 24px rgb(0 0 0/.12);outline:none}.role-option{display:flex;justify-content:space-between;padding:10px;border-radius:4px;cursor:pointer;font-size:14px}.role-option[data-focus]{background:var(--accent-soft)}.role-option .selected{visibility:hidden;color:var(--accent)}.role-option[data-selected] .selected{visibility:visible}.permission-help{margin-top:12px}.dialog-actions{display:flex;justify-content:flex-end;gap:12px;margin-top:24px}@media(max-width:700px){.sharing-shell{padding:20px}.sharing-shell>header{align-items:flex-start;flex-direction:column}.document-grid{grid-template-columns:1fr}.paper,.access{padding:24px}.paper h2{font-size:26px}.dialog-panel{padding:24px}}@media(prefers-reduced-motion:reduce){.switch span{transition:none}}

Référence du code

  • Distinguez data-focus de data-selected.
  • Tailwind n’est pas un prérequis.
04

Configuration de l’agent IA pour système de design Headless UI

Point de départ de l’implémentation

Les fichiers complets des styles et des composants sont fournis ci-dessus. Nécessite la version de Node prise en charge par la version de Vite sélectionnée. La démo intègre localement le même paquet de composants natifs.

texte
# 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

Un prompt pour votre projet

Prompt pour l’agent IA
Utilisez App.jsx et style.css de Headless UI. Conservez la composition native de Dialog, Listbox et Switch, séparez les permissions provisoires des accès confirmés et préservez tous les sélecteurs de focus et d’état sélectionné. Remplacez l’ajout local d’invitations par notre API autorisé, avec des états de chargement et d’échec ; n’enregistrez jamais les changements dans onClose. Ne présentez pas la palette personnalisée CSS comme un thème officiel de Headless UI.
05

Sources et périmètre du système de design Headless UI

Configuration visuelle originale de OpenDesign et exemple React utilisant le comportement natif de Headless UI 2.2.10. Les valeurs de CSS sont explicitement des choix propres à l’application ; Headless UI ne fournit pas ce thème. Les invitations et les changements de permission n’existent que dans la mémoire locale de l’aperçu.