Menu

Un menu est un <details> avec un panneau stylé. C’est un choix délibéré face à l’API popover : un popover vit dans la couche supérieure et ne peut pas être positionné par rapport à son déclencheur sans le positionnement par ancre, qui n’est pas encore partout. Un <details> se place correctement dès aujourd’hui et n’a besoin de rien de chargé.

Le déclencheur est ce <summary>, stylé en bouton — vous passez donc le libellé et les props de bouton à menu() plutôt qu’un button() déjà rendu. Un summary est déjà interactif, et un bouton à l’intérieur imbrique deux contrôles là où il n’y a qu’une action : balisage invalide, et deux arrêts de tabulation pour une seule chose.

La fermeture au clic extérieur et à Échap vient d’un gestionnaire ontoggle qui les importe à la première ouverture d’un menu — et seulement alors. Si ce module n’arrive jamais, un menu s’ouvre et se ferme quand même depuis son propre summary.

menu({ trigger: 'Actions' },
  menuItem({ href: '#edit' }, 'Modifier'),
  menuItem({ href: '#duplicate' }, 'Dupliquer'),
  menuSeparator(),
  menuItem({ href: '#delete' }, 'Supprimer'),
)

Alignement

Un menu s’ouvre depuis le bord de départ de son déclencheur. align: 'end' le retourne, ce qu’il faut à un menu proche du bord droit d’une barre.

Aligné au début
Aligné à la fin
stack({ direction: 'row', gap: 'xl', justify: 'space-between', style: 'width: 100%' },
  menu({ trigger: 'Aligné au début', variant: 'soft' },
    menuItem({ href: '#a' }, 'Premier'),
    menuItem({ href: '#b' }, 'Deuxième'),
  ),
  menu({ trigger: 'Aligné à la fin', variant: 'soft', align: 'end' },
    menuItem({ href: '#c' }, 'Premier'),
    menuItem({ href: '#d' }, 'Deuxième'),
  ),
)

Déclencheurs en icône

Une icône sans texte de trigger réclame un label — il devient le nom accessible que l’icône ne peut pas fournir.

stack({ direction: 'row', gap: 'md' },
  menu({
    align: 'end',
    label: 'Plus d’actions',
    variant: 'ghost',
    icon: icon('more-horizontal'),
  },
    menuItem({ href: '#rename' }, 'Renommer'),
    menuItem({ href: '#move' }, 'Déplacer'),
    menuSeparator(),
    menuItem({ href: '#archive' }, 'Archiver'),
  ),
)

Des éléments avec icônes

menu({ trigger: 'Fichier' },
  menuItem({
    href: '#new',
    icon: icon('plus'),
  }, 'Nouvelle page'),
  menuItem({
    href: '#open',
    icon: icon('folder'),
  }, 'Ouvrir…'),
  menuSeparator(),
  menuItem({
    href: '#build',
    icon: icon('zap'),
  }, 'Construire le site'),
)

Des boutons plutôt que des liens

Un élément sans href rend un <button> — pour une action qui se produit sur la page plutôt qu’une navigation.

Exporter
menu({ trigger: 'Exporter', variant: 'soft', color: 'primary' },
  menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Exporté en JSON.',{color:'success'}))" }, 'En JSON'),
  menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Exporté en CSV.',{color:'success'}))" }, 'En CSV'),
)

Dans une barre d’application

appBar({ brand: 'sitelo' },
  appBarSpacer(),
  appBarActions(
    themeToggle(),
    menu({
      align: 'end',
      label: 'Plus',
      variant: 'ghost',
      icon: icon('more-horizontal'),
    },
      menuItem({ href: '/fr/docs' }, 'Documentation'),
      menuItem({ href: '/fr/examples' }, 'Exemples'),
      menuSeparator(),
      menuItem({ href: 'https://github.com/paul-browne/sitelo' }, 'GitHub'),
    ),
  ),
)

Accessibilité

Le panneau est un role="menu" dont les éléments sont des role="menuitem", et le summary porte aria-haspopup. Un <details> n’est pas un widget de menu natif : c’est donc une approximation raisonnable, pas une parfaite — pour une simple liste de liens, un nav à l’intérieur du details est tout aussi valable et promet moins.

Props

menu() — les props du déclencheur sont celles du bouton :

PropTypeDéfautDescription
triggerChild—Libellé visible. Passez du texte, pas un button() rendu.
iconChild—Balisage avant le libellé, ou seul pour un déclencheur en icône.
labelstring—Nom accessible. Obligatoire quand il y a une icône et aucun texte de trigger.
variant'solid' | 'soft' | 'outline' | 'ghost' | 'link''outline'Style du déclencheur.
color'primary' | 'neutral' | 'success' | 'warning' | 'danger''neutral'La palette du déclencheur.
size'sm' | 'md' | 'lg''md'Taille du déclencheur.
align'start' | 'end''start'Le bord du déclencheur sur lequel le panneau s’aligne.
triggerClassstring—Classes en plus pour le déclencheur plutôt que pour le details englobant.

menuItem() :

PropTypeDéfautDescription
hrefstring—Rend une ancre ; sans lui, un bouton.
iconChild—Balisage avant le libellé.
asstring'button'Élément rendu en l’absence de href.

menuSeparator() ne prend aucune prop — c’est le filet entre deux groupes d’éléments.