# Sistema de diseño Headless UI

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

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

| Nombre / Regla | Valor / Configuración | Uso |
| --- | --- | --- |
| --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

| Nombre / Regla | Valor / Configuración | Uso |
| --- | --- | --- |
| --space-1 / --space-2 | 4px / 8px | Incrementos pequeños. |
| --space-3 / --space-4 | 12px / 16px | Separación entre controles y margen interior en móviles. |
| --space-6 / --space-8 | 24px / 32px | Relleno de paneles y separación amplia. |
| --radius-control | 8px | Botones y campos de entrada. |
| --radius-panel | 16px | Papel y diálogo. |
| .dialog-panel width | min(100%, 460px) | Capa superpuesta delimitada en pantallas estrechas. |
| .dialog-panel max-height | calc(100% - 16px) | Deja espacio dentro del contenedor de posicionamiento fijo. |
### Tipografía y opciones de diseño adaptativo originales

| Nombre / Regla | Valor / Configuración | Uso |
| --- | --- | --- |
| Body | 16px / 1.5 system-ui | No se requiere ninguna fuente externa. |
| Screen title | 28px / 1.25 | Identidad del documento. |
| Document headline | 32px; 26px below 700px | Jerarquía del contenido editorial. |
| Dialog heading | 20px / 1.3 | Indica la tarea de compartir. |
| Labels and helper copy | 14px | Contexto del campo. |
| Error and access description | 13px | Mensajes persistentes. |
| Breakpoint | 700px | Los paneles del documento y de acceso se apilan. |
### Contratos de interacción nativos

| Nombre / Regla | Valor / Configuración | Uso |
| --- | --- | --- |
| Dialog open / onClose | Controlled boolean / dismissal callback | Escape y la interacción fuera del diálogo solicitan cerrarlo; no guardan los cambios. |
| DialogPanel | Content boundary | El fondo superpuesto y el contenedor de posicionamiento quedan fuera de este límite. |
| Listbox value / onChange | Controlled permission string | La selección con el teclado actualiza el permiso seleccionado. |
| data-focus / data-selected | Native state attributes | CSS diferencia el foco del teclado de la selección confirmada. |
| Switch checked / onChange | Controlled boolean | data-checked aplica estilos a su estado visual. |
## 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.

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

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

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.

```
: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.

```
.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.

```
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:'alex@example.test',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='jules@example.test'/>{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.

```
: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.

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

```
# Terminal
npm create vite@latest sharing-demo -- --template react
cd sharing-demo
npm install
npm install @headlessui/react@2.2.10

# 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

```
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.
```

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

- Dialog de API Referencia oficial de interacción, composición y estados.

- Listbox de API Referencia oficial de interacción, composición y estados.

- Switch de API Referencia oficial de interacción, composición y estados.

## Headless UI Recursos relacionados con el sistema de diseño

### HeroUI

Una referencia de HeroUI v3 con variables CSS semánticas reales, controles compuestos de React, superficies claras y oscuras, y una receta funcional para exportar imágenes.

### Ionic Framework

Una interfaz nativa de Ionic para las preferencias de lectura, con roles de color verificados, geometría de controles deslizantes para iOS y Material, comportamiento real de segmentos y conmutadores, y un artículo que responde a las preferencias de fuente, tamaño y papel.

### Kobalte

Kobalte proporciona componentes sin interfaz para Solid, incluidos campos numéricos con valores sin formato y formateados por separado, la anatomía compuesta de casillas de verificación y botones con estado de pulsación. Esta guía conecta esas primitivas con las señales de Solid mediante una herramienta para ajustar recetas, con cantidades derivadas y controles de ingredientes que se pueden restablecer.


- [Referencia de implementación](https://headlessui.com/react/dialog)
- [Fuente](https://headlessui.com/react/dialog)
- [Fuente](https://headlessui.com/react/listbox)
- [Consulta la especificación complementaria](https://headlessui.com/react/dialog)
- [Consulta la especificación complementaria](https://headlessui.com/react/listbox)
- [Referencia de código](https://headlessui.com/react/listbox)
- [Referencia de código](https://headlessui.com/react/dialog)
- [Consulta la especificación complementaria](https://headlessui.com/react/switch)
- [Dialog de API Referencia oficial de interacción, composición y estados.](https://headlessui.com/react/dialog)
- [Listbox de API Referencia oficial de interacción, composición y estados.](https://headlessui.com/react/listbox)
- [Switch de API Referencia oficial de interacción, composición y estados.](https://headlessui.com/react/switch)
