Menu

Menu container primitive that manages focus and selection across ui-menu-item children.

Inline menu

Inline keeps a menu visible in the page. Use for an embedded set of actions; an anchored menu uses for and opens from its trigger.

First item
Second item

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>

<ui-menu
  aria-label="Actions"
  inline
>
  <ui-menu-item
    value="first"
  >First item</ui-menu-item>
  <ui-menu-item
    value="second"
  >Second item</ui-menu-item>
</ui-menu>
<script setup>
  import { UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiMenu
    aria-label="Actions"
    :inline="true"
  >
    <UiMenuItem
      value="first"
    >First item</UiMenuItem>
    <UiMenuItem
      value="second"
    >Second item</UiMenuItem>
  </UiMenu>
</template>
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    aria-label="Actions"
    inline
  >
    <UiMenuItem
      value="first"
    >First item</UiMenuItem>
    <UiMenuItem
      value="second"
    >Second item</UiMenuItem>
  </UiMenu>;
}
/** @jsxImportSource preact */
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    aria-label="Actions"
    inline
  >
    <UiMenuItem
      value="first"
    >First item</UiMenuItem>
    <UiMenuItem
      value="second"
    >Second item</UiMenuItem>
  </UiMenu>;
}
<script>
  import { UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<UiMenu
  aria-label="Actions"
  inline
>
  <UiMenuItem
    value="first"
  >First item</UiMenuItem>
  <UiMenuItem
    value="second"
  >Second item</UiMenuItem>
</UiMenu>
import { Component } from "@angular/core";
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiMenu, UiMenuItem],
  template: `
    <div
      ui-menu
      aria-label="Actions"
      [inline]="true"
    >
      <ng-template
        ui-menu-item
        [value]="'first'"
      >First item</ng-template>
      <ng-template
        ui-menu-item
        [value]="'second'"
      >Second item</ng-template>
    </div>
  `,
})
export default class Example {}

for

For associates the menu with the control that toggles and anchors it.

Rename
Share

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-icon-button/ui-icon-button.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-icon/ui-icon.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>

<ui-icon-button
  id="menu-trigger"
  label="Open menu"
>
  <ui-icon
    name="ellipsis"
  ></ui-icon>
</ui-icon-button>
<ui-menu
  for="menu-trigger"
  aria-label="Document actions"
>
  <ui-menu-item
    value="rename"
  >Rename</ui-menu-item>
  <ui-menu-item
    value="share"
  ><ui-icon
      name="link"
    ></ui-icon>Share</ui-menu-item>
</ui-menu>
<script setup>
  import { UiIcon, UiIconButton, UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiIconButton
    id="menu-trigger"
    label="Open menu"
  >
    <UiIcon
      name="ellipsis"
    />
  </UiIconButton>
  <UiMenu
    for="menu-trigger"
    aria-label="Document actions"
  >
    <UiMenuItem
      value="rename"
    >Rename</UiMenuItem>
    <UiMenuItem
      value="share"
    ><UiIcon
        name="link"
      />Share</UiMenuItem>
  </UiMenu>
</template>
import { UiIcon, UiIconButton, UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <>
    <UiIconButton
      id="menu-trigger"
      label="Open menu"
    >
      <UiIcon
        name="ellipsis"
      />
    </UiIconButton>
    <UiMenu
      htmlFor="menu-trigger"
      aria-label="Document actions"
    >
      <UiMenuItem
        value="rename"
      >Rename</UiMenuItem>
      <UiMenuItem
        value="share"
      ><UiIcon
          name="link"
        />Share</UiMenuItem>
    </UiMenu>
  </>;
}
/** @jsxImportSource preact */
import { UiIcon, UiIconButton, UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <>
    <UiIconButton
      id="menu-trigger"
      label="Open menu"
    >
      <UiIcon
        name="ellipsis"
      />
    </UiIconButton>
    <UiMenu
      htmlFor="menu-trigger"
      aria-label="Document actions"
    >
      <UiMenuItem
        value="rename"
      >Rename</UiMenuItem>
      <UiMenuItem
        value="share"
      ><UiIcon
          name="link"
        />Share</UiMenuItem>
    </UiMenu>
  </>;
}
<script>
  import { UiIcon, UiIconButton, UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<UiIconButton
  id="menu-trigger"
  label="Open menu"
>
  <UiIcon
    name="ellipsis"
  />
</UiIconButton>
<UiMenu
  for="menu-trigger"
  aria-label="Document actions"
>
  <UiMenuItem
    value="rename"
  >Rename</UiMenuItem>
  <UiMenuItem
    value="share"
  ><UiIcon
      name="link"
    />Share</UiMenuItem>
</UiMenu>
import { Component } from "@angular/core";
import { UiIcon, UiIconButton, UiMenu, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiIconButton, UiIcon, UiMenu, UiMenuItem],
  template: `
    <button
      ui-icon-button
      id="menu-trigger"
      [label]="'Open menu'"
    >
      <span
        ui-icon
        name="ellipsis"
      ></span>
    </button>
    <div
      ui-menu
      [for]="'menu-trigger'"
      aria-label="Document actions"
    >
      <ng-template
        ui-menu-item
        [value]="'rename'"
      >Rename</ng-template>
      <ng-template
        ui-menu-item
        [value]="'share'"
      ><span
          ui-icon
          name="link"
        ></span>Share</ng-template>
    </div>
  `,
})
export default class Example {}

disabled items

Disabled on a child item keeps it discoverable while removing it from selection.

Rename
Share
Delete (locked)

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>

<ui-menu
  role="menu"
  aria-label="Document actions"
  open
>
  <ui-menu-item
    value="rename"
  >Rename</ui-menu-item>
  <ui-menu-item
    value="share"
  >Share</ui-menu-item>
  <ui-menu-item
    value="delete"
    disabled
  >Delete (locked)</ui-menu-item>
</ui-menu>
<script setup>
  import { UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiMenu
    role="menu"
    aria-label="Document actions"
    :open="true"
  >
    <UiMenuItem
      value="rename"
    >Rename</UiMenuItem>
    <UiMenuItem
      value="share"
    >Share</UiMenuItem>
    <UiMenuItem
      value="delete"
      :disabled="true"
    >Delete (locked)</UiMenuItem>
  </UiMenu>
</template>
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    role="menu"
    aria-label="Document actions"
    open
  >
    <UiMenuItem
      value="rename"
    >Rename</UiMenuItem>
    <UiMenuItem
      value="share"
    >Share</UiMenuItem>
    <UiMenuItem
      value="delete"
      disabled
    >Delete (locked)</UiMenuItem>
  </UiMenu>;
}
/** @jsxImportSource preact */
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    role="menu"
    aria-label="Document actions"
    open
  >
    <UiMenuItem
      value="rename"
    >Rename</UiMenuItem>
    <UiMenuItem
      value="share"
    >Share</UiMenuItem>
    <UiMenuItem
      value="delete"
      disabled
    >Delete (locked)</UiMenuItem>
  </UiMenu>;
}
<script>
  import { UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<UiMenu
  role="menu"
  aria-label="Document actions"
  open
>
  <UiMenuItem
    value="rename"
  >Rename</UiMenuItem>
  <UiMenuItem
    value="share"
  >Share</UiMenuItem>
  <UiMenuItem
    value="delete"
    disabled
  >Delete (locked)</UiMenuItem>
</UiMenu>
import { Component } from "@angular/core";
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiMenu, UiMenuItem],
  template: `
    <div
      ui-menu
      role="menu"
      aria-label="Document actions"
      [open]="true"
    >
      <ng-template
        ui-menu-item
        [value]="'rename'"
      >Rename</ng-template>
      <ng-template
        ui-menu-item
        [value]="'share'"
      >Share</ng-template>
      <ng-template
        ui-menu-item
        [value]="'delete'"
        [disabled]="true"
      >Delete (locked)</ng-template>
    </div>
  `,
})
export default class Example {}

placement="top-end"

Placement sets the preferred edge and alignment against the trigger.

Rename
Duplicate

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>

<ui-button
  id="menu-placed-trigger"
>Open above</ui-button>
<ui-menu
  for="menu-placed-trigger"
  placement="top-end"
>
  <ui-menu-item
    value="rename"
  >Rename</ui-menu-item>
  <ui-menu-item
    value="duplicate"
  >Duplicate</ui-menu-item>
</ui-menu>
<script setup>
  import { UiButton, UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiButton
    id="menu-placed-trigger"
  >Open above</UiButton>
  <UiMenu
    for="menu-placed-trigger"
    placement="top-end"
  >
    <UiMenuItem
      value="rename"
    >Rename</UiMenuItem>
    <UiMenuItem
      value="duplicate"
    >Duplicate</UiMenuItem>
  </UiMenu>
</template>
import { UiButton, UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <>
    <UiButton
      id="menu-placed-trigger"
    >Open above</UiButton>
    <UiMenu
      htmlFor="menu-placed-trigger"
      placement="top-end"
    >
      <UiMenuItem
        value="rename"
      >Rename</UiMenuItem>
      <UiMenuItem
        value="duplicate"
      >Duplicate</UiMenuItem>
    </UiMenu>
  </>;
}
/** @jsxImportSource preact */
import { UiButton, UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <>
    <UiButton
      id="menu-placed-trigger"
    >Open above</UiButton>
    <UiMenu
      htmlFor="menu-placed-trigger"
      placement="top-end"
    >
      <UiMenuItem
        value="rename"
      >Rename</UiMenuItem>
      <UiMenuItem
        value="duplicate"
      >Duplicate</UiMenuItem>
    </UiMenu>
  </>;
}
<script>
  import { UiButton, UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<UiButton
  id="menu-placed-trigger"
>Open above</UiButton>
<UiMenu
  for="menu-placed-trigger"
  placement="top-end"
>
  <UiMenuItem
    value="rename"
  >Rename</UiMenuItem>
  <UiMenuItem
    value="duplicate"
  >Duplicate</UiMenuItem>
</UiMenu>
import { Component } from "@angular/core";
import { UiButton, UiMenu, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiButton, UiMenu, UiMenuItem],
  template: `
    <ng-template
      ui-button
      [hnAttributes]="{ id: 'menu-placed-trigger' }"
    >Open above</ng-template>
    <div
      ui-menu
      [for]="'menu-placed-trigger'"
      [placement]="'top-end'"
    >
      <ng-template
        ui-menu-item
        [value]="'rename'"
      >Rename</ng-template>
      <ng-template
        ui-menu-item
        [value]="'duplicate'"
      >Duplicate</ng-template>
    </div>
  `,
})
export default class Example {}

Groups, links, and shortcuts

Group headings, native separators, real links, and display-only shortcut hints make larger menus easier to scan.

Create
New file
From template

Settings

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-group/ui-menu-group.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>

<ui-button
  id="open-file-menu"
>File</ui-button>
<ui-menu
  for="open-file-menu"
  aria-label="File actions"
>
  <ui-menu-group
    label="Create"
  >
    <ui-menu-item
      value="new"
    >
      New file
      <kbd
        slot="shortcut"
      >⌘N</kbd>
    </ui-menu-item>
    <ui-menu-item
      value="template"
      href="#templates"
    >From template</ui-menu-item>
  </ui-menu-group>
  <hr>
  <ui-menu-item
    value="settings"
  >Settings</ui-menu-item>
</ui-menu>
<script setup>
  import { UiButton, UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiButton
    id="open-file-menu"
  >File</UiButton>
  <UiMenu
    for="open-file-menu"
    aria-label="File actions"
  >
    <UiMenuGroup
      label="Create"
    >
      <UiMenuItem
        value="new"
      >New file <template #shortcut>
          <kbd>⌘N</kbd>
        </template></UiMenuItem>
      <UiMenuItem
        value="template"
        href="#templates"
      >From template</UiMenuItem>
    </UiMenuGroup>
    <hr>
    <UiMenuItem
      value="settings"
    >Settings</UiMenuItem>
  </UiMenu>
</template>
import { UiButton, UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <>
    <UiButton
      id="open-file-menu"
    >File</UiButton>
    <UiMenu
      htmlFor="open-file-menu"
      aria-label="File actions"
    >
      <UiMenuGroup
        label="Create"
      >
        <UiMenuItem
          value="new"
          slots={{
            shortcut: <kbd>⌘N</kbd>,
          }}
        >New file </UiMenuItem>
        <UiMenuItem
          value="template"
          href="#templates"
        >From template</UiMenuItem>
      </UiMenuGroup>
      <hr />
      <UiMenuItem
        value="settings"
      >Settings</UiMenuItem>
    </UiMenu>
  </>;
}
/** @jsxImportSource preact */
import { UiButton, UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <>
    <UiButton
      id="open-file-menu"
    >File</UiButton>
    <UiMenu
      htmlFor="open-file-menu"
      aria-label="File actions"
    >
      <UiMenuGroup
        label="Create"
      >
        <UiMenuItem
          value="new"
          slots={{
            shortcut: <kbd>⌘N</kbd>,
          }}
        >New file </UiMenuItem>
        <UiMenuItem
          value="template"
          href="#templates"
        >From template</UiMenuItem>
      </UiMenuGroup>
      <hr />
      <UiMenuItem
        value="settings"
      >Settings</UiMenuItem>
    </UiMenu>
  </>;
}
<script>
  import { UiButton, UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";
</script>

{#snippet shortcut()}
  <kbd>⌘N</kbd>
{/snippet}

<UiButton
  id="open-file-menu"
>File</UiButton>
<UiMenu
  for="open-file-menu"
  aria-label="File actions"
>
  <UiMenuGroup
    label="Create"
  >
    <UiMenuItem
      value="new"
      slots={{ shortcut }}
    >New file </UiMenuItem>
    <UiMenuItem
      value="template"
      href="#templates"
    >From template</UiMenuItem>
  </UiMenuGroup>
  <hr>
  <UiMenuItem
    value="settings"
  >Settings</UiMenuItem>
</UiMenu>
import { Component } from "@angular/core";
import { UiButton, UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiButton, UiMenu, UiMenuGroup, UiMenuItem],
  template: `
    <ng-template
      ui-button
      [hnAttributes]="{ id: 'open-file-menu' }"
    >File</ng-template>
    <div
      ui-menu
      [for]="'open-file-menu'"
      aria-label="File actions"
    >
      <div
        ui-menu-group
        [label]="'Create'"
      >
        <ng-template #shortcut>
          <kbd>⌘N</kbd>
        </ng-template>
        <ng-template
          ui-menu-item
          [value]="'new'"
          [slots]="{ shortcut: shortcut }"
        >New file </ng-template>
        <ng-template
          ui-menu-item
          [value]="'template'"
          [href]="'#templates'"
        >From template</ng-template>
      </div>
      <hr>
      <ng-template
        ui-menu-item
        [value]="'settings'"
      >Settings</ng-template>
    </div>
  `,
})
export default class Example {}

Checkable view options

Checkbox items toggle independently, while radio items choose one option within their named group. They keep the menu open so another choice can be made.

Show grid

Sort by
Name
Date

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-group/ui-menu-group.html"
>

<ui-button
  id="open-view-menu"
>View</ui-button>
<ui-menu
  for="open-view-menu"
  aria-label="View options"
>
  <ui-menu-item
    type="checkbox"
    value="grid"
    checked
  >Show grid</ui-menu-item>
  <hr>
  <ui-menu-group
    label="Sort by"
  >
    <ui-menu-item
      type="radio"
      value="name"
      checked
    >Name</ui-menu-item>
    <ui-menu-item
      type="radio"
      value="date"
    >Date</ui-menu-item>
  </ui-menu-group>
</ui-menu>
<script setup>
  import { UiButton, UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiButton
    id="open-view-menu"
  >View</UiButton>
  <UiMenu
    for="open-view-menu"
    aria-label="View options"
  >
    <UiMenuItem
      type="checkbox"
      value="grid"
      :checked="true"
    >Show grid</UiMenuItem>
    <hr>
    <UiMenuGroup
      label="Sort by"
    >
      <UiMenuItem
        type="radio"
        value="name"
        :checked="true"
      >Name</UiMenuItem>
      <UiMenuItem
        type="radio"
        value="date"
      >Date</UiMenuItem>
    </UiMenuGroup>
  </UiMenu>
</template>
import { UiButton, UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <>
    <UiButton
      id="open-view-menu"
    >View</UiButton>
    <UiMenu
      htmlFor="open-view-menu"
      aria-label="View options"
    >
      <UiMenuItem
        type="checkbox"
        value="grid"
        checked
      >Show grid</UiMenuItem>
      <hr />
      <UiMenuGroup
        label="Sort by"
      >
        <UiMenuItem
          type="radio"
          value="name"
          checked
        >Name</UiMenuItem>
        <UiMenuItem
          type="radio"
          value="date"
        >Date</UiMenuItem>
      </UiMenuGroup>
    </UiMenu>
  </>;
}
/** @jsxImportSource preact */
import { UiButton, UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <>
    <UiButton
      id="open-view-menu"
    >View</UiButton>
    <UiMenu
      htmlFor="open-view-menu"
      aria-label="View options"
    >
      <UiMenuItem
        type="checkbox"
        value="grid"
        checked
      >Show grid</UiMenuItem>
      <hr />
      <UiMenuGroup
        label="Sort by"
      >
        <UiMenuItem
          type="radio"
          value="name"
          checked
        >Name</UiMenuItem>
        <UiMenuItem
          type="radio"
          value="date"
        >Date</UiMenuItem>
      </UiMenuGroup>
    </UiMenu>
  </>;
}
<script>
  import { UiButton, UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";
</script>

<UiButton
  id="open-view-menu"
>View</UiButton>
<UiMenu
  for="open-view-menu"
  aria-label="View options"
>
  <UiMenuItem
    type="checkbox"
    value="grid"
    checked
  >Show grid</UiMenuItem>
  <hr>
  <UiMenuGroup
    label="Sort by"
  >
    <UiMenuItem
      type="radio"
      value="name"
      checked
    >Name</UiMenuItem>
    <UiMenuItem
      type="radio"
      value="date"
    >Date</UiMenuItem>
  </UiMenuGroup>
</UiMenu>
import { Component } from "@angular/core";
import { UiButton, UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiButton, UiMenu, UiMenuItem, UiMenuGroup],
  template: `
    <ng-template
      ui-button
      [hnAttributes]="{ id: 'open-view-menu' }"
    >View</ng-template>
    <div
      ui-menu
      [for]="'open-view-menu'"
      aria-label="View options"
    >
      <ng-template
        ui-menu-item
        [type]="'checkbox'"
        [value]="'grid'"
        [checked]="true"
      >Show grid</ng-template>
      <hr>
      <div
        ui-menu-group
        [label]="'Sort by'"
      >
        <ng-template
          ui-menu-item
          [type]="'radio'"
          [value]="'name'"
          [checked]="true"
        >Name</ng-template>
        <ng-template
          ui-menu-item
          [type]="'radio'"
          [value]="'date'"
        >Date</ng-template>
      </div>
    </div>
  `,
})
export default class Example {}

Small menu

size="sm" matches a menu to a small trigger, such as an xs or sm button. The chosen radio item shows a check, as in every menu.

Name
Date
Size

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>

<ui-button
  id="open-small-menu"
  variant="ghost"
  tone="neutral"
  size="xs"
  shape="pill"
>Sort: Name</ui-button>
<ui-menu
  for="open-small-menu"
  size="sm"
  aria-label="Sort by"
>
  <ui-menu-item
    type="radio"
    value="name"
    checked
  >Name</ui-menu-item>
  <ui-menu-item
    type="radio"
    value="date"
  >Date</ui-menu-item>
  <ui-menu-item
    type="radio"
    value="size"
  >Size</ui-menu-item>
</ui-menu>
<script setup>
  import { UiButton, UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiButton
    id="open-small-menu"
    variant="ghost"
    tone="neutral"
    size="xs"
    shape="pill"
  >Sort: Name</UiButton>
  <UiMenu
    for="open-small-menu"
    size="sm"
    aria-label="Sort by"
  >
    <UiMenuItem
      type="radio"
      value="name"
      :checked="true"
    >Name</UiMenuItem>
    <UiMenuItem
      type="radio"
      value="date"
    >Date</UiMenuItem>
    <UiMenuItem
      type="radio"
      value="size"
    >Size</UiMenuItem>
  </UiMenu>
</template>
import { UiButton, UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <>
    <UiButton
      id="open-small-menu"
      variant="ghost"
      tone="neutral"
      size="xs"
      shape="pill"
    >Sort: Name</UiButton>
    <UiMenu
      htmlFor="open-small-menu"
      size="sm"
      aria-label="Sort by"
    >
      <UiMenuItem
        type="radio"
        value="name"
        checked
      >Name</UiMenuItem>
      <UiMenuItem
        type="radio"
        value="date"
      >Date</UiMenuItem>
      <UiMenuItem
        type="radio"
        value="size"
      >Size</UiMenuItem>
    </UiMenu>
  </>;
}
/** @jsxImportSource preact */
import { UiButton, UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <>
    <UiButton
      id="open-small-menu"
      variant="ghost"
      tone="neutral"
      size="xs"
      shape="pill"
    >Sort: Name</UiButton>
    <UiMenu
      htmlFor="open-small-menu"
      size="sm"
      aria-label="Sort by"
    >
      <UiMenuItem
        type="radio"
        value="name"
        checked
      >Name</UiMenuItem>
      <UiMenuItem
        type="radio"
        value="date"
      >Date</UiMenuItem>
      <UiMenuItem
        type="radio"
        value="size"
      >Size</UiMenuItem>
    </UiMenu>
  </>;
}
<script>
  import { UiButton, UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<UiButton
  id="open-small-menu"
  variant="ghost"
  tone="neutral"
  size="xs"
  shape="pill"
>Sort: Name</UiButton>
<UiMenu
  for="open-small-menu"
  size="sm"
  aria-label="Sort by"
>
  <UiMenuItem
    type="radio"
    value="name"
    checked
  >Name</UiMenuItem>
  <UiMenuItem
    type="radio"
    value="date"
  >Date</UiMenuItem>
  <UiMenuItem
    type="radio"
    value="size"
  >Size</UiMenuItem>
</UiMenu>
import { Component } from "@angular/core";
import { UiButton, UiMenu, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiButton, UiMenu, UiMenuItem],
  template: `
    <ng-template
      ui-button
      [variant]="'ghost'"
      [tone]="'neutral'"
      [size]="'xs'"
      [shape]="'pill'"
      [hnAttributes]="{ id: 'open-small-menu' }"
    >Sort: Name</ng-template>
    <div
      ui-menu
      [for]="'open-small-menu'"
      [size]="'sm'"
      aria-label="Sort by"
    >
      <ng-template
        ui-menu-item
        [type]="'radio'"
        [value]="'name'"
        [checked]="true"
      >Name</ng-template>
      <ng-template
        ui-menu-item
        [type]="'radio'"
        [value]="'date'"
      >Date</ng-template>
      <ng-template
        ui-menu-item
        [type]="'radio'"
        [value]="'size'"
      >Size</ng-template>
    </div>
  `,
})
export default class Example {}

Aligned icon columns

Icon-bearing and text-only menu actions share a label column; destructive actions keep their danger tone.

Edit
Comments
History
Delete

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-icon/ui-icon.html"
>

<ui-menu
  inline
  icons
  aria-label="File actions"
>
  <ui-menu-item
    value="edit"
  >
    <ui-icon
      slot="icon"
      name="pencil"
    ></ui-icon>
    Edit
    <span
      slot="shortcut"
    >Ctrl E</span>
  </ui-menu-item>
  <ui-menu-item
    value="comments"
  >Comments</ui-menu-item>
  <ui-menu-item
    value="history"
    href="#history"
  >
    <ui-icon
      slot="icon"
      name="history"
    ></ui-icon>
    History
  </ui-menu-item>
  <ui-menu-item
    value="delete"
    tone="danger"
  >
    <ui-icon
      slot="icon"
      name="trash-2"
    ></ui-icon>
    Delete
  </ui-menu-item>
</ui-menu>
<script setup>
  import { UiIcon, UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiMenu
    :inline="true"
    :icons="true"
    aria-label="File actions"
  >
    <UiMenuItem
      value="edit"
    ><template #icon>
        <UiIcon
          name="pencil"
        />
      </template>Edit<template #shortcut>
        <span>Ctrl E</span>
      </template></UiMenuItem>
    <UiMenuItem
      value="comments"
    >Comments</UiMenuItem>
    <UiMenuItem
      value="history"
      href="#history"
    ><template #icon>
        <UiIcon
          name="history"
        />
      </template>History</UiMenuItem>
    <UiMenuItem
      value="delete"
      tone="danger"
    ><template #icon>
        <UiIcon
          name="trash-2"
        />
      </template>Delete</UiMenuItem>
  </UiMenu>
</template>
import { UiIcon, UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    inline
    icons
    aria-label="File actions"
  >
    <UiMenuItem
      value="edit"
      slots={{
        icon: <UiIcon
          name="pencil"
        />,
        shortcut: <span>Ctrl E</span>,
      }}
    >Edit</UiMenuItem>
    <UiMenuItem
      value="comments"
    >Comments</UiMenuItem>
    <UiMenuItem
      value="history"
      href="#history"
      slots={{
        icon: <UiIcon
          name="history"
        />,
      }}
    >History</UiMenuItem>
    <UiMenuItem
      value="delete"
      tone="danger"
      slots={{
        icon: <UiIcon
          name="trash-2"
        />,
      }}
    >Delete</UiMenuItem>
  </UiMenu>;
}
/** @jsxImportSource preact */
import { UiIcon, UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    inline
    icons
    aria-label="File actions"
  >
    <UiMenuItem
      value="edit"
      slots={{
        icon: <UiIcon
          name="pencil"
        />,
        shortcut: <span>Ctrl E</span>,
      }}
    >Edit</UiMenuItem>
    <UiMenuItem
      value="comments"
    >Comments</UiMenuItem>
    <UiMenuItem
      value="history"
      href="#history"
      slots={{
        icon: <UiIcon
          name="history"
        />,
      }}
    >History</UiMenuItem>
    <UiMenuItem
      value="delete"
      tone="danger"
      slots={{
        icon: <UiIcon
          name="trash-2"
        />,
      }}
    >Delete</UiMenuItem>
  </UiMenu>;
}
<script>
  import { UiIcon, UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

{#snippet icon()}
  <UiIcon
    name="pencil"
  />
{/snippet}

{#snippet shortcut()}
  <span>Ctrl E</span>
{/snippet}

{#snippet menuItemIcon()}
  <UiIcon
    name="history"
  />
{/snippet}

{#snippet menuItemIcon2()}
  <UiIcon
    name="trash-2"
  />
{/snippet}

<UiMenu
  inline
  icons
  aria-label="File actions"
>
  <UiMenuItem
    value="edit"
    slots={{ icon, shortcut }}
  >Edit</UiMenuItem>
  <UiMenuItem
    value="comments"
  >Comments</UiMenuItem>
  <UiMenuItem
    value="history"
    href="#history"
    slots={{ icon: menuItemIcon }}
  >History</UiMenuItem>
  <UiMenuItem
    value="delete"
    tone="danger"
    slots={{ icon: menuItemIcon2 }}
  >Delete</UiMenuItem>
</UiMenu>
import { Component } from "@angular/core";
import { UiIcon, UiMenu, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiMenu, UiMenuItem, UiIcon],
  template: `
    <div
      ui-menu
      [inline]="true"
      [icons]="true"
      aria-label="File actions"
    >
      <ng-template #icon>
        <span
          ui-icon
          name="pencil"
        ></span>
      </ng-template>
      <ng-template #shortcut>
        <span>Ctrl E</span>
      </ng-template>
      <ng-template
        ui-menu-item
        [value]="'edit'"
        [slots]="{ icon: icon, shortcut: shortcut }"
      >Edit</ng-template>
      <ng-template
        ui-menu-item
        [value]="'comments'"
      >Comments</ng-template>
      <ng-template #menuItemIcon>
        <span
          ui-icon
          name="history"
        ></span>
      </ng-template>
      <ng-template
        ui-menu-item
        [value]="'history'"
        [href]="'#history'"
        [slots]="{ icon: menuItemIcon }"
      >History</ng-template>
      <ng-template #menuItemIcon2>
        <span
          ui-icon
          name="trash-2"
        ></span>
      </ng-template>
      <ng-template
        ui-menu-item
        [value]="'delete'"
        [tone]="'danger'"
        [slots]="{ icon: menuItemIcon2 }"
      >Delete</ng-template>
    </div>
  `,
})
export default class Example {}

Named group

A visible group heading also names its grouped menu items for assistive technology.

Sort by
Name
Date

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-group/ui-menu-group.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>

<ui-menu
  open
  aria-label="Sort options"
>
  <ui-menu-group
    label="Sort by"
  >
    <ui-menu-item
      value="name"
    >Name</ui-menu-item>
    <ui-menu-item
      value="date"
    >Date</ui-menu-item>
  </ui-menu-group>
</ui-menu>
<script setup>
  import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiMenu
    :open="true"
    aria-label="Sort options"
  >
    <UiMenuGroup
      label="Sort by"
    >
      <UiMenuItem
        value="name"
      >Name</UiMenuItem>
      <UiMenuItem
        value="date"
      >Date</UiMenuItem>
    </UiMenuGroup>
  </UiMenu>
</template>
import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    open
    aria-label="Sort options"
  >
    <UiMenuGroup
      label="Sort by"
    >
      <UiMenuItem
        value="name"
      >Name</UiMenuItem>
      <UiMenuItem
        value="date"
      >Date</UiMenuItem>
    </UiMenuGroup>
  </UiMenu>;
}
/** @jsxImportSource preact */
import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    open
    aria-label="Sort options"
  >
    <UiMenuGroup
      label="Sort by"
    >
      <UiMenuItem
        value="name"
      >Name</UiMenuItem>
      <UiMenuItem
        value="date"
      >Date</UiMenuItem>
    </UiMenuGroup>
  </UiMenu>;
}
<script>
  import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";
</script>

<UiMenu
  open
  aria-label="Sort options"
>
  <UiMenuGroup
    label="Sort by"
  >
    <UiMenuItem
      value="name"
    >Name</UiMenuItem>
    <UiMenuItem
      value="date"
    >Date</UiMenuItem>
  </UiMenuGroup>
</UiMenu>
import { Component } from "@angular/core";
import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiMenu, UiMenuGroup, UiMenuItem],
  template: `
    <div
      ui-menu
      [open]="true"
      aria-label="Sort options"
    >
      <div
        ui-menu-group
        [label]="'Sort by'"
      >
        <ng-template
          ui-menu-item
          [value]="'name'"
        >Name</ng-template>
        <ng-template
          ui-menu-item
          [value]="'date'"
        >Date</ng-template>
      </div>
    </div>
  `,
})
export default class Example {}

Radio choices

Radio items are exclusive within their nearest named group.

Density
Comfortable
Compact

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-group/ui-menu-group.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>

<ui-menu
  open
  aria-label="View options"
>
  <ui-menu-group
    label="Density"
  >
    <ui-menu-item
      type="radio"
      name="density"
      value="comfortable"
      checked
    >Comfortable</ui-menu-item>
    <ui-menu-item
      type="radio"
      name="density"
      value="compact"
    >Compact</ui-menu-item>
  </ui-menu-group>
</ui-menu>
<script setup>
  import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiMenu
    :open="true"
    aria-label="View options"
  >
    <UiMenuGroup
      label="Density"
    >
      <UiMenuItem
        type="radio"
        name="density"
        value="comfortable"
        :checked="true"
      >Comfortable</UiMenuItem>
      <UiMenuItem
        type="radio"
        name="density"
        value="compact"
      >Compact</UiMenuItem>
    </UiMenuGroup>
  </UiMenu>
</template>
import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    open
    aria-label="View options"
  >
    <UiMenuGroup
      label="Density"
    >
      <UiMenuItem
        type="radio"
        name="density"
        value="comfortable"
        checked
      >Comfortable</UiMenuItem>
      <UiMenuItem
        type="radio"
        name="density"
        value="compact"
      >Compact</UiMenuItem>
    </UiMenuGroup>
  </UiMenu>;
}
/** @jsxImportSource preact */
import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    open
    aria-label="View options"
  >
    <UiMenuGroup
      label="Density"
    >
      <UiMenuItem
        type="radio"
        name="density"
        value="comfortable"
        checked
      >Comfortable</UiMenuItem>
      <UiMenuItem
        type="radio"
        name="density"
        value="compact"
      >Compact</UiMenuItem>
    </UiMenuGroup>
  </UiMenu>;
}
<script>
  import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";
</script>

<UiMenu
  open
  aria-label="View options"
>
  <UiMenuGroup
    label="Density"
  >
    <UiMenuItem
      type="radio"
      name="density"
      value="comfortable"
      checked
    >Comfortable</UiMenuItem>
    <UiMenuItem
      type="radio"
      name="density"
      value="compact"
    >Compact</UiMenuItem>
  </UiMenuGroup>
</UiMenu>
import { Component } from "@angular/core";
import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiMenu, UiMenuGroup, UiMenuItem],
  template: `
    <div
      ui-menu
      [open]="true"
      aria-label="View options"
    >
      <div
        ui-menu-group
        [label]="'Density'"
      >
        <ng-template
          ui-menu-item
          [type]="'radio'"
          [value]="'comfortable'"
          [checked]="true"
          [hnAttributes]="{ name: 'density' }"
        >Comfortable</ng-template>
        <ng-template
          ui-menu-item
          [type]="'radio'"
          [value]="'compact'"
          [hnAttributes]="{ name: 'density' }"
        >Compact</ng-template>
      </div>
    </div>
  `,
})
export default class Example {}

Default

With disabled omitted, Menu Item is selectable inside its required menu context.

Menu item

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>

<ui-menu
  role="menu"
  aria-label="Actions"
  open
>
  <ui-menu-item
    value="item"
  >Menu item</ui-menu-item>
</ui-menu>
<script setup>
  import { UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiMenu
    role="menu"
    aria-label="Actions"
    :open="true"
  >
    <UiMenuItem
      value="item"
    >Menu item</UiMenuItem>
  </UiMenu>
</template>
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    role="menu"
    aria-label="Actions"
    open
  >
    <UiMenuItem
      value="item"
    >Menu item</UiMenuItem>
  </UiMenu>;
}
/** @jsxImportSource preact */
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    role="menu"
    aria-label="Actions"
    open
  >
    <UiMenuItem
      value="item"
    >Menu item</UiMenuItem>
  </UiMenu>;
}
<script>
  import { UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<UiMenu
  role="menu"
  aria-label="Actions"
  open
>
  <UiMenuItem
    value="item"
  >Menu item</UiMenuItem>
</UiMenu>
import { Component } from "@angular/core";
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiMenu, UiMenuItem],
  template: `
    <div
      ui-menu
      role="menu"
      aria-label="Actions"
      [open]="true"
    >
      <ng-template
        ui-menu-item
        [value]="'item'"
      >Menu item</ng-template>
    </div>
  `,
})
export default class Example {}

disabled

Disabled retains the row label while removing the item from selection.

Publish now
Schedule publishing

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>

<ui-menu
  role="menu"
  aria-label="Publishing actions"
  open
>
  <ui-menu-item
    value="publish"
  >Publish now</ui-menu-item>
  <ui-menu-item
    value="schedule"
    disabled
  >Schedule publishing</ui-menu-item>
</ui-menu>
<script setup>
  import { UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiMenu
    role="menu"
    aria-label="Publishing actions"
    :open="true"
  >
    <UiMenuItem
      value="publish"
    >Publish now</UiMenuItem>
    <UiMenuItem
      value="schedule"
      :disabled="true"
    >Schedule publishing</UiMenuItem>
  </UiMenu>
</template>
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    role="menu"
    aria-label="Publishing actions"
    open
  >
    <UiMenuItem
      value="publish"
    >Publish now</UiMenuItem>
    <UiMenuItem
      value="schedule"
      disabled
    >Schedule publishing</UiMenuItem>
  </UiMenu>;
}
/** @jsxImportSource preact */
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    role="menu"
    aria-label="Publishing actions"
    open
  >
    <UiMenuItem
      value="publish"
    >Publish now</UiMenuItem>
    <UiMenuItem
      value="schedule"
      disabled
    >Schedule publishing</UiMenuItem>
  </UiMenu>;
}
<script>
  import { UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<UiMenu
  role="menu"
  aria-label="Publishing actions"
  open
>
  <UiMenuItem
    value="publish"
  >Publish now</UiMenuItem>
  <UiMenuItem
    value="schedule"
    disabled
  >Schedule publishing</UiMenuItem>
</UiMenu>
import { Component } from "@angular/core";
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiMenu, UiMenuItem],
  template: `
    <div
      ui-menu
      role="menu"
      aria-label="Publishing actions"
      [open]="true"
    >
      <ng-template
        ui-menu-item
        [value]="'publish'"
      >Publish now</ng-template>
      <ng-template
        ui-menu-item
        [value]="'schedule'"
        [disabled]="true"
      >Schedule publishing</ng-template>
    </div>
  `,
})
export default class Example {}

Link and shortcut

A destination renders a native anchor, while the shortcut slot is a display hint and does not bind a key.

Browse templates
New file

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>

<ui-menu
  open
  aria-label="File actions"
>
  <ui-menu-item
    value="templates"
    href="#templates"
  >Browse templates</ui-menu-item>
  <ui-menu-item
    value="new"
  >
    New file
    <kbd
      slot="shortcut"
    >⌘N</kbd>
  </ui-menu-item>
</ui-menu>
<script setup>
  import { UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiMenu
    :open="true"
    aria-label="File actions"
  >
    <UiMenuItem
      value="templates"
      href="#templates"
    >Browse templates</UiMenuItem>
    <UiMenuItem
      value="new"
    >New file <template #shortcut>
        <kbd>⌘N</kbd>
      </template></UiMenuItem>
  </UiMenu>
</template>
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    open
    aria-label="File actions"
  >
    <UiMenuItem
      value="templates"
      href="#templates"
    >Browse templates</UiMenuItem>
    <UiMenuItem
      value="new"
      slots={{
        shortcut: <kbd>⌘N</kbd>,
      }}
    >New file </UiMenuItem>
  </UiMenu>;
}
/** @jsxImportSource preact */
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    open
    aria-label="File actions"
  >
    <UiMenuItem
      value="templates"
      href="#templates"
    >Browse templates</UiMenuItem>
    <UiMenuItem
      value="new"
      slots={{
        shortcut: <kbd>⌘N</kbd>,
      }}
    >New file </UiMenuItem>
  </UiMenu>;
}
<script>
  import { UiMenu, UiMenuItem } from "@threadlabs/looma";
</script>

{#snippet shortcut()}
  <kbd>⌘N</kbd>
{/snippet}

<UiMenu
  open
  aria-label="File actions"
>
  <UiMenuItem
    value="templates"
    href="#templates"
  >Browse templates</UiMenuItem>
  <UiMenuItem
    value="new"
    slots={{ shortcut }}
  >New file </UiMenuItem>
</UiMenu>
import { Component } from "@angular/core";
import { UiMenu, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiMenu, UiMenuItem],
  template: `
    <div
      ui-menu
      [open]="true"
      aria-label="File actions"
    >
      <ng-template
        ui-menu-item
        [value]="'templates'"
        [href]="'#templates'"
      >Browse templates</ng-template>
      <ng-template #shortcut>
        <kbd>⌘N</kbd>
      </ng-template>
      <ng-template
        ui-menu-item
        [value]="'new'"
        [slots]="{ shortcut: shortcut }"
      >New file </ng-template>
    </div>
  `,
})
export default class Example {}

Checkable items

Checked state is announced through menuitemcheckbox or menuitemradio. A checked item emits change and the menu stays open.

Show grid
Sort by
Name
Date

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu/ui-menu.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-item/ui-menu-item.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-menu-group/ui-menu-group.html"
>

<ui-menu
  open
  aria-label="View options"
>
  <ui-menu-item
    type="checkbox"
    value="grid"
    checked
  >Show grid</ui-menu-item>
  <ui-menu-group
    label="Sort by"
  >
    <ui-menu-item
      type="radio"
      value="name"
      checked
    >Name</ui-menu-item>
    <ui-menu-item
      type="radio"
      value="date"
    >Date</ui-menu-item>
  </ui-menu-group>
</ui-menu>
<script setup>
  import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";
</script>

<template>
  <UiMenu
    :open="true"
    aria-label="View options"
  >
    <UiMenuItem
      type="checkbox"
      value="grid"
      :checked="true"
    >Show grid</UiMenuItem>
    <UiMenuGroup
      label="Sort by"
    >
      <UiMenuItem
        type="radio"
        value="name"
        :checked="true"
      >Name</UiMenuItem>
      <UiMenuItem
        type="radio"
        value="date"
      >Date</UiMenuItem>
    </UiMenuGroup>
  </UiMenu>
</template>
import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    open
    aria-label="View options"
  >
    <UiMenuItem
      type="checkbox"
      value="grid"
      checked
    >Show grid</UiMenuItem>
    <UiMenuGroup
      label="Sort by"
    >
      <UiMenuItem
        type="radio"
        value="name"
        checked
      >Name</UiMenuItem>
      <UiMenuItem
        type="radio"
        value="date"
      >Date</UiMenuItem>
    </UiMenuGroup>
  </UiMenu>;
}
/** @jsxImportSource preact */
import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

export default function Example() {
  return <UiMenu
    open
    aria-label="View options"
  >
    <UiMenuItem
      type="checkbox"
      value="grid"
      checked
    >Show grid</UiMenuItem>
    <UiMenuGroup
      label="Sort by"
    >
      <UiMenuItem
        type="radio"
        value="name"
        checked
      >Name</UiMenuItem>
      <UiMenuItem
        type="radio"
        value="date"
      >Date</UiMenuItem>
    </UiMenuGroup>
  </UiMenu>;
}
<script>
  import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";
</script>

<UiMenu
  open
  aria-label="View options"
>
  <UiMenuItem
    type="checkbox"
    value="grid"
    checked
  >Show grid</UiMenuItem>
  <UiMenuGroup
    label="Sort by"
  >
    <UiMenuItem
      type="radio"
      value="name"
      checked
    >Name</UiMenuItem>
    <UiMenuItem
      type="radio"
      value="date"
    >Date</UiMenuItem>
  </UiMenuGroup>
</UiMenu>
import { Component } from "@angular/core";
import { UiMenu, UiMenuGroup, UiMenuItem } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiMenu, UiMenuItem, UiMenuGroup],
  template: `
    <div
      ui-menu
      [open]="true"
      aria-label="View options"
    >
      <ng-template
        ui-menu-item
        [type]="'checkbox'"
        [value]="'grid'"
        [checked]="true"
      >Show grid</ng-template>
      <div
        ui-menu-group
        [label]="'Sort by'"
      >
        <ng-template
          ui-menu-item
          [type]="'radio'"
          [value]="'name'"
          [checked]="true"
        >Name</ng-template>
        <ng-template
          ui-menu-item
          [type]="'radio'"
          [value]="'date'"
        >Date</ng-template>
      </div>
    </div>
  `,
})
export default class Example {}

API

Package: @threadlabs/looma · Native root: div

Attributes

NameDescriptionPropertyTypeDefaultAllowed values
densityCompact trades padding and type size for rows that fit more on screen.densitykeywordcomfortablecomfortable, compact
forID of the control that toggles and anchors the menu.forstring—
iconsReserves a fixed icon column in every item, including items without an icon. Supply item icons through their icon slot.iconsbooleanfalse—
inlineDisplays the menu in the page flow without an overlay or trigger, for an embedded list of actions.inlinebooleanfalse—
openWhether the menu is open initially or when set externally.openbooleanfalse—
placementPositions an associated menu relative to its trigger.placementstringbottom-start—
sizesm sizes the menu to a small trigger, such as an xs or sm button: smallest text, short rows, a small check, and a narrower minimum width. md is the standard size.sizekeywordmdsm, md

Framework props

NameTypeDefaultAllowed values
density"comfortable" | "compact"comfortablecomfortable, compact
forstring—
iconsbooleanfalse—
inlinebooleanfalse—
openbooleanfalse—
placementstringbottom-start—
size"sm" | "md"mdsm, md

Events

NameDetailDescription
closeobject
  • open: boolean (required)
  • reason: keyword (required)
  • trigger: keyword (required)
Emitted when the component transitions to a closed state.
openobject
  • open: boolean (required)
  • reason: keyword (required)
  • trigger: keyword (required)
Emitted when the component transitions to an open state.
selectobject
  • value: string (required)
  • checked: boolean
  • trigger: keyword (required)
Emitted when a selectable option becomes active.

Slots

NamePurpose
defaultDefault child content.

Design tokens

Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.

Component tokens

TokenDeclared valuesFallback
--ui-menu-border—var(--ui-overlay-border, var(--ui-border-strong)) | color-mix(in oklab, var(--ui-overlay-border, var(--ui-border-strong)) calc((1 - var(--_ui-material-active)) * 100%), transparent)
--ui-menu-radius—var(--ui-overlay-radius, var(--ui-radius-lg))
--ui-menu-shadow—var(--ui-overlay-shadow, var(--ui-elevation-overlay, var(--ui-shadow-lg))) | var( --ui-overlay-shadow, 0 calc(var(--_ui-material-separation) * var(--_ui-material-scale, 0.28) * 8px) calc( var(--_ui-material-separation) * var(--_ui-material-scale, 0.28) * 24px ) color-mix(in oklab, var(--ui-text-primary) 15%, transparent), var(--ui-material-glow, 0 0 transparent), inset 0 0 0 calc(var(--_ui-material-active) * var(--ui-border-width)) var(--ui-material-edge, var(--ui-border-strong)), var(--ui-elevation-overlay, var(--ui-shadow-lg)) )
--ui-menu-surface—var(--ui-overlay-surface, var(--ui-surface-elevated)) | var(--_ui-material-underpaint, var(--ui-material-content, var(--ui-overlay-surface, var(--ui-surface-elevated))))
--ui-menu-viewport-gap—var(--ui-space-2)

Shared tokens

TokenFallback
--ui-border-divider—
--ui-border-strong—
--ui-border-width—
--ui-elevation-overlayvar(--ui-shadow-lg)
--ui-font-size-sm—
--ui-font-size-xs—
--ui-glassiness0
--ui-material-contentvar(--ui-overlay-surface, var(--ui-surface-elevated))
--ui-material-edgevar(--ui-border-strong)
--ui-material-glassvar(--ui-surface-elevated)
--ui-material-glow0 0 transparent
--ui-material-highlightvar(--ui-surface-highlight, linear-gradient(transparent, transparent))
--ui-motion-ease—
--ui-motion-fast—
--ui-overlay-bordervar(--ui-border-strong)
--ui-overlay-radiusvar(--ui-radius-lg)
--ui-overlay-shadowvar(--ui-elevation-overlay, var(--ui-shadow-lg)) | 0 calc(var(--_ui-material-separation) * var(--_ui-material-scale, 0.28) * 8px) calc( var(--_ui-material-separation) * var(--_ui-material-scale, 0.28) * 24px ) color-mix(in oklab, var(--ui-text-primary) 15%, transparent), var(--ui-material-glow, 0 0 transparent), inset 0 0 0 calc(var(--_ui-material-active) * var(--ui-border-width)) var(--ui-material-edge, var(--ui-border-strong)), var(--ui-elevation-overlay, var(--ui-shadow-lg))
--ui-overlay-surfacevar(--ui-surface-elevated)
--ui-radius-lg—
--ui-radius-sm—
--ui-shadow-lg—
--ui-space-0-5—
--ui-space-1—
--ui-space-2—
--ui-surface-elevated—
--ui-surface-highlightlinear-gradient(transparent, transparent)
--ui-surface-separation0
--ui-text-primary—

Menu Group API

Use a named group to organize related commands or choices within a Menu or Context Menu.

Package: @threadlabs/looma · Native root: div

Attributes

NameDescriptionPropertyTypeDefaultAllowed values
labelVisible heading and accessible name for this group of menu items.labelstring—

Framework props

NameTypeDefaultAllowed values
labelstring—

Events

No events.

Slots

NamePurpose
defaultDefault child content.

Design tokens

Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.

Component tokens

No component tokens.

Shared tokens

TokenFallback
--ui-font-semibold—
--ui-font-size-sm—
--ui-space-1—
--ui-space-2—
--ui-space-3—
--ui-text-muted—

Menu Item API

Use an item inside a Menu or Context Menu.

Package: @threadlabs/looma · Native root: div

Attributes

NameDescriptionPropertyTypeDefaultAllowed values
checkedInitial or externally controlled checked state of a checkbox or radio item.checkedbooleanfalse—
disabledMakes the item unavailable: it is faded, skipped by the arrow keys, and choosing it does nothing. It stays visible and is announced as disabled.disabledbooleanfalse—
hrefA destination makes an action item a real link, with native navigation and link context actions.hrefstring—
toneDanger identifies a destructive action without changing its behavior.tonekeywordneutralneutral, danger
typeAn action closes its menu; a checkbox toggles, and a radio chooses one item within its nearest Menu Group. Checkable items keep the menu open.typekeywordactionaction, checkbox, radio
valueThe value the enclosing menu reports in its select event when this item is chosen. Give each item a distinct value so one handler can tell them apart.valuestring—

Framework props

NameTypeDefaultAllowed values
checkedbooleanfalse—
disabledbooleanfalse—
hrefstring—
tone"neutral" | "danger"neutralneutral, danger
type"action" | "checkbox" | "radio"actionaction, checkbox, radio
valuestring—

Events

NameDetailDescription
changeobject
  • checked: boolean (required)
  • value: string (required)
  • type: keyword (required)
  • trigger: keyword (required)
Emitted when a toggleable control changes checked state.

Slots

NamePurpose
iconNamed content for the icon region.
defaultDefault child content.
shortcutNamed content for the shortcut region.

Design tokens

Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.

Component tokens

TokenDeclared valuesFallback
--ui-menu-item-font-size—var(--ui-font-size-md)
--ui-menu-item-hover-surface—var(--ui-accent-subtle)
--ui-menu-item-min-block-size—var(--ui-control-size-md, 2.5rem)
--ui-menu-item-padding-block—var(--ui-space-2)
--ui-menu-item-padding-inline—var(--ui-space-3)
--ui-menu-item-radius—var(--ui-selection-radius)

Shared tokens

TokenFallback
--ui-accent—
--ui-accent-subtle—
--ui-border-width—
--ui-control-border—
--ui-control-size-md2.5rem
--ui-danger-subtle-text—
--ui-disabled-border—
--ui-disabled-surface—
--ui-disabled-text—
--ui-focus-ring—
--ui-focus-width—
--ui-font-medium—
--ui-font-normal—
--ui-font-size-md—
--ui-font-size-sm—
--ui-icon-size-sm—
--ui-motion-ease—
--ui-motion-fast—
--ui-radius-sm—
--ui-selection-radius—
--ui-selection-surface—
--ui-selection-text—
--ui-space-2—
--ui-space-3—
--ui-surface—
--ui-text-primary—
--ui-text-secondary—