Sistema di componenti senza interfaccia · Nuova risorsa

Sistema di progettazione Headless UI.

Headless UI offre componenti di interazione senza stile per React, destinati a finestre di dialogo, listbox e interruttori. Questa guida si concentra sulla composizione del comportamento nativo di focus, selezione e popup con CSS ordinario, usando come esempio pratico un flusso di condivisione dei documenti.

Guida OpenDesign9 capitoli27 voci di riferimentoGuida applicativa originale
01

Anteprima dell’interfaccia del sistema di progettazione sistema di design Headless UI

Apri il dialogo di condivisione, convalida un indirizzo email, scegli un’autorizzazione con la tastiera, aggiungi un invito locale e attiva o disattiva il link pubblico.

Condivisione di documenti con primitive headless nativeApri l’esempio completo
@headlessui/react 2.2.10 nativo con React 19. Tutti i colori, la tipografia, le spaziature e il layout sono CSS originali dell’esempio, non token ufficiali di Headless UI. · Riferimento per l’implementazione
02

Riferimento di progettazione del sistema di progettazione sistema di design Headless UI

Headless UI non dispone di una scala ufficiale di token visivi. Tutti i valori numerici e cromatici qui sotto appartengono a questo esempio OpenDesign; la documentazione collegata descrive i comportamenti e le API di stato. Pacchetto testato: @headlessui/react 2.2.10.

Ruoli cromatici originali di CSS

Fonte
Nome / regolaValore / configurazioneUtilizzo
--canvas#f5f5f4 Area di lavoro del documento.
--surface#ffffff Carta, pannello delle autorizzazioni e dialogo.
--ink#1c1917 Testo primario.
--muted#57534e Testo secondario.
--line#d6d3d1 Bordi di pannelli e campi di input.
--accent#4338ca Azione primaria e contorno di evidenziazione.
--accent-soft#eef2ff Sfondo dell’opzione selezionata e dell’avatar.
--error#b91c1c Testo e bordo dell’email non valida.

Spaziatura e geometria originali

Fonte
Nome / regolaValore / configurazioneUtilizzo
--space-1 / --space-24px / 8pxIncrementi ridotti.
--space-3 / --space-412px / 16pxSpazi tra i controlli e margine interno su mobile.
--space-6 / --space-824px / 32pxSpaziatura interna del pannello e separazione principale.
--radius-control8pxPulsanti e campi di input.
--radius-panel16pxCarta e dialogo.
.dialog-panel widthmin(100%, 460px)Sovrapposizione con larghezza limitata sugli schermi stretti.
.dialog-panel max-heightcalc(100% - 16px)Lascia spazio all’interno del contenitore con posizionamento fisso.

Scelte tipografiche e responsive originali

Fonte
Nome / regolaValore / configurazioneUtilizzo
Body16px / 1.5 system-uiNon è necessario alcun carattere esterno.
Screen title28px / 1.25Identità del documento.
Document headline32px; 26px below 700pxGerarchia editoriale dei contenuti.
Dialog heading20px / 1.3Indica l’attività di condivisione.
Labels and helper copy14pxContesto del campo.
Error and access description13pxFeedback persistente.
Breakpoint700pxIl documento e i pannelli delle autorizzazioni si dispongono uno sotto l’altro.

Comportamenti nativi delle interazioni

Fonte
Nome / regolaValore / configurazioneUtilizzo
Dialog open / onCloseControlled boolean / dismissal callbackLa pressione di Escape e l’interazione all’esterno richiedono la chiusura; non salvano le modifiche.
DialogPanelContent boundaryLo sfondo oscurato e il contenitore di posizionamento sono esterni a questo limite.
Listbox value / onChangeControlled permission stringLa selezione da tastiera aggiorna l’autorizzazione selezionata.
data-focus / data-selectedNative state attributesCSS distingue il focus da tastiera dalla selezione confermata.
Switch checked / onChangeControlled booleandata-checked definisce lo stile dello stato visivo.
03

Guida completa al sistema di progettazione sistema di design Headless UI

Una guida pratica per scegliere, applicare e valutare questo sistema in un’interfaccia reale.

DESIGN-headless-ui.md

Indicazioni complete, un punto di partenza per l’implementazione e link alle fonti.

Scarica la guida

Sistema di progettazione sistema di design Headless UI: componi il comportamento prima di definire lo stile della schermata

Componi il comportamento prima di definire lo stile della schermata

Dialog, Listbox e Switch modellano ciascuno un’interazione diversa. Dialog gestisce un’attività circoscritta e il relativo contesto di focus; Listbox rappresenta una singola scelta confermata; Switch rappresenta un’impostazione booleana indipendente. Combinarli non significa unificarne lo stato: la bozza dell’invito, l’autorizzazione selezionata e l’impostazione del link pubblico restano separati.

Da questa guida su OpenDesign

L’esempio usa il componente React API con import diretti, come DialogPanel e ListboxButton. La finestra di dialogo contiene un listbox, quindi il posizionamento del popup e la sua chiusura devono funzionare all’interno di un overlay già presente. Parti da questa composizione prima di aggiungere il pannello del documento e il riepilogo degli accessi. Il CSS fornito è un esempio completo di aspetto per questi elementi nativi.

Anatomia di Dialog, Listbox e Switch nel sistema di progettazione sistema di design Headless UI

Definisci con attenzione i limiti della sovrapposizione

DialogBackdrop è un elemento fratello del contenitore con posizionamento fisso. DialogPanel è il pannello bianco circoscritto all’interno di tale contenitore; non occupa l’intera viewport. DialogTitle indica l’attività e Description spiega cosa accade quando si chiude. L’input nativo ha data-autofocus e Headless UI gestisce il comportamento del focus nel dialog circostante.

Leggi la specifica di supporto

Una listbox seleziona un valore; uno switch modifica un valore booleano

ListboxButton mostra il ruolo selezionato. ListboxOptions usa anchor="bottom" e una larghezza basata su --button-width, così il menu a comparsa si allinea al relativo pulsante di attivazione. Ogni ListboxOption ha un valore stringa stabile. Gli attributi nativi data-focus e data-selected determinano due trattamenti CSS distinti.

Lo Switch per il link pubblico si trova in Field con Label, anziché essere un pulsante a pillola senza etichetta. Quando è selezionato, cambia lo stato del controllo accessibile e la frase esplicativa visibile. L’invito locale è indipendente dall’impostazione del link pubblico.

Leggi la specifica di supporto

Sistema di design Headless UI Sistema di progettazione: separare selezione, chiusura e conferma locale

Il modello di stato utilizzato qui

All’apertura viene inizializzata una nuova bozza per il destinatario con il ruolo Can view. Annulla, Escape e un clic all’esterno chiudono la finestra di dialogo; nessuna di queste azioni aggiunge un destinatario. Al momento dell’invio, l’email viene convalidata e si rifiuta un indirizzo già presente; poi l’invito viene aggiunto alla memoria locale e la finestra si chiude. La conferma specifica chiaramente che non è stata inviata alcuna email.

Il Listbox delle autorizzazioni modifica il ruolo della bozza, non l’accesso del destinatario esistente. Lo stato del link pubblico modifica una variabile booleana separata. In questo modo si evita il comune errore di considerare ogni evento di chiusura come un salvataggio delle autorizzazioni.

  • Input non valido: lascia aperta la finestra di dialogo e associa l’errore all’email.
  • Destinatario duplicato: lascia invariato l’elenco degli accessi esistente.
  • Invito locale accettato: aggiorna l’elenco e comunica che il risultato è solo un’anteprima.

Sistema di design Headless UI Sistema di progettazione: gestire separatamente la selezione e la posizione da tastiera

Gestire separatamente la selezione e la posizione da tastiera

ListboxOption espone data-focus e data-selected per condizioni diverse. L’opzione con il focus riceve un tenue risalto cromatico, mentre l’autorizzazione selezionata è contrassegnata da un segno di spunta. Chi usa la tastiera può quindi passare tra le alternative senza perdere di vista la scelta esistente.

La stessa distinzione si applica fuori dal popup: il ruolo di errore appartiene al campo del destinatario non valido, mentre l’aspetto selezionato dello Switch indica l’accesso tramite link pubblico. Limita CSS allo stato del componente che descrive. Un selettore generico che colora allo stesso modo tutti gli elementi con il focus o selezionati renderebbe meno chiare queste diverse funzioni.

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

Riferimento del codice

Sistema di design Headless UI Sistema di progettazione: rendere leggibili l’identità del documento e le autorizzazioni

Impostazioni tipografiche riproducibili

Il titolo del documento è di 28px con un’altezza della riga pari a 1.25, mentre il titolo della finestra di dialogo è di 20px con un’altezza della riga pari a 1.3. Il testo del corpo è di 16px/1.5 e usa il font di sistema della piattaforma. Le etichette dei campi e il testo di supporto sono di 14px. Questi valori esatti sono riportati in style.css: non serve ricavarli da uno screenshot.

L’email nell’elenco degli accessi usa overflow-wrap:anywhere, così l’indirizzo non può allargare la colonna. L’autorizzazione è visualizzata su una riga separata. Nella finestra di dialogo, il titolo completo del documento e l’autorizzazione selezionata sono visibili nel momento in cui viene aggiunto l’invito.

Sistema di design Headless UI Sistema di progettazione: completare il posizionamento dell’overlay e la responsività di CSS

Il layout non è nascosto dietro un prerequisito

Il foglio di stile fornito include l’intero overlay: posizionamento con inset fisso, opacità dello sfondo, z-index, larghezza massima, scorrimento verticale e padding. Sotto i 700px, i pannelli del documento si dispongono in colonna e il padding della finestra di dialogo si riduce. Non è necessario alcun processo di compilazione Tailwind.

Tieni distinto il contenitore di posizionamento da DialogPanel. Il contenitore copre la porzione disponibile del viewport e fornisce lo spazio dai bordi; il pannello definisce la superficie del contenuto con larghezza limitata. Assegna ai contenuti più lunghi della finestra una regione scorrevole, così il campo del destinatario, la scelta dell’autorizzazione e le azioni per chiudere restano accessibili anche su schermi stretti.

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

Riferimento del codice

Sistema di design Headless UI Sistema di progettazione: usare il comportamento nativo per stato e focus

Verifica da tastiera

Apri la finestra di dialogo e verifica che il focus iniziale sia sul destinatario. Raggiungi il pulsante di attivazione delle autorizzazioni con Tab; usa i tasti freccia e Invio per scegliere un ruolo. Escape chiude il popup o la finestra di dialogo attivi in base al contesto di focus corrente del componente nativo. Dopo aver chiuso la finestra di dialogo, il focus dovrebbe tornare al pulsante di condivisione.

L’email usa aria-invalid e aria-describedby per l’errore personalizzato. Il segno di spunta in un’opzione è decorativo, perché Listbox fornisce le semantiche di selezione. Il controllo per il link pubblico ha un’etichetta visibile e la frase esplicativa rimane visibile dopo l’attivazione. L’opzione per la riduzione del movimento CSS rimuove la transizione decorativa del cursore dello Switch.

Leggi la specifica di supporto

Sistema di design Headless UI: usa in modo coerente un’unica generazione di React API

Usare con coerenza una sola generazione di React API

La voce completa React monta App, il cui foglio di stile definisce la geometria dell’overlay e i selettori di stato. Nell’esempio verificato 2.2.10, DialogPanel, DialogBackdrop, ListboxButton e ListboxOptions sono importazioni dirette. Le sintassi precedenti per i componenti composti vanno migrate con attenzione, anziché mescolate a questa composizione.

ListboxOptions usa anchor="bottom" e la variabile nativa --button-width per allineare le opzioni al pulsante di attivazione. Questi dettagli fanno parte dell’integrazione, non sono decorazioni facoltative: rimuovere il contenitore di posizionamento o sostituire la larghezza con un valore fisso non correlato può compromettere un popup annidato, anche se il campo chiuso sembra corretto.

  1. Installa @headlessui/react nel progetto React.
  2. Salva App.jsx e style.css dagli esempi, poi sostituisci main.jsx come mostrato.
  3. Avvia il server di sviluppo e verifica la chiusura della finestra di dialogo, la selezione delle autorizzazioni e il recupero dagli errori di email duplicata.

Sistema di design Headless UI Sistema di progettazione: esaminare un’interazione reale con le autorizzazioni

Criteri di accettazione

Una revisione utile parte da un documento e da un destinatario esistente, non da una galleria di componenti vuota. Aggiungi un invito valido con il ruolo Can comment, poi riapri la finestra e riprova con la stessa email. Verifica che la riga originale resti invariata e che il duplicato generi un errore specifico. Attiva il link pubblico e controlla la spiegazione indipendente.

L’esempio dimostra solo lo stato dell’interfaccia locale. Non include né la ricerca dei destinatari né un backend di condivisione. Per collegare un servizio reale occorre un modello di risposta con stati di attesa ed errore, oltre a una policy esplicita per le modifiche simultanee alle autorizzazioni; le primitive native non definiscono le regole di autorizzazione.

Sistema di design Headless UI Esempi di componenti del sistema di progettazione

App.jsx — composizione nativa per la condivisione

Dialog controllato completo, Listbox delle autorizzazioni, Switch con etichetta e convalida 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>;
}

Riferimento del codice

  • Non viene inviata alcuna email né effettuata alcuna richiesta.
  • Escape e Annulla eliminano l’invito non salvato.

style.css — sistema visivo originale completo

Tutti i token visivi, i selettori di stato, il posizionamento dell’overlay e le regole responsive richiesti da App.jsx. Sono scelte esemplificative, non impostazioni predefinite ufficiali di 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}}

Riferimento del codice

  • Mantieni distinti data-focus e data-selected.
  • Tailwind non è un prerequisito.
04

Sistema di design Headless UI Configurazione dell’agente

Punto di partenza per l’implementazione

I file completi dei componenti e degli stili sono forniti sopra. È richiesta la versione di Node supportata dalla versione di Vite selezionata. La demo include localmente lo stesso pacchetto di componenti nativi.

testo
# 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 per il tuo progetto

Prompt per l’agente
Usa App.jsx e style.css di Headless UI forniti. Mantieni la composizione nativa di Dialog, Listbox e Switch, separa le autorizzazioni in bozza dagli accessi confermati e conserva tutti i selettori per il focus e lo stato selezionato. Sostituisci l’aggiunta locale dell’invito con il nostro API autorizzato, includendo gli stati di attesa e di errore; non salvare mai in onClose. Non descrivere la palette personalizzata di CSS come tema ufficiale di Headless UI.
05

Sistema di design Headless UI Fonti e ambito del sistema di progettazione

Configurazione visiva originale di OpenDesign ed esempio di React che usa il comportamento nativo di Headless UI 2.2.10. I valori di CSS sono esplicitamente scelte dell’applicazione; Headless UI non fornisce questo tema. Gli inviti e le modifiche alle autorizzazioni esistono solo nella memoria locale dell’anteprima.