Ir al contenido

Componentes · Ola 2 · Superposiciones

Menu

Menú de acciones que se abre desde un botón, con atajos, grupos y submenús. Se maneja con el teclado como un menú del sistema: flechas, Home, End, búsqueda por letra y Escape.

import { Menu, MenuItem, MenuSeparator, MenuGroup, MenuSub } from '@archeblack/ui';

Ejemplos

Acciones

Ítems con ícono y atajo, un separador y una acción danger al final, como en el demo.

Todavía no elegiste nada.

Svelte
<script lang="ts">
	import { Button, Menu, MenuItem, MenuSeparator } from '@archeblack/ui';
	import { IconCopy, IconDots, IconEdit, IconRocket, IconTrash } from '@archeblack/ui/icons';

	let last = $state('');
</script>

<!--
	El disparador es un botón de solo ícono con aria-label, que también nombra al menú. onSelect se
	llama con el clic, Enter o espacio; después el menú se cierra y el foco vuelve al botón.
-->
<div style:display="grid" style:justify-items="center" style:gap="var(--arche-spacing-3)">
	<Menu>
		{#snippet trigger(props)}
			<Button {...props} variant="ghost" icon={IconDots} aria-label="Acciones del proyecto" />
		{/snippet}
		<MenuItem icon={IconEdit} shortcut="R" onSelect={() => (last = 'Renombrar')}>Renombrar</MenuItem
		>
		<MenuItem icon={IconCopy} shortcut="⌘D" onSelect={() => (last = 'Duplicar')}>Duplicar</MenuItem>
		<MenuItem icon={IconRocket} onSelect={() => (last = 'Desplegar ahora')}
			>Desplegar ahora</MenuItem
		>
		<MenuSeparator />
		<MenuItem icon={IconTrash} variant="danger" onSelect={() => (last = 'Eliminar')}>
			Eliminar
		</MenuItem>
	</Menu>
	<p role="status" style:margin="0" style:color="var(--arche-color-text-muted)">
		{last ? `Elegiste «${last}».` : 'Todavía no elegiste nada.'}
	</p>
</div>

Grupos y deshabilitados

MenuGroup suma una etiqueta que nombra al grupo. Un ítem disabled se ve atenuado y las flechas lo saltan.

Svelte
<script lang="ts">
	import { Button, Menu, MenuGroup, MenuItem, MenuSeparator } from '@archeblack/ui';
	import {
		IconArchive,
		IconChevronDown,
		IconDownload,
		IconLink,
		IconMail,
		IconUserPlus
	} from '@archeblack/ui/icons';
</script>

<!-- Cada grupo se nombra con su etiqueta; un ítem deshabilitado se ve y las flechas lo saltan. -->
<Menu>
	{#snippet trigger(props)}
		<Button {...props} iconEnd={IconChevronDown}>Compartir</Button>
	{/snippet}
	<MenuGroup label="Enlace">
		<MenuItem icon={IconLink} shortcut="⌘L">Copiar link</MenuItem>
		<MenuItem icon={IconMail}>Enviar por correo</MenuItem>
	</MenuGroup>
	<MenuSeparator />
	<MenuGroup label="Equipo">
		<MenuItem icon={IconUserPlus}>Invitar al proyecto</MenuItem>
		<MenuItem icon={IconDownload} disabled>Exportar (solo administradores)</MenuItem>
		<MenuItem icon={IconArchive} disabled>Archivar</MenuItem>
	</MenuGroup>
</Menu>

Submenú

MenuSub abre otro menú al lado de su ítem; si al costado no entra, debajo. Úsalo con moderación: un nivel como máximo.

Svelte
<script lang="ts">
	import { Button, Menu, MenuItem, MenuSeparator, MenuSub } from '@archeblack/ui';
	import { IconChevronDown, IconCopy, IconEdit, IconFolder, IconTrash } from '@archeblack/ui/icons';
</script>

<!--
	El submenú se abre con el puntero, Enter, espacio o la flecha derecha; la izquierda lo cierra.
	En una pantalla angosta, donde al costado no entra, se abre debajo de su ítem.
-->
<Menu>
	{#snippet trigger(props)}
		<Button {...props} iconEnd={IconChevronDown}>Archivo</Button>
	{/snippet}
	<MenuItem icon={IconEdit} shortcut="R">Renombrar</MenuItem>
	<MenuItem icon={IconCopy} shortcut="⌘D">Duplicar</MenuItem>
	<MenuSub label="Mover a" icon={IconFolder}>
		<MenuItem>Clientes</MenuItem>
		<MenuItem>Interno</MenuItem>
		<MenuItem>Archivo histórico</MenuItem>
	</MenuSub>
	<MenuSeparator />
	<MenuItem icon={IconTrash} variant="danger">Eliminar</MenuItem>
</Menu>

Links

Con href, el ítem es un link. align="end" alinea el menú con el borde derecho del disparador.

Svelte
<script lang="ts">
	import { Button, Menu, MenuItem, MenuSeparator } from '@archeblack/ui';
	import { IconBook, IconChevronDown, IconSettings, IconUser } from '@archeblack/ui/icons';
</script>

<!-- Con href el ítem es un link: Enter lo sigue y el menú se cierra. -->
<Menu align="end">
	{#snippet trigger(props)}
		<Button {...props} variant="ghost" iconStart={IconUser} iconEnd={IconChevronDown}>
			Cuenta
		</Button>
	{/snippet}
	<MenuItem href="#props" icon={IconUser}>Perfil</MenuItem>
	<MenuItem href="#accessibility" icon={IconSettings}>Ajustes</MenuItem>
	<MenuSeparator />
	<MenuItem href="#avoid" icon={IconBook}>Documentación</MenuItem>
</Menu>

Props

Menu

Props de Menu
PropDescripción
trigger Snippet<[TriggerAttributes]> El disparador, obligatorio: el menú se ancla a él. Recibe los atributos del disparador (id, aria-haspopup="menu", aria-expanded, aria-controls, el puntero, el teclado y la referencia), que se esparcen en un Button: {#snippet trigger(props)}<Button {...props}>Acciones</Button>{/snippet}.
children Snippet Los ítems: MenuItem, MenuSeparator, MenuGroup y MenuSub.
side? 'top' | 'right' | 'bottom' | 'left' Por defecto 'bottom'Lado del disparador en el que aparece. Si no entra, pasa al opuesto.
align? 'start' | 'center' | 'end' Por defecto 'start'Alineación sobre el lado elegido.
bind:open? boolean Por defecto falseSi el menú está abierto.
onOpenChange? (open: boolean) => void Se llama cada vez que el menú se abre o se cierra por sí mismo (disparador, Escape, Tab, clic afuera o al elegir un ítem), con el estado nuevo. Llega después de actualizar open y solo si el cambio se aceptó: si un bind:open de función lo rechaza, no se llama. Tampoco se llama cuando el producto cambia open.
aria-label? string Nombre del menú. Por defecto lo nombra el disparador (aria-labelledby); úsalo si el texto del disparador no alcanza.
class? ClassValue Clases del producto; se suman a arche-menu.
...rest HTMLAttributes<HTMLDivElement> Cualquier otro atributo va al <div role="menu">.

MenuItem

Props de MenuItem
PropDescripción
children Snippet El texto del ítem. También sirve para la búsqueda por letra (typeahead).
icon? IconGlyph Ícono decorativo al inicio, de 16 px. En un menú, todos los ítems con ícono o ninguno.
shortcut? string Atajo a la derecha, en mono («⌘D», «R»). Es visual: el producto implementa el atajo y, para anunciarlo, pasa aria-keyshortcuts («Meta+D»).
variant? 'neutral' | 'danger' Por defecto 'neutral'danger para una acción destructiva: texto y resaltado del estado.
disabled? boolean Por defecto falseSe ve atenuado y no se puede elegir. Las flechas lo saltan.
onSelect? (event: Event) => void Se llama al elegir el ítem (clic, Enter o espacio). Después el menú se cierra y el foco vuelve al disparador; event.preventDefault() lo deja abierto.
href? string Destino: el ítem es un <a role="menuitem"> y Enter lo sigue.
textValue? string Texto para el typeahead si el contenido no es solo texto.
...rest HTMLAttributes<HTMLDivElement> Cualquier otro atributo va al ítem (<div> o <a>), por ejemplo aria-keyshortcuts. class se suma a arche-menu__item.

MenuGroup

Props de MenuGroup
PropDescripción
label string Etiqueta visible, en mono y mayúsculas. Nombra al grupo (aria-labelledby).
children Snippet Los ítems del grupo.

MenuSub

Props de MenuSub
PropDescripción
label string Texto del ítem que abre el submenú. También nombra al submenú.
icon? IconGlyph Ícono decorativo al inicio del ítem.
disabled? boolean Por defecto falseEl ítem no abre el submenú.
bind:open? boolean Por defecto falseSi el submenú está abierto.
onOpenChange? (open: boolean) => void Se llama cuando el submenú se abre o se cierra por sí mismo (flechas, Escape o al elegir un ítem), con el estado nuevo. Llega después de actualizar open y solo si el cambio se aceptó: si un bind:open de función lo rechaza, no se llama. Tampoco se llama cuando el producto cambia open.
children Snippet Los ítems del submenú. ...rest y class van al submenú (role="menu").

MenuSeparator no tiene props propias: sus atributos van al role="separator".

Accesibilidad

  • Sigue el patrón de botón de menú de WAI-ARIA: el disparador lleva aria-haspopup="menu" y aria-expanded; el menú es un role="menu" con role="menuitem", role="group" y role="separator".
  • Enter, espacio o la flecha abajo abren el menú con el foco en el primer ítem. Con el menú abierto, las flechas mueven el foco (y dan la vuelta), Home y End van al primero y al último, y una letra salta al siguiente ítem que empieza por ella.
  • Enter o espacio eligen el ítem y Escape cierra el menú: en los dos casos el foco vuelve al disparador. Tab cierra el menú y lleva el foco a lo que sigue al disparador (Mayúsculas+Tab, a lo anterior).
  • En un submenú, la flecha derecha lo abre y la izquierda lo cierra; el submenú se nombra con el texto de su ítem. Abrirlo con el teclado no desplaza la página. En una pantalla angosta, donde al costado no entran 12 rem, se abre debajo de su ítem (o encima, si abajo no entra).
  • El ítem resaltado es el que tiene el foco y toma hover y text-strong. Con el teclado suma el aro de foco hacia adentro (1,5 px en el color de foco, dentro del menú); con el puntero, solo el fondo. En colores forzados usa los colores de selección del sistema.
  • Los ítems deshabilitados llevan aria-disabled="true": se leen, pero las flechas los saltan y no se pueden elegir.
  • El atajo es visual (aria-hidden) y no forma parte del nombre del ítem. Para anunciarlo, pasa aria-keyshortcuts al ítem.
  • Por defecto el menú se nombra con el disparador. Un botón de solo ícono necesita su aria-label, que nombra a los dos.
  • Con movimiento reducido, el menú aparece y desaparece sin escala ni fundido.

Qué evitar

  • Un menú para elegir un valor de un formulario. Un Select o un RadioGroup: el valor se ve y se envía con el formulario.
  • Un menú para navegar las secciones principales del producto. Links a la vista (navegación lateral, Tabs o Breadcrumb).
  • Más de un nivel de submenú, o submenús con uno o dos ítems. Grupos con etiqueta en un solo menú.
  • Una acción destructiva sin confirmación, o sin la variante danger. El ítem en danger, al final y separado, y un Dialog que confirme.
  • Ítems con controles adentro (campos, casillas sueltas o botones). Un Popover, que admite contenido interactivo.