Menú

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.

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.

Alineado al inicio
Alineado al final
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.

Exportar
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:

PropTipoPor defectoDescripción
triggerChild—Etiqueta visible. Pasa texto, no un button() ya dibujado.
iconChild—Marcado antes de la etiqueta, o por su cuenta para un disparador de solo icono.
labelstring—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.
triggerClassstring—Clases extra para el disparador, no para el details que lo envuelve.

menuItem():

PropTipoPor defectoDescripción
hrefstring—Dibuja un ancla; sin él, un botón.
iconChild—Marcado antes de la etiqueta.
asstring'button'Elemento que se renderiza cuando no hay href.

menuSeparator() no admite props: es la línea fina entre grupos de elementos.