Headless UI fornece componentes de interação sem estilos para React, como diálogos, listas de opções e interruptores. Este guia mostra como combinar o comportamento nativo de foco, seleção e pop-up desses componentes com CSS comum, usando como exemplo um fluxo de compartilhamento de documentos.
Guia OpenDesign9 capítulos27 entradas de referênciaGuia de aplicação original
01
Prévia da interface do sistema de design Headless UI
Abra a caixa de diálogo de compartilhamento, valide um e-mail, escolha uma permissão pelo teclado, adicione um convite local e alterne o estado do link público.
@headlessui/react 2.2.10 nativo com React 19. Todas as cores, tipografia, espaçamentos e layout são tokens de design originais deste exemplo CSS, não tokens oficiais de Headless UI. · Referência de implementação
02
Referência de design do sistema de design Headless UI
Headless UI não tem uma escala oficial de tokens visuais. Cada valor numérico ou de cor abaixo pertence a este exemplo de OpenDesign; a documentação vinculada descreve o comportamento e as APIs de estado. Pacote testado: @headlessui/react 2.2.10.
Sistema de design Headless UI: componha o comportamento antes de estilizar a tela
Componha o comportamento antes de estilizar a tela
Dialog, Listbox e Switch representam interações diferentes. Dialog controla uma tarefa delimitada e seu contexto de foco; Listbox representa uma única opção confirmada; Switch representa uma configuração booleana independente. Combiná-los não exige combinar seus estados: o rascunho do convite, a permissão selecionada e a configuração do link público permanecem separados.
O exemplo usa o componente React API com importações diretas, como DialogPanel e ListboxButton. A caixa de diálogo contém uma caixa de listagem, então o posicionamento do menu e o fechamento precisam funcionar dentro de uma sobreposição existente. Comece por essa composição antes de adicionar ao redor dela o painel do documento e o resumo de acesso. O CSS fornecido apresenta uma aparência completa para essas partes nativas.
Sistema de design Headless UI: estrutura de Dialog, Listbox e Switch
Defina limites intencionais para a sobreposição
DialogBackdrop é irmão do contêiner de posicionamento fixo. DialogPanel é o painel branco delimitado dentro desse contêiner; ele não ocupa toda a janela de visualização. DialogTitle identifica a tarefa, e Description explica o que acontece ao fechar. O campo de entrada nativo tem data-autofocus, e Headless UI gerencia o comportamento de foco da caixa de diálogo ao redor.
A caixa de listagem seleciona um valor; o interruptor altera um valor booleano
ListboxButton mostra a função selecionada. ListboxOptions usa anchor="bottom" e uma largura baseada em --button-width, para que o menu fique alinhado ao botão que o aciona. Cada ListboxOption tem um valor de texto estável. Os atributos nativos data-focus e data-selected controlam dois estilos CSS distintos.
O Switch do link público fica em Field com Label, em vez de ser um botão decorativo sem rótulo. Seu estado marcado altera o controle acessível e a frase explicativa visível. O convite local é independente da configuração do link público.
Sistema de design Headless UI: separe seleção, fechamento e confirmação local
O modelo de estado usado aqui
Ao abrir, um novo rascunho de destinatário é iniciado com a função Can view. Cancelar, pressionar Escape ou clicar fora fecha a caixa de diálogo; nenhuma dessas ações adiciona um destinatário. Ao enviar, o e-mail é validado e, se o endereço já estiver presente, é rejeitado. Em seguida, o convite é adicionado à memória e a caixa de diálogo é fechada. A confirmação informa explicitamente que nenhum e-mail foi enviado.
A caixa de listagem de permissões altera a função do rascunho, não o acesso de um destinatário existente. O estado do link público altera um valor booleano separado. Assim, evita-se o erro comum de tratar cada evento de fechamento como persistência de permissão.
Entrada inválida: mantenha a caixa de diálogo aberta e associe o erro ao e-mail.
Destinatário duplicado: mantenha inalterada a lista de acessos existente.
Convite local aceito: atualize a lista e anuncie o resultado, disponível apenas na prévia.
Sistema de design Headless UI: estilize a seleção e o foco do teclado de forma independente
Estilize a seleção e o foco do teclado de forma independente
ListboxOption expõe data-focus e data-selected para condições diferentes. A opção em foco recebe o tratamento de destaque claro, enquanto a permissão confirmada recebe uma marca de seleção. Assim, quem usa o teclado pode percorrer as alternativas sem perder de vista a opção atual.
A mesma distinção se aplica fora do menu: o papel de erro pertence ao campo de destinatário inválido, enquanto a aparência marcada do Switch corresponde ao acesso pelo link público. Restrinja CSS ao estado do componente que ele descreve. Um seletor amplo que aplicasse a mesma cor a todos os elementos em foco ou marcados ocultaria esses significados diferentes.
Sistema de design Headless UI: torne a identidade do documento e as permissões fáceis de entender
Configurações tipográficas reproduzíveis
O título do documento usa 28px com altura de linha de 1.25, enquanto o título da caixa de diálogo usa 20px com altura de linha de 1.3. O texto do corpo usa 16px/1.5 na fonte de sistema da plataforma. Os rótulos dos campos e o texto auxiliar usam 14px. Esses valores exatos estão em style.css; não é necessário inferi-los de uma captura de tela.
O endereço de e-mail na lista de acessos usa overflow-wrap:anywhere para impedir que ele force a coluna a ficar mais larga. A permissão permanece em uma linha separada. Dentro da caixa de diálogo, o título completo do documento e a permissão selecionada ficam visíveis no momento em que o convite é adicionado.
Sistema de design Headless UI: posicionamento completo da sobreposição e CSS responsivo
O layout não fica oculto em um pré-requisito
A folha de estilos fornecida inclui toda a sobreposição: posicionamento fixo com inset, opacidade do fundo, z-index, largura máxima, rolagem vertical e espaçamento interno. Abaixo de 700px, os painéis do documento ficam empilhados e o diálogo reduz o espaçamento interno. Não se pressupõe uma compilação do Tailwind.
Mantenha o contêiner de posicionamento separado de DialogPanel. O contêiner ocupa a área disponível da janela de visualização e fornece espaçamento nas bordas; o painel fornece uma superfície de conteúdo com limites definidos. Para diálogos com conteúdo mais longo, crie uma área rolável para que o campo de destinatário, a escolha de permissão e as ações de fechar continuem acessíveis em larguras estreitas.
Use o estado e o comportamento de foco nativos do sistema de design Headless UI
Verificação pelo teclado
Abra o diálogo e confirme que o foco inicial está no destinatário. Pressione Tab para chegar ao acionador de permissão; use as teclas de seta e Enter para escolher uma função. Escape fecha o popup ou o diálogo ativo, de acordo com o contexto de foco atual do componente nativo. Depois de fechar o diálogo, o foco deve retornar ao acionador de compartilhamento.
O e-mail usa aria-invalid e aria-describedby para exibir um erro personalizado. A marca de seleção em uma opção é decorativa, pois o Listbox fornece a semântica de seleção. O controle de link público tem um rótulo visível, e a frase explicativa permanece após a alternância. A configuração de movimento reduzido CSS remove a transição decorativa do botão deslizante.
Use uma única geração de React API de forma consistente no sistema de design Headless UI
Use uma única geração de React API de forma consistente
A entrada completa React monta App, cuja folha de estilos fornece a geometria da sobreposição e os seletores de estado. DialogPanel, DialogBackdrop, ListboxButton e ListboxOptions são importações diretas no exemplo verificado da versão 2.2.10. As grafias antigas de componentes compostos devem ser migradas deliberadamente, em vez de misturadas a esta composição.
ListboxOptions usa anchor="bottom" e a variável nativa --button-width para alinhar as opções ao acionador. Esses detalhes fazem parte da integração, não são enfeites opcionais: remover o contêiner de posicionamento ou substituir a largura por um valor fixo sem relação com o acionador pode comprometer um popup aninhado, mesmo que o campo fechado ainda pareça correto.
Instale @headlessui/react no projeto React.
Salve App.jsx e style.css a partir dos exemplos e, em seguida, substitua main.jsx conforme mostrado.
Inicie o servidor de desenvolvimento e teste o fechamento do diálogo, a seleção de permissões e a recuperação após e-mails duplicados.
Inspecione uma interação real de permissão no sistema de design Headless UI
Critérios de aceitação
Uma revisão útil começa com um documento e um destinatário existente, não com uma galeria de componentes vazia. Adicione um convite válido com a permissão Pode comentar e, em seguida, reabra o diálogo e tente usar o mesmo e-mail. Confirme que a linha original é mantida e que a duplicata gera um erro específico. Ative o link público e confira a explicação correspondente.
O exemplo demonstra apenas o estado local da interface. Ele não oferece busca de destinatários nem back-end de compartilhamento. Conectar um serviço real exige um modelo de respostas para estados pendentes e de erro, além de uma política explícita para alterações simultâneas de permissões; os elementos primitivos nativos não definem regras de autorização.
Exemplos de componentes do sistema de design Headless UI
App.jsx — composição de compartilhamento nativa
Dialog controlado completo, Listbox de permissões, Switch com rótulo e validação local.
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>;
}
Nenhum e-mail é enviado, nem qualquer solicitação é feita.
Escape e Cancelar descartam o convite não salvo.
style.css — sistema visual original completo
Todos os tokens visuais, seletores de estado, posicionamento da sobreposição e regras responsivas exigidos por App.jsx. Estas são escolhas do exemplo, não configurações padrão oficiais de Headless UI.
Configuração do agente do sistema de design Headless UI
Ponto de partida para implementação
Os arquivos completos de estilos e componentes estão acima. Requer a versão do Node compatível com a versão selecionada do Vite. A demonstração inclui o mesmo pacote de componentes nativos localmente.
texto
# 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
Um prompt para seu projeto
Prompt para agente de IA
Use App.jsx e style.css de Headless UI fornecidos. Preserve a composição nativa de Dialog, Listbox e Switch, mantenha as permissões provisórias separadas do acesso confirmado e preserve todos os seletores de foco e de estado selecionado. Substitua a inclusão local de convites pela nossa API autorizada, incluindo estados pendentes e de falha; nunca salve em onClose. Não descreva a paleta personalizada de CSS como um tema oficial de Headless UI.
05
Fontes e escopo do sistema de design Headless UI
Configuração visual original de OpenDesign e exemplo de React usando o comportamento nativo de Headless UI 2.2.10. Os valores de CSS são explicitamente escolhas do aplicativo; Headless UI não oferece este tema. Os convites e as alterações de permissões existem apenas na memória da prévia local.