Skip to main content

TextMenu

A text-only dropdown trigger — plain label + chevron, no button chrome, no border, no background. Opens a PopupList panel on click.

Quick start
import { TextMenu } from "mates-ui";

TextMenu({
  label: "File",
  list: {
    items: [
      { id: "new",  label: "New",   icon: NoteAddIcon() },
      { id: "open", label: "Open…", icon: FolderOpenIcon() },
      { id: "save", label: "Save",  icon: SaveIcon() },
    ],
    onSelect: (_ids, item) => console.log(item?.id),
  },
});

Basic TextMenu

import { html, atom, renderApp } from 'mates';
import { TextMenu, Text } from 'mates-ui';
import { CloseIcon, ContentCopyIcon, ContentCutIcon, ContentPasteIcon, FolderOpenIcon, FitScreenIcon, FullscreenIcon, NoteAddIcon, RedoIcon, SaveIcon, UndoIcon, ZoomInIcon, ZoomOutIcon } from 'mates-icons';

const App = () => {
const lastFile = atom("—"); const lastEdit = atom("—"); const lastView = atom("—");
return () => html`
<x-row gap="var(--md-space-4)" align="center">
${TextMenu({ label: "File", list: { items: [
{ id: "new", label: "New", icon: NoteAddIcon() },
{ id: "open", label: "Open…", icon: FolderOpenIcon() },
{ id: "save", label: "Save", icon: SaveIcon() },
{ type: "divider", label: "" },
{ id: "close", label: "Close", icon: CloseIcon() },
], onSelect: (_ids, item) => lastFile.set(item?.id ?? "—") }, popup: { position: "bottom-start" } })}
${TextMenu({ label: "Edit", list: { items: [
{ id: "undo", label: "Undo", icon: UndoIcon() },
{ id: "redo", label: "Redo", icon: RedoIcon() },
{ type: "divider", label: "" },
{ id: "cut", label: "Cut", icon: ContentCutIcon() },
{ id: "copy", label: "Copy", icon: ContentCopyIcon() },
{ id: "paste", label: "Paste", icon: ContentPasteIcon() },
], onSelect: (_ids, item) => lastEdit.set(item?.id ?? "—") }, popup: { position: "bottom-start" } })}
${TextMenu({ label: "View", list: { items: [
{ id: "zoom-in", label: "Zoom In", icon: ZoomInIcon() },
{ id: "zoom-out", label: "Zoom Out", icon: ZoomOutIcon() },
{ id: "fit", label: "Fit Page", icon: FitScreenIcon() },
{ type: "divider", label: "" },
{ id: "fullscreen", label: "Full Screen", icon: FullscreenIcon() },
], onSelect: (_ids, item) => lastView.set(item?.id ?? "—") }, popup: { position: "bottom-start" } })}
</x-row>
`;
};

renderApp(App, document.getElementById('app'));

Sizes

import { html, atom, renderApp } from 'mates';
import { TextMenu, Text } from 'mates-ui';
import { StarIcon } from 'mates-icons';

const ITEMS = [{ id: "a", label: "Alpha", icon: StarIcon() }, { id: "b", label: "Beta", icon: StarIcon() }, { id: "c", label: "Gamma", icon: StarIcon() }];

const App = () => {
const last = atom("—");
return () => html`
<x-row gap="var(--md-space-6)" align="center">
${TextMenu({ label: "Small", size: "sm", list: { items: ITEMS, onSelect: (_ids, item) => last.set("sm → " + (item?.id ?? "—")) }, popup: { position: "bottom-start" } })}
${TextMenu({ label: "Medium", size: "md", list: { items: ITEMS, onSelect: (_ids, item) => last.set("md → " + (item?.id ?? "—")) }, popup: { position: "bottom-start" } })}
${TextMenu({ label: "Large", size: "lg", list: { items: ITEMS, onSelect: (_ids, item) => last.set("lg → " + (item?.id ?? "—")) }, popup: { position: "bottom-start" } })}
</x-row>
`;
};

renderApp(App, document.getElementById('app'));

In a toolbar / menu-bar style

import { html, atom, renderApp } from 'mates';
import { TextMenu, Text } from 'mates-ui';
import { FolderOpenIcon, InfoIcon, NoteAddIcon, RedoIcon, SaveIcon, SearchIcon, UndoIcon } from 'mates-icons';

const App = () => {
const last = atom("—");
const sel = (_ids, item) => last.set(item?.id ?? "—");
return () => html`
<x-col gap="var(--md-space-3)">
<div style="display:inline-flex;align-items:center;gap:0;border-bottom:1px solid var(--md-color-border);padding:var(--md-space-1) var(--md-space-2);border-radius:var(--md-radius-sm) var(--md-radius-sm) 0 0;background:var(--md-color-surface-raised);">
${TextMenu({ label: "File", size: "sm", list: { items: [
{ id: "new", label: "New File", icon: NoteAddIcon() },
{ id: "open", label: "Open…", icon: FolderOpenIcon() },
{ type: "divider", label: "" },
{ id: "save", label: "Save", icon: SaveIcon() },
], onSelect: sel }, popup: { position: "bottom-start" } })}
${TextMenu({ label: "Edit", size: "sm", list: { items: [
{ id: "undo", label: "Undo", icon: UndoIcon() },
{ id: "redo", label: "Redo", icon: RedoIcon() },
{ type: "divider", label: "" },
{ id: "find", label: "Find", icon: SearchIcon() },
], onSelect: sel }, popup: { position: "bottom-start" } })}
${TextMenu({ label: "Help", size: "sm", list: { items: [
{ id: "about", label: "About", icon: InfoIcon() },
], onSelect: sel }, popup: { position: "bottom-start" } })}
</div>
${Text("Last action: " + last(), { type: "body-sm", color: "muted" })}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

With groups and icons

import { html, atom, renderApp } from 'mates';
import { TextMenu, Text } from 'mates-ui';
import { CodeIcon, FormatAlignCenterIcon, FormatAlignJustifyIcon, FormatAlignLeftIcon, FormatAlignRightIcon, FormatBoldIcon, FormatItalicIcon, FormatListBulletedIcon, FormatUnderlinedIcon, HorizontalRuleIcon, ImageIcon, LinkIcon, MoodIcon, MusicNoteIcon, TableChartIcon } from 'mates-icons';

const App = () => {
const last = atom("—");
return () => html`
<x-row gap="var(--md-space-6)" align="flex-start">
${TextMenu({ label: "Insert", list: { groups: [
{ label: "Content", items: [{ id: "img", label: "Image", icon: ImageIcon() }, { id: "audio", label: "Audio", icon: MusicNoteIcon() }] },
{ label: "Structure", items: [{ id: "table", label: "Table", icon: TableChartIcon() }, { id: "list", label: "List", icon: FormatListBulletedIcon() }, { id: "code", label: "Code Block", icon: CodeIcon() }] },
{ label: "Extras", items: [{ id: "link", label: "Link", icon: LinkIcon() }, { id: "emoji", label: "Emoji", icon: MoodIcon() }, { id: "hr", label: "Divider", icon: HorizontalRuleIcon() }] },
], onSelect: (_ids, item) => last.set(item?.id ?? "—") }, popup: { position: "bottom-start", matchAnchorWidth: false } })}
${TextMenu({ label: "Format", list: { groups: [
{ label: "Text", items: [{ id: "bold", label: "Bold", icon: FormatBoldIcon() }, { id: "italic", label: "Italic", icon: FormatItalicIcon() }, { id: "underline", label: "Underline", icon: FormatUnderlinedIcon() }] },
{ label: "Alignment", items: [{ id: "left", label: "Left", icon: FormatAlignLeftIcon() }, { id: "center", label: "Center", icon: FormatAlignCenterIcon() }, { id: "right", label: "Right", icon: FormatAlignRightIcon() }, { id: "justify", label: "Justify", icon: FormatAlignJustifyIcon() }] },
], onSelect: (_ids, item) => last.set(item?.id ?? "—") }, popup: { position: "bottom-start", matchAnchorWidth: false } })}
${Text("Last: " + last(), { type: "body-sm", color: "muted" })}
</x-row>
`;
};

renderApp(App, document.getElementById('app'));

Cascading (nested sub-panels)

import { html, atom, renderApp } from 'mates';
import { CascadingPopupList, TextMenu, Text } from 'mates-ui';
import { DockToBottomIcon, DockToLeftIcon, ExpandIcon, FitScreenIcon, FullscreenIcon, ZoomInIcon, ZoomOutIcon } from 'mates-icons';

const App = () => {
const last = atom("—");
return () => html`
<x-col gap="var(--md-space-3)">
${TextMenu({
label: "View",
panel: CascadingPopupList({
items: [
{ id: "zoom", label: "Zoom", icon: ZoomInIcon(), children: [
{ id: "zoom-in", label: "Zoom In", icon: ZoomInIcon() },
{ id: "zoom-out", label: "Zoom Out", icon: ZoomOutIcon() },
{ id: "zoom-fit", label: "Fit Screen", icon: FitScreenIcon() },
],
},
{ id: "layout", label: "Layout", children: [
{ id: "dock-left", label: "Dock Left", icon: DockToLeftIcon() },
{ id: "dock-bottom", label: "Dock Bottom", icon: DockToBottomIcon() },
{ id: "expand", label: "Expand", icon: ExpandIcon() },
],
},
{ type: "divider", label: "" },
{ id: "fullscreen", label: "Full Screen", icon: FullscreenIcon() },
],
onSelect: (_ids, item) => {
last.set(item?.label ?? "—");
requestAnimationFrame(() => document.dispatchEvent(new MouseEvent("click", { bubbles: true })));
},
onEscape: () => { requestAnimationFrame(() => document.dispatchEvent(new MouseEvent("click", { bubbles: true }))); },
}),
popup: { position: "bottom-start", matchAnchorWidth: false },
})}
${Text("Last selected: " + last(), { type: "body-sm", color: "muted" })}

TextMenu

import { TextMenu } from "mates-ui";
Prop Type Default Description
attr AttrMap Attribute overrides on the root element.
classes string Extra CSS class on the root element.
classes string | string[] Extra root classes — joined and appended after built-in classes.
data DataAttrMap Root `data-*` map (`testId` → `data-testid`). `testId` prop / `attr` win over `data`.
enableRipple boolean Enables a Material ripple effect on the label when clicked.
id string Root `id` shorthand (`attr.id` wins). Form native-control `id` props are unchanged.
label* string | TemplateResult Trigger label — plain text or a rendered template.
list* PopupListItemData[] Flat or grouped items to display in the popup panel.
on OnEventMap DOM event handlers on the root element.
panel TemplateResult Custom popup panel content. When set, replaces the default PopupList rendering.
popup Partial<PopupOptions> Options forwarded to the popup (position, gap, offset, etc.).
size "sm" | "md" | "lg" "md" Font-size scale of the label.
style StyleMap Inline CSS on the root element.
testId string Data-testid attribute forwarded to the root element for testing.