Saltar al contenido
ssntl/ui

Menu

Un menú desplegable en elevation 2 cuyo encabezado interno y elementos comparten la curva del panel.

Instalación

$ pnpm dlx shadcn@latest add @ssntl/menu

Uso

tsx
import { Menu, MenuContent, MenuHeader, MenuItem, MenuSeparator, MenuTrigger } from "@/components/ui/menu"

<Menu>
  <MenuTrigger asChild><Button variant="secondary">Acciones del producto</Button></MenuTrigger>
  <MenuContent>
    <MenuHeader>Taza de cerámica</MenuHeader>
    <MenuItem><Pencil /> Editar</MenuItem>
    <MenuSeparator />
    <MenuItem destructive><Trash2 /> Eliminar</MenuItem>
  </MenuContent>
</Menu>

Reglas

Lo que este componente espera de ti, tal como se instala en components/ui/menu.md junto al código. El archivo completo agrega cuándo usarlo, teclado, los fundamentos de UI que aplica y cada regla compartida (en inglés).

components/ui/menu.md
## Reglas de Menu

- **Menú + encabezado interno.** El panel es `radius-md` (22) con 8px de padding, así el encabezado y los elementos toman 14 (`radius-sm`) y se anidan en su curva.
- **Capa de menús.** Elevation-2, entrando con `pop-in` desde el lado del disparador.
- Radix maneja flechas, búsqueda por letra, Escape y el regreso del foco. Los elementos miden 44px.
- Los elementos destructivos van en texto rojo, al final, tras un separador; confírmalos en un Dialog antes de ejecutarlos.
- Muestra solo atajos que funcionen.
Abrir el archivo de reglas completo (se abre en otra pestaña)

Código

components/ui/menu.tsx
"use client"

import * as React from "react"
import { DropdownMenu as MenuPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"

// A dropdown menu (Radix): keyboard, typeahead, focus return and collision
// handling come from the primitive. Elevation 2: it floats above the page.
// Nested corners: the panel is radius-md (22) with 8px of padding, so the
// inset header and every item take 22 − 8 = 14 (radius-sm) and share its
// curve.
const Menu = MenuPrimitive.Root
const MenuTrigger = MenuPrimitive.Trigger
const MenuGroup = MenuPrimitive.Group

function MenuContent({
  className,
  sideOffset = 8,
  align = "start",
  ...props
}: React.ComponentProps<typeof MenuPrimitive.Content>) {
  return (
    <MenuPrimitive.Portal>
      <MenuPrimitive.Content
        data-slot="menu-content"
        sideOffset={sideOffset}
        align={align}
        className={cn(
          "z-50 grid max-h-(--radix-dropdown-menu-content-available-height) min-w-56 origin-(--radix-dropdown-menu-content-transform-origin) gap-0.5 overflow-y-auto overscroll-contain rounded-outer-md bg-popover p-concentric-2 text-popover-foreground elevation-2",
          "data-[state=closed]:pop-out data-[state=open]:pop-in",
          className
        )}
        {...props}
      />
    </MenuPrimitive.Portal>
  )
}

/** A muted card at the top: who or what the menu acts on. */
function MenuHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="menu-header"
      className={cn(
        "mb-1 grid gap-0.5 rounded-inner bg-muted px-3 py-2.5",
        className
      )}
      {...props}
    />
  )
}

function MenuItem({
  className,
  destructive = false,
  ...props
}: React.ComponentProps<typeof MenuPrimitive.Item> & {
  /** Red text for Delete or Remove; confirm before it runs. */
  destructive?: boolean
}) {
  return (
    <MenuPrimitive.Item
      data-slot="menu-item"
      className={cn(
        "flex min-h-11 cursor-pointer items-center gap-3 rounded-inner px-3 text-label font-normal outline-none select-none",
        "data-disabled:pointer-events-none data-disabled:opacity-40 data-highlighted:bg-secondary",
        "[&_svg]:size-5 [&_svg]:shrink-0 [&_svg]:opacity-80",
        destructive && "text-destructive",
        className
      )}
      {...props}
    />
  )
}

function MenuSeparator({
  className,
  ...props
}: React.ComponentProps<typeof MenuPrimitive.Separator>) {
  return (
    <MenuPrimitive.Separator
      data-slot="menu-separator"
      className={cn("mx-3 my-1 h-px bg-border", className)}
      {...props}
    />
  )
}

/** A keyboard shortcut at the end of an item. */
function MenuShortcut({ className, ...props }: React.ComponentProps<"kbd">) {
  return (
    <kbd
      data-slot="menu-shortcut"
      className={cn(
        "ms-auto font-sans text-caption text-muted-foreground",
        className
      )}
      {...props}
    />
  )
}

export {
  Menu,
  MenuContent,
  MenuGroup,
  MenuHeader,
  MenuItem,
  MenuSeparator,
  MenuShortcut,
  MenuTrigger,
}