Sistema de componentes sin estilos · Nuevo recurso

Sistema de diseño Headless UI.

Headless UI proporciona componentes de interacción React sin estilos para diálogos, listas desplegables y conmutadores. Esta guía se centra en combinar su comportamiento nativo de enfoque, selección y ventanas emergentes con CSS convencional, usando como ejemplo práctico un flujo para compartir documentos.

Guía de OpenDesign9 capítulos27 entradas de referenciaGuía de aplicación original
01

Vista previa de la interfaz del sistema de diseño Headless UI

Abre el diálogo para compartir, valida un correo electrónico, elige un permiso con el teclado, añade una invitación local y cambia el estado del enlace público.

Compartir documentos con primitivas headless nativasAbrir ejemplo completo
@headlessui/react 2.2.10 nativo con React 19. Todos los colores, la tipografía, el espaciado y la composición son CSS originales de ejemplo, no tokens oficiales de Headless UI. · Referencia de implementación
02

Referencia de diseño del sistema de diseño Headless UI

Headless UI no tiene una escala oficial de tokens visuales. Todos los valores numéricos o de color que aparecen a continuación pertenecen a este ejemplo de OpenDesign; la documentación enlazada proporciona las API de comportamiento y estado. Paquete probado: @headlessui/react 2.2.10.

Roles de color originales de CSS

Fuente
Nombre / ReglaValor / ConfiguraciónUso
--canvas#f5f5f4 Espacio de trabajo de documentos.
--surface#ffffff Papel, panel de acceso y diálogo.
--ink#1c1917 Texto principal.
--muted#57534e Texto secundario.
--line#d6d3d1 Bordes de paneles y campos de entrada.
--accent#4338ca Acción principal y contorno de foco.
--accent-soft#eef2ff Opción enfocada y fondo del avatar.
--error#b91c1c Texto y borde de correo electrónico no válido.

Geometría y espaciado originales

Fuente
Nombre / ReglaValor / ConfiguraciónUso
--space-1 / --space-24px / 8pxIncrementos pequeños.
--space-3 / --space-412px / 16pxSeparación entre controles y margen interior en móviles.
--space-6 / --space-824px / 32pxRelleno de paneles y separación amplia.
--radius-control8pxBotones y campos de entrada.
--radius-panel16pxPapel y diálogo.
.dialog-panel widthmin(100%, 460px)Capa superpuesta delimitada en pantallas estrechas.
.dialog-panel max-heightcalc(100% - 16px)Deja espacio dentro del contenedor de posicionamiento fijo.

Tipografía y opciones de diseño adaptativo originales

Fuente
Nombre / ReglaValor / ConfiguraciónUso
Body16px / 1.5 system-uiNo se requiere ninguna fuente externa.
Screen title28px / 1.25Identidad del documento.
Document headline32px; 26px below 700pxJerarquía del contenido editorial.
Dialog heading20px / 1.3Indica la tarea de compartir.
Labels and helper copy14pxContexto del campo.
Error and access description13pxMensajes persistentes.
Breakpoint700pxLos paneles del documento y de acceso se apilan.

Contratos de interacción nativos

Fuente
Nombre / ReglaValor / ConfiguraciónUso
Dialog open / onCloseControlled boolean / dismissal callbackEscape y la interacción fuera del diálogo solicitan cerrarlo; no guardan los cambios.
DialogPanelContent boundaryEl fondo superpuesto y el contenedor de posicionamiento quedan fuera de este límite.
Listbox value / onChangeControlled permission stringLa selección con el teclado actualiza el permiso seleccionado.
data-focus / data-selectedNative state attributesCSS diferencia el foco del teclado de la selección confirmada.
Switch checked / onChangeControlled booleandata-checked aplica estilos a su estado visual.
03

Guía completa del sistema de diseño Headless UI

Una guía práctica para elegir, aplicar y revisar este sistema en una interfaz real.

DESIGN-headless-ui.md

Orientación completa, un punto de partida para la implementación y enlaces al código fuente.

Descargar la guía

Sistema de diseño Headless UI: compón el comportamiento antes de dar estilo a la pantalla

Compón el comportamiento antes de dar estilo a la pantalla

Dialog, Listbox y Switch modelan interacciones distintas. Dialog controla una tarea delimitada y su contexto de foco; Listbox representa una elección confirmada; Switch representa un ajuste booleano independiente. Combinarlos no exige unificar su estado: el borrador de la invitación, el permiso seleccionado y el ajuste del enlace público siguen siendo independientes.

De esta guía de OpenDesign

El ejemplo usa el componente React API mediante importaciones directas, como DialogPanel y ListboxButton. El diálogo contiene una lista de opciones, por lo que el posicionamiento de la ventana emergente y su cierre deben funcionar dentro de una superposición existente. Empieza con esa composición antes de añadir el panel del documento y el resumen de acceso a su alrededor. El CSS proporcionado muestra una apariencia completa para estas partes nativas.

Anatomía de Dialog, Listbox y Switch del sistema de diseño Headless UI

Delimita la superposición intencionadamente

DialogBackdrop es un elemento hermano del contenedor de posicionamiento fijo. DialogPanel es el panel blanco delimitado dentro de ese contenedor; no ocupa toda la ventana. DialogTitle nombra la tarea y Description explica qué ocurre al cerrar. El campo de entrada nativo tiene data-autofocus y Headless UI gestiona el comportamiento del foco en el diálogo.

Consulta la especificación complementaria

Un cuadro de lista selecciona un valor; un interruptor cambia un valor booleano

ListboxButton muestra el rol seleccionado. ListboxOptions usa anchor="bottom" y un ancho basado en --button-width, para que la ventana emergente se alinee con el activador. Cada ListboxOption tiene un valor de cadena estable. Los atributos nativos data-focus y data-selected aplican dos tratamientos CSS distintos.

El Switch del enlace público está dentro de Field con Label, en lugar de presentarse como una pastilla visual sin etiqueta. Su estado activado cambia el control accesible y la frase explicativa visible. La invitación local es independiente del ajuste del enlace público.

Consulta la especificación complementaria

Sistema de diseño Headless UI: separa la selección, el cierre y la confirmación local

El modelo de estado utilizado aquí

Al abrirlo, se inicializa un nuevo borrador del destinatario y se selecciona el rol Puede ver. Cancelar, pulsar Escape y hacer clic fuera cierran el diálogo; ninguna de estas acciones añade un destinatario. Al enviar, se valida el correo electrónico y se rechaza si la dirección ya existe. Después, se añade una invitación a la memoria y se cierra el diálogo. La confirmación indica explícitamente que no se ha enviado ningún correo.

El cuadro de lista de permisos cambia el rol del borrador, no el acceso de los destinatarios existentes. El estado del enlace público modifica un booleano independiente. Así se evita el error habitual de tratar cada evento de cierre como si guardara los permisos.

  • Entrada no válida: mantén abierto el diálogo y asocia el error al correo electrónico.
  • Destinatario duplicado: no modifiques la lista de accesos existente.
  • Invitación local aceptada: actualiza la lista y anuncia que el resultado es solo una vista previa.

Sistema de diseño Headless UI: aplica estilos por separado a la selección y a la posición del foco del teclado

Aplica estilos por separado a la selección y a la posición del foco del teclado

ListboxOption expone data-focus y data-selected para condiciones distintas. La opción enfocada recibe un tratamiento de acento pálido, mientras que el permiso confirmado se indica con una marca de verificación. Así, quienes usan el teclado pueden recorrer las alternativas sin perder de vista la opción elegida.

La misma distinción se aplica fuera de la ventana emergente: el rol del error corresponde al campo del destinatario no válido, mientras que el aspecto activado de Switch corresponde al acceso mediante enlace público. Limita CSS al estado del componente que describe. Un selector general que aplicara el mismo color a todos los elementos enfocados o activados ocultaría estos significados distintos.

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

Referencia de código

Sistema de diseño Headless UI: facilita la lectura de la identidad del documento y los permisos

Ajustes tipográficos reproducibles

El título del documento mide 28px y tiene una altura de línea de 1.25; el título del diálogo mide 20px y tiene una altura de línea de 1.3. El texto del cuerpo usa 16px/1.5 y la fuente del sistema de la plataforma. Las etiquetas de campo y el texto de ayuda miden 14px. Estos valores exactos están en style.css; no hace falta deducirlos a partir de una captura de pantalla.

El correo electrónico de la lista de accesos usa overflow-wrap:anywhere para evitar que una dirección ensanche su columna. El permiso aparece en una línea aparte. Dentro del diálogo, el título completo del documento y el permiso seleccionado se ven cuando se añade la invitación.

Posicionamiento completo de la superposición y diseño adaptativo CSS del sistema de diseño Headless UI

La composición no está oculta en un requisito previo

La hoja de estilos proporcionada incluye toda la superposición: posicionamiento fijo con inset, opacidad del fondo, z-index, ancho máximo, desplazamiento vertical y relleno. Por debajo de 700px, los paneles del documento se apilan y el diálogo reduce su relleno. No se presupone una compilación de Tailwind.

Mantén el contenedor de posicionamiento separado de DialogPanel. El contenedor cubre la ventana gráfica disponible y proporciona espacio en los bordes; el panel proporciona la superficie de contenido acotada. Si el contenido del diálogo es extenso, añade una región desplazable para que el campo del destinatario, la opción de permiso y las acciones para cerrarlo sigan siendo accesibles en pantallas estrechas.

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

Referencia de código

Headless UI Usa el comportamiento nativo de estado y foco del sistema de diseño

Verificación con el teclado

Abre el diálogo y verifica que el foco inicial esté en el destinatario. Usa Tab para llegar al activador de permisos; usa las flechas y Enter para elegir un rol. Escape cierra la ventana emergente o el diálogo activo según el contexto de foco actual del componente nativo. Al cerrar el diálogo, el foco debe volver al activador para compartir.

El correo electrónico usa aria-invalid y aria-describedby para su error personalizado. La marca de verificación de una opción es decorativa, ya que Listbox proporciona la semántica de selección. El control de enlace público tiene una etiqueta visible y la oración explicativa permanece después de activarlo o desactivarlo. El ajuste de movimiento reducido CSS elimina la transición decorativa del pulgar del interruptor.

Consulta la especificación complementaria

Headless UI Usa de forma coherente una sola generación del sistema de diseño React API

Usa de forma coherente una sola generación de React API

La entrada completa de React monta App, cuya hoja de estilos proporciona la geometría de la superposición y los selectores de estado. DialogPanel, DialogBackdrop, ListboxButton y ListboxOptions se importan directamente en el ejemplo verificado de la versión 2.2.10. Las formas compuestas más antiguas deben migrarse de forma deliberada, en lugar de mezclarse con esta composición.

ListboxOptions usa anchor="bottom" y la variable nativa --button-width para alinear las opciones con el activador. Estos detalles forman parte de la integración, no son adornos opcionales: eliminar el contenedor de posicionamiento o sustituir el ancho por un valor fijo sin relación puede romper una ventana emergente anidada, aunque el campo cerrado siga teniendo el aspecto correcto.

  1. Instala @headlessui/react en el proyecto React.
  2. Guarda App.jsx y style.css de los ejemplos y luego sustituye main.jsx como se muestra.
  3. Inicia el servidor de desarrollo y prueba el cierre del diálogo, la selección de permisos y la recuperación ante correos electrónicos duplicados.

Headless UI Inspecciona una interacción de permisos real del sistema de diseño

Criterios de aceptación

Una revisión útil parte de un documento y de un destinatario existente, no de una galería de componentes vacía. Añade una invitación válida con permiso Puede comentar, vuelve a abrirla e intenta usar el mismo correo electrónico. Confirma que se conserva la fila original y que el duplicado genera un error específico. Activa el enlace público y revisa su explicación independiente.

El ejemplo solo demuestra el estado local de la interfaz. No incluye búsqueda de destinatarios ni un backend para compartir. Conectar un servicio real requiere un modelo de respuestas pendientes y de error, así como una política explícita para los cambios simultáneos de permisos; las primitivas nativas no determinan las reglas de autorización.

Headless UI Ejemplos de componentes del sistema de diseño

App.jsx — composición nativa para compartir

Diálogo controlado completo, Listbox de permisos, Switch con etiqueta y validación 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>;
}

Referencia de código

  • No se envía ningún correo electrónico ni solicitud.
  • Escape y Cancelar descartan la invitación sin guardar.

style.css — sistema visual original completo

Todos los tokens visuales, selectores de estado, ubicación de la superposición y reglas adaptables necesarios para App.jsx. Son opciones del ejemplo, no valores predeterminados oficiales 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}}

Referencia de código

  • Mantén diferenciados data-focus y data-selected.
  • No se requiere Tailwind.
04

Headless UI Configuración del agente del sistema de diseño

Punto de partida para la implementación

Los archivos completos de estilos y componentes se proporcionan arriba. Se requiere una versión de Node compatible con la versión de Vite seleccionada. La demostración incluye localmente el mismo paquete de componentes nativos.

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

Una instrucción para tu proyecto

Instrucción para el agente
Usa App.jsx y style.css de Headless UI proporcionados. Conserva la composición nativa de Dialog, Listbox y Switch, separa los permisos en borrador del acceso confirmado y mantén todos los selectores de foco y estado seleccionado. Sustituye la incorporación local de invitaciones por nuestra API autorizada, incluidos los estados pendiente y fallido; nunca guardes al ejecutar onClose. No presentes la paleta personalizada de CSS como un tema oficial de Headless UI.
05

Headless UI Fuentes y alcance del sistema de diseño

Configuración visual original de OpenDesign y ejemplo de React con el comportamiento nativo de Headless UI 2.2.10. Los valores de CSS son explícitamente opciones de la aplicación; Headless UI no proporciona este tema. Las invitaciones y los cambios de permisos solo existen en la memoria de la vista previa local.