Menú
En esta página
Un menú es un <details> con un panel estilizado. Es una elección deliberada frente a la API de popover: un popover vive en la capa superior y no se puede colocar respecto de su disparador sin posicionamiento por anclaje, que todavía no está en todas partes. Un <details> se coloca bien hoy y no necesita que se cargue nada.
El disparador es ese <summary> con estilo de botón, así que le pasas la etiqueta y las props de botón a menu() en vez de pasarle un button() ya dibujado. Un summary ya es interactivo, y un botón dentro anida dos controles donde solo hay una acción: marcado inválido, y dos paradas de tabulación para una sola cosa.
Cerrar al hacer clic fuera y con Escape viene de un manejador ontoggle que los importa la primera vez que se abre un menú, y solo entonces. Si ese módulo no llega nunca, un menú sigue abriéndose y cerrándose desde su propio summary.
Menú básico
menu({ trigger: 'Acciones' },
menuItem({ href: '#edit' }, 'Editar'),
menuItem({ href: '#duplicate' }, 'Duplicar'),
menuSeparator(),
menuItem({ href: '#delete' }, 'Eliminar'),
)Alineación
Un menú se abre desde el borde inicial de su disparador. align: 'end' lo voltea, que es lo que necesita un menú cerca del borde derecho de una barra.
stack({ direction: 'row', gap: 'xl', justify: 'space-between', style: 'width: 100%' },
menu({ trigger: 'Alineado al inicio', variant: 'soft' },
menuItem({ href: '#a' }, 'Primero'),
menuItem({ href: '#b' }, 'Segundo'),
),
menu({ trigger: 'Alineado al final', variant: 'soft', align: 'end' },
menuItem({ href: '#c' }, 'Primero'),
menuItem({ href: '#d' }, 'Segundo'),
),
)Disparadores de icono
Un icono sin texto de trigger necesita un label: pasa a ser el nombre accesible que el icono no puede dar.
stack({ direction: 'row', gap: 'md' },
menu({
align: 'end',
label: 'Más acciones',
variant: 'ghost',
icon: icon('more-horizontal'),
},
menuItem({ href: '#rename' }, 'Renombrar'),
menuItem({ href: '#move' }, 'Mover'),
menuSeparator(),
menuItem({ href: '#archive' }, 'Archivar'),
),
)Elementos con iconos
menu({ trigger: 'Archivo' },
menuItem({
href: '#new',
icon: icon('plus'),
}, 'Página nueva'),
menuItem({
href: '#open',
icon: icon('folder'),
}, 'Abrir…'),
menuSeparator(),
menuItem({
href: '#build',
icon: icon('zap'),
}, 'Compilar el sitio'),
)Botones en vez de enlaces
Un elemento sin href dibuja un <button>, para una acción que ocurre en la página en vez de una navegación.
menu({ trigger: 'Exportar', variant: 'soft', color: 'primary' },
menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Exportado como JSON.',{color:'success'}))" }, 'Como JSON'),
menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Exportado como CSV.',{color:'success'}))" }, 'Como CSV'),
)En una barra de aplicación
appBar({ brand: 'sitelo' },
appBarSpacer(),
appBarActions(
themeToggle(),
menu({
align: 'end',
label: 'Más',
variant: 'ghost',
icon: icon('more-horizontal'),
},
menuItem({ href: '/es/docs' }, 'Documentación'),
menuItem({ href: '/es/examples' }, 'Ejemplos'),
menuSeparator(),
menuItem({ href: 'https://github.com/paul-browne/sitelo' }, 'GitHub'),
),
),
)Accesibilidad
El panel es un role="menu" cuyos elementos son role="menuitem", y el summary lleva aria-haspopup. Un <details> no es un widget de menú nativo, así que esto es una aproximación razonable y no una perfecta: para una lista simple de enlaces, un nav dentro del details es igual de válido y promete menos.
Props
menu() — las props del disparador son las del botón:
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
trigger | Child | — | Etiqueta visible. Pasa texto, no un button() ya dibujado. |
icon | Child | — | Marcado antes de la etiqueta, o por su cuenta para un disparador de solo icono. |
label | string | — | Nombre accesible. Obligatorio cuando hay icono y no hay texto de trigger. |
variant | 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'outline' | Estilo del disparador. |
color | 'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'neutral' | De qué paleta bebe el disparador. |
size | 'sm' | 'md' | 'lg' | 'md' | Tamaño del disparador. |
align | 'start' | 'end' | 'start' | Con qué borde del disparador se alinea el panel. |
triggerClass | string | — | Clases extra para el disparador, no para el details que lo envuelve. |
menuItem():
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
href | string | — | Dibuja un ancla; sin él, un botón. |
icon | Child | — | Marcado antes de la etiqueta. |
as | string | 'button' | Elemento que se renderiza cuando no hay href. |
menuSeparator() no admite props: es la línea fina entre grupos de elementos.