Context Menu is a component that you can attach to any component to display a context menu. The menu appears on the right by default, or with a left click. On a touch device, a long press opens the context menu.
Important
Open the Context Menu by right-clicking (i.e., with a mouse), or by long-pressing (i.e., on a touch screen), a Grid row. Open in apackage com.vaadin.demo.component.contextmenu; import com.vaadin.demo.domain.DataService; import com.vaadin.demo.domain.Person; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.contextmenu.GridContextMenu; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.router.Route; import java.util.List; @Route("context-menu-basic") public class ContextMenuBasic extends Div { private List<Person> people = DataService.getPeople(5); public ContextMenuBasic() { Grid<Person> grid = new Grid(); grid.setAllRowsVisible(true); grid.setItems(people); grid.addColumn(person -> person.getFirstName()).setHeader("First name"); grid.addColumn(person -> person.getLastName()).setHeader("Last name"); grid.addColumn(person -> person.getEmail()).setHeader("Email"); grid.addColumn(person -> person.getAddress().getPhone()) .setHeader("Phone number"); // tag::snippet[] GridContextMenu<Person> menu = grid.addContextMenu(); menu.addItem("View", event -> { }); menu.addItem("Edit", event -> { }); menu.addItem("Delete", event -> { }); // end::snippet[] add(grid); } }
context-menu-basic.tsximport React, { useEffect, useRef } from 'react'; import { useSignal } from '@vaadin/hilla-react-signals'; import { ContextMenu } from '@vaadin/react-components/ContextMenu.js'; import { Grid, type GridElement } from '@vaadin/react-components/Grid.js'; import { GridColumn } from '@vaadin/react-components/GridColumn.js'; import { getPeople } from 'Frontend/demo/domain/DataService'; import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; function Example() { const gridItems = useSignal<Person[]>([]); const gridRef = useRef<GridElement>(null); useEffect(() => { getPeople({ count: 5 }).then(({ people }) => { gridItems.value = people; }); }, []); useEffect(() => { const grid = gridRef.current; if (grid) { // Workaround: Prevent opening context menu on header row. // @ts-expect-error vaadin-contextmenu isn't a GridElement event. grid.addEventListener('vaadin-contextmenu', (e) => { if (grid.getEventContext(e).section !== 'body') { e.stopPropagation(); } }); } }, [gridRef.current]); // tag::snippet[] const items = [{ text: 'View' }, { text: 'Edit' }, { text: 'Delete' }]; return ( <ContextMenu items={items}> <Grid allRowsVisible items={gridItems.value} ref={gridRef}> <GridColumn path="firstName" /> <GridColumn path="lastName" /> <GridColumn path="email" /> <GridColumn header="Phone number" path="address.phone" /> </Grid> </ContextMenu> ); // end::snippet[] }
context-menu-basic.tsimport '@vaadin/context-menu'; import '@vaadin/grid'; import { html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { Grid } from '@vaadin/grid'; import { getPeople } from 'Frontend/demo/domain/DataService'; import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('context-menu-basic') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } // tag::snippet[] @state() private items = [{ text: 'View' }, { text: 'Edit' }, { text: 'Delete' }]; // end::snippet[] @state() private gridItems: Person[] = []; protected override async firstUpdated() { this.gridItems = (await getPeople({ count: 5 })).people; } protected override render() { return html` <!-- tag::snippethtml[] --> <vaadin-context-menu .items=${this.items}> <vaadin-grid all-rows-visible .items=${this.gridItems} @vaadin-contextmenu=${this.onContextMenu} > <vaadin-grid-column path="firstName"></vaadin-grid-column> <vaadin-grid-column path="lastName"></vaadin-grid-column> <vaadin-grid-column path="email"></vaadin-grid-column> <vaadin-grid-column header="Phone number" path="address.phone"></vaadin-grid-column> </vaadin-grid> </vaadin-context-menu> <!-- end::snippethtml[] --> `; } onContextMenu(e: MouseEvent) { // Prevent opening context menu on header row. const target = e.currentTarget as Grid<Person>; if (target.getEventContext(e).section !== 'body') { e.stopPropagation(); } } }
DividersYou can use dividers to separate and group related content. Use dividers sparingly to avoid creating unnecessary visual clutter.
Important
Open the Context Menu by right-clicking (i.e., with a mouse), or by long-pressing (i.e., on a touch screen), a Grid row. Open in apackage com.vaadin.demo.component.contextmenu; import com.vaadin.demo.domain.DataService; import com.vaadin.demo.domain.Person; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.contextmenu.GridContextMenu; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Hr; import com.vaadin.flow.router.Route; import java.util.List; @Route("context-menu-dividers") public class ContextMenuDividers extends Div { private List<Person> people = DataService.getPeople(5); public ContextMenuDividers() { Grid<Person> grid = new Grid(); grid.setAllRowsVisible(true); grid.setItems(people); grid.addColumn(person -> person.getFirstName()).setHeader("First name"); grid.addColumn(person -> person.getLastName()).setHeader("Last name"); grid.addColumn(person -> person.getEmail()).setHeader("Email"); grid.addColumn(person -> person.getAddress().getPhone()) .setHeader("Phone number"); // tag::snippet[] GridContextMenu<Person> menu = grid.addContextMenu(); menu.addItem("View", event -> { }); menu.addSeparator(); menu.addItem("Edit", event -> { }); menu.addItem("Delete", event -> { }); menu.addSeparator(); menu.addItem("Email", event -> { }); menu.addItem("Call", event -> { }); // end::snippet[] add(grid); } }
context-menu-dividers.tsximport React, { useEffect, useRef } from 'react'; import { useSignal } from '@vaadin/hilla-react-signals'; import { ContextMenu } from '@vaadin/react-components/ContextMenu.js'; import { Grid, type GridElement } from '@vaadin/react-components/Grid.js'; import { GridColumn } from '@vaadin/react-components/GridColumn.js'; import { getPeople } from 'Frontend/demo/domain/DataService'; import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; function Example() { const gridItems = useSignal<Person[]>([]); const gridRef = useRef<GridElement>(null); useEffect(() => { getPeople({ count: 5 }).then(({ people }) => { gridItems.value = people; }); }, []); useEffect(() => { const grid = gridRef.current; if (grid) { // Prevent opening context menu on header row. // @ts-expect-error vaadin-contextmenu isn't a GridElement event. grid.addEventListener('vaadin-contextmenu', (e) => { if (grid.getEventContext(e).section !== 'body') { e.stopPropagation(); } }); } }, [gridRef.current]); return ( // tag::snippet[] <ContextMenu items={[ { text: 'View' }, { component: 'hr' }, { text: 'Edit' }, { text: 'Delete' }, { component: 'hr' }, { text: 'Email' }, { text: 'Call' }, ]} > <Grid allRowsVisible items={gridItems.value} ref={gridRef}> <GridColumn path="firstName" /> <GridColumn path="lastName" /> <GridColumn path="email" /> <GridColumn header="Phone number" path="address.phone" /> </Grid> </ContextMenu> // end::snippet[] ); }
context-menu-dividers.tsimport '@vaadin/context-menu'; import '@vaadin/grid'; import { html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { Grid } from '@vaadin/grid'; import { getPeople } from 'Frontend/demo/domain/DataService'; import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('context-menu-dividers') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } @state() private gridItems: Person[] = []; protected override async firstUpdated() { this.gridItems = (await getPeople({ count: 5 })).people; } protected override render() { return html` <!-- tag::snippet[] --> <vaadin-context-menu .items=${[ { text: 'View' }, { component: 'hr' }, { text: 'Edit' }, { text: 'Delete' }, { component: 'hr' }, { text: 'Email' }, { text: 'Call' }, ]} > <vaadin-grid all-rows-visible .items=${this.gridItems} @vaadin-contextmenu=${this.onContextMenu} > <vaadin-grid-column path="firstName"></vaadin-grid-column> <vaadin-grid-column path="lastName"></vaadin-grid-column> <vaadin-grid-column path="email"></vaadin-grid-column> <vaadin-grid-column header="Phone number" path="address.phone"></vaadin-grid-column> </vaadin-grid> </vaadin-context-menu> <!-- end::snippet[] --> `; } onContextMenu(e: MouseEvent) { // Prevent opening context menu on header row. const target = e.currentTarget as Grid<Person>; if (target.getEventContext(e).section !== 'body') { e.stopPropagation(); } } }
Checkable Menu Items can be used to toggle a setting on and off.
Open in apackage com.vaadin.demo.component.contextmenu; import com.vaadin.demo.domain.DataService; import com.vaadin.demo.domain.Person; import com.vaadin.flow.component.contextmenu.ContextMenu; import com.vaadin.flow.component.contextmenu.MenuItem; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.router.Route; import java.util.List; @Route("context-menu-checkable") public class ContextMenuCheckable extends Div { private final ContextMenu menu; private final Span assignee; public ContextMenuCheckable() { // tag::snippet1[] assignee = new Span(); menu = new ContextMenu(); menu.setTarget(assignee); List<Person> people = DataService.getPeople(5); for (Person person : people) { MenuItem menuItem = menu.addItem(person.getFullName(), event -> { setAssignee(person); }); menuItem.setCheckable(true); } setAssignee(people.get(0)); // end::snippet1[] Div assigneeInfo = new Div(new Span("Assignee: "), assignee); assignee.getStyle().set("font-weight", "bold"); add(assigneeInfo); } private void setAssignee(Person person) { // Update checked state of menu items menu.getItems().forEach(item -> item .setChecked(item.getText().equals(person.getFullName()))); assignee.setText(person.getFullName()); } }
context-menu-checkable.tsximport { useSignal } from '@vaadin/hilla-react-signals'; import { ContextMenu, type ContextMenuItem, type ContextMenuItemSelectedEvent, } from '@vaadin/react-components/ContextMenu.js'; function Example() { // tag::snippet[] const items = useSignal<ContextMenuItem[]>([ { text: 'Abigail Lewis', checked: true }, { text: 'Allison Torres' }, { text: 'Anna Myers' }, { text: 'Lauren Wright' }, { text: 'Tamaki Ryushi' }, ]); const selectedItem = items.value.find((item) => item.checked); function itemSelected(e: ContextMenuItemSelectedEvent) { items.value = items.value.map((item) => ({ ...item, checked: item === e.detail.value })); } return ( <ContextMenu items={items.value} onItemSelected={itemSelected}> <span> Assignee: <b>{selectedItem?.text}</b> </span> </ContextMenu> ); // end::snippet[] }
context-menu-checkable.tsimport '@vaadin/context-menu'; import { html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { ContextMenuItem, ContextMenuItemSelectedEvent } from '@vaadin/context-menu'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('context-menu-checkable') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } // tag::snippet[] @state() private items: ContextMenuItem[] = [ { text: 'Abigail Lewis', checked: true }, { text: 'Allison Torres' }, { text: 'Anna Myers' }, { text: 'Lauren Wright' }, { text: 'Tamaki Ryushi' }, ]; protected override render() { const selectedItem = this.items.find((item) => item.checked); return html` <vaadin-context-menu .items="${this.items}" @item-selected="${this.itemSelected}"> <span>Assignee: <b>${selectedItem?.text}</b></span> </vaadin-context-menu> `; } itemSelected(e: ContextMenuItemSelectedEvent) { this.items.forEach((item) => { item.checked = item === e.detail.value; }); this.items = [...this.items]; } // end::snippet[] }
Context Menu, like Menu Bar, supports multi-level sub-menus. You can use a hierarchical menu to organize a large set of options and group related items.
Important
Open the Context Menu by right-clicking (i.e., with a mouse), or by long-pressing (i.e., on a touch screen), a Grid row. Open in apackage com.vaadin.demo.component.contextmenu; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.contextmenu.GridContextMenu; import com.vaadin.flow.component.grid.contextmenu.GridMenuItem; import com.vaadin.flow.component.grid.contextmenu.GridSubMenu; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Hr; import com.vaadin.flow.router.Route; import static org.apache.commons.io.FileUtils.byteCountToDisplaySize; @Route("context-menu-hierarchical") public class ContextMenuHierarchical extends Div { public ContextMenuHierarchical() { Grid<File> grid = new Grid(); grid.setAllRowsVisible(true); grid.setItems(getFiles()); grid.addColumn(File::getName).setHeader("Name"); grid.addColumn(File::getDisplaySize).setHeader("Size"); // tag::snippet[] GridContextMenu<File> menu = grid.addContextMenu(); // end::snippet[] menu.addItem("Preview", event -> { }); menu.addItem("Edit", event -> { }); menu.addSeparator(); // tag::snippet[] GridMenuItem<File> export = menu.addItem("Export"); GridSubMenu<File> exportSubMenu = export.getSubMenu(); exportSubMenu.addItem("Portable Document Format (.pdf)", event -> { }); exportSubMenu.addItem("Rich Text Format (.rtf)", event -> { }); exportSubMenu.addItem("Plain text (.txt)", event -> { }); // end::snippet[] GridMenuItem<File> share = menu.addItem("Share"); GridSubMenu<File> shareSubMenu = share.getSubMenu(); shareSubMenu.addItem("Copy link", event -> { }); shareSubMenu.addItem("Email", event -> { }); menu.addSeparator(); menu.addItem("Delete", event -> { }); add(grid); } private File[] getFiles() { return new File[] { new File("Annual Report.docx", 25165824), new File("Financials.xlsx", 44040192) }; } private class File { private String name; private long size; File(String name, long size) { this.name = name; this.size = size; } public String getName() { return name; } public long getSize() { return size; } public String getDisplaySize() { return byteCountToDisplaySize(size); } } }
context-menu-hierarchical.tsximport React, { useEffect, useRef } from 'react'; import { ContextMenu } from '@vaadin/react-components/ContextMenu.js'; import { Grid, type GridElement } from '@vaadin/react-components/Grid.js'; import { GridColumn } from '@vaadin/react-components/GridColumn.js'; interface FileItem { name: string; size: string; } function Example() { const gridRef = useRef<GridElement>(null); useEffect(() => { const grid = gridRef.current; if (grid) { // Prevent opening context menu on header row. // @ts-expect-error vaadin-contextmenu isn't a GridElement event. grid.addEventListener('vaadin-contextmenu', (e) => { if (grid.getEventContext(e).section !== 'body') { e.stopPropagation(); } }); } }, [gridRef.current]); // tag::snippet[] const items = [ { text: 'Preview' }, { text: 'Edit' }, { component: 'hr' }, { text: 'Export', children: [ { text: 'Portable Document Format (.pdf)' }, { text: 'Rich Text Format (.rtf)' }, { text: 'Plain text (.txt)' }, ], }, { text: 'Share', children: [{ text: 'Copy link' }, { text: 'Email' }] }, { component: 'hr' }, { text: 'Delete' }, ]; const gridItems: FileItem[] = [ { name: 'Annual Report.docx', size: '24 MB' }, { name: 'Financials.xlsx', size: '42 MB' }, ]; return ( <ContextMenu items={items}> <Grid allRowsVisible items={gridItems} ref={gridRef}> <GridColumn path="name" /> <GridColumn path="size" /> </Grid> </ContextMenu> ); // end::snippet[] }
context-menu-hierarchical.tsimport '@vaadin/context-menu'; import '@vaadin/grid'; import { html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { Grid } from '@vaadin/grid'; import { applyTheme } from 'Frontend/generated/theme'; interface FileItem { name: string; size: string; } @customElement('context-menu-hierarchical') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } // tag::snippet[] @state() private items = [ { text: 'Preview' }, { text: 'Edit' }, { component: 'hr' }, { text: 'Export', children: [ { text: 'Portable Document Format (.pdf)' }, { text: 'Rich Text Format (.rtf)' }, { text: 'Plain text (.txt)' }, ], }, { text: 'Share', children: [{ text: 'Copy link' }, { text: 'Email' }] }, { component: 'hr' }, { text: 'Delete' }, ]; // end::snippet[] @state() private gridItems: FileItem[] = [ { name: 'Annual Report.docx', size: '24 MB' }, { name: 'Financials.xlsx', size: '42 MB' }, ]; protected override render() { return html` <!-- tag::snippethtml[] --> <vaadin-context-menu .items=${this.items}> <vaadin-grid all-rows-visible .items=${this.gridItems} @vaadin-contextmenu=${this.onContextMenu} > <vaadin-grid-column path="name"></vaadin-grid-column> <vaadin-grid-column path="size"></vaadin-grid-column> </vaadin-grid> </vaadin-context-menu> <!-- end::snippethtml[] --> `; } onContextMenu(e: MouseEvent) { // Prevent opening context menu on header row. const target = e.currentTarget as Grid<FileItem>; if (target.getEventContext(e).section !== 'body') { e.stopPropagation(); } } }
Custom ItemsYou can customize the items to include more than a single line of text.
Important
Open the Context Menu by right-clicking (i.e., with a mouse), or by long-pressing (i.e., on a touch screen), a Grid row. Open in apackage com.vaadin.demo.component.contextmenu; import com.vaadin.demo.domain.DataService; import com.vaadin.demo.domain.Person; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.avatar.Avatar; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.contextmenu.GridContextMenu; import com.vaadin.flow.component.grid.contextmenu.GridMenuItem; import com.vaadin.flow.component.grid.contextmenu.GridSubMenu; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Hr; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.Route; import java.util.List; import java.util.Random; @Route("context-menu-presentation") public class ContextMenuPresentation extends Div { private List<Person> people = DataService.getPeople(10); private Random random = new Random(1); public ContextMenuPresentation() { Grid<Person> grid = new Grid(); grid.setAllRowsVisible(true); grid.setItems(people.subList(0, 5)); grid.addColumn(person -> person.getFullName()).setHeader("Applicant"); grid.addColumn(person -> person.getEmail()).setHeader("Email"); grid.addColumn(person -> person.getAddress().getPhone()) .setHeader("Phone number"); // tag::snippet1[] GridContextMenu<Person> menu = grid.addContextMenu(); // end::snippet1[] GridMenuItem<Person> open = menu.addItem("Open", event -> { }); open.addComponentAsFirst(createIcon(VaadinIcon.FILE_SEARCH)); GridMenuItem<Person> assign = menu.addItem("Assign"); assign.addComponentAsFirst(createIcon(VaadinIcon.USER_CHECK)); GridSubMenu<Person> assignSubMenu = assign.getSubMenu(); people.subList(5, 10).forEach(person -> { assignSubMenu.addItem(createPersonItem(person), event -> { }); }); menu.addSeparator(); GridMenuItem<Person> delete = menu.addItem("Delete", event -> { }); delete.addComponentAsFirst(createIcon(VaadinIcon.TRASH)); add(grid); } private Component createIcon(VaadinIcon vaadinIcon) { Icon icon = vaadinIcon.create(); icon.getStyle().set("color", "var(--lumo-secondary-text-color)") .set("margin-inline-end", "var(--lumo-space-s") .set("padding", "var(--lumo-space-xs"); return icon; } private Component createPersonItem(Person person) { Avatar avatar = new Avatar(); avatar.setImage(person.getPictureUrl()); avatar.setName(person.getFirstName()); Span name = new Span(person.getFullName()); Span apps = new Span(getApplicationCount()); apps.getStyle().set("color", "var(--lumo-secondary-text-color)") .set("font-size", "var(--lumo-font-size-s)"); VerticalLayout verticalLayout = new VerticalLayout(name, apps); verticalLayout.setPadding(false); verticalLayout.setSpacing(false); HorizontalLayout horizontalLayout = new HorizontalLayout(avatar, verticalLayout); horizontalLayout.setAlignItems(FlexComponent.Alignment.CENTER); horizontalLayout.getStyle().set("line-height", "var(--lumo-line-height-m)"); return horizontalLayout; } private String getApplicationCount() { // Randomised dummy data return random.nextInt(20) + 1 + " applications"; } }
context-menu-presentation.tsximport '@vaadin/icons'; import React, { useEffect, useRef } from 'react'; import { useSignal } from '@vaadin/hilla-react-signals'; import { Avatar } from '@vaadin/react-components/Avatar.js'; import { ContextMenu, type ContextMenuItem } from '@vaadin/react-components/ContextMenu.js'; import { Grid, type GridElement } from '@vaadin/react-components/Grid.js'; import { GridColumn } from '@vaadin/react-components/GridColumn.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; import { Icon } from '@vaadin/react-components/Icon.js'; import { VerticalLayout } from '@vaadin/react-components/VerticalLayout.js'; import { getPeople } from 'Frontend/demo/domain/DataService'; import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; function Item({ person }: { person: Person }) { return ( <HorizontalLayout style={{ alignItems: 'center', lineHeight: 'var(--lumo-line-height-m)' }} theme="spacing" > <Avatar img={person.pictureUrl} name={`${person.firstName} ${person.lastName}`} /> <VerticalLayout> <span> {person.firstName} {person.lastName} </span> <span style={{ color: 'var(--lumo-secondary-text-color)', fontSize: 'var(--lumo-font-size-s)' }} > {Math.floor(Math.random() * 20) + 1} applications </span> </VerticalLayout> </HorizontalLayout> ); } function createItem(iconName: string, text: string) { return ( <> <Icon icon={iconName} style={{ color: 'var(--lumo-secondary-text-color)', marginInlineEnd: 'var(--lumo-space-s)', padding: 'var(--lumo-space-xs)', }} /> {text} </> ); } function renderApplicant({ item }: { item: Person }) { return ( <span> {item.firstName} {item.lastName} </span> ); } function Example() { const gridItems = useSignal<Person[]>([]); const items = useSignal<ContextMenuItem[]>([]); const gridRef = useRef<GridElement>(null); useEffect(() => { getPeople({ count: 5 }).then(({ people }) => { gridItems.value = people; // tag::snippet[] const contextMenuItems: ContextMenuItem[] = [ { component: createItem('vaadin:file-search', 'Open') }, { component: createItem('vaadin:user-check', 'Assign'), children: [ { component: <Item person={people[0]} />, }, { component: <Item person={people[1]} />, }, { component: <Item person={people[2]} />, }, { component: <Item person={people[3]} />, }, { component: <Item person={people[4]} />, }, ], }, { component: 'hr' }, { component: createItem('vaadin:trash', 'Delete') }, ]; items.value = contextMenuItems; // end::snippet[] }); }, []); useEffect(() => { const grid = gridRef.current; if (grid) { // Workaround: Prevent opening context menu on header row. // @ts-expect-error vaadin-contextmenu isn't a GridElement event. grid.addEventListener('vaadin-contextmenu', (e) => { if (grid.getEventContext(e).section !== 'body') { e.stopPropagation(); } }); } }, [gridRef.current]); // tag::snippet[] return ( <ContextMenu items={items.value}> <Grid allRowsVisible items={gridItems.value} ref={gridRef}> <GridColumn header="Applicant" renderer={renderApplicant} /> <GridColumn path="email" /> <GridColumn header="Phone number" path="address.phone" /> </Grid> </ContextMenu> ); // end::snippet[] }
context-menu-presentation.tsimport '@vaadin/avatar'; import '@vaadin/context-menu'; import '@vaadin/grid'; import '@vaadin/horizontal-layout'; import '@vaadin/icon'; import '@vaadin/icons'; import '@vaadin/vertical-layout'; import { html, LitElement, render } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { ContextMenuItem } from '@vaadin/context-menu'; import type { Grid } from '@vaadin/grid'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; import { getPeople } from 'Frontend/demo/domain/DataService'; import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('context-menu-presentation') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } @state() private gridItems: Person[] = []; @state() private items: ContextMenuItem[] | undefined; // tag::snippet[] protected override async firstUpdated() { const { people } = await getPeople({ count: 10 }); this.gridItems = people.slice(0, 5); const itemsArray = this.createItemsArray(people.slice(5, 10)); this.items = [ { component: this.createItem('vaadin:file-search', 'Open') }, { component: this.createItem('vaadin:user-check', 'Assign'), children: [ { component: itemsArray[0] }, { component: itemsArray[1] }, { component: itemsArray[2] }, { component: itemsArray[3] }, { component: itemsArray[4] }, ], }, { component: 'hr' }, { component: this.createItem('vaadin:trash', 'Delete') }, ]; } // end::snippet[] protected override render() { return html` <!-- tag::snippethtml[] --> <vaadin-context-menu .items=${this.items}> <vaadin-grid all-rows-visible .items=${this.gridItems} @vaadin-contextmenu=${this.onContextMenu} > <vaadin-grid-column header="Applicant" ${columnBodyRenderer<Person>( (person) => html`<span>${person.firstName} ${person.lastName}</span>`, [] )} ></vaadin-grid-column> <vaadin-grid-column path="email"></vaadin-grid-column> <vaadin-grid-column header="Phone number" path="address.phone"></vaadin-grid-column> </vaadin-grid> </vaadin-context-menu> <!-- end::snippethtml[] --> `; } createItemsArray(people: Person[]) { return people.map((person, index) => { const item = document.createElement('vaadin-context-menu-item'); if (index === 0) { item.setAttribute('selected', ''); } render( html` <vaadin-horizontal-layout style="align-items: center; line-height: var(--lumo-line-height-m)" theme="spacing" > <vaadin-avatar .img=${person.pictureUrl} .name=${`${person.firstName} ${person.lastName}`} ></vaadin-avatar> <vaadin-vertical-layout> <span> ${person.firstName} ${person.lastName} </span> <span style="color: var(--lumo-secondary-text-color); font-size: var(--lumo-font-size-s);" > ${Math.floor(Math.random() * 20) + 1} applications </span> </vaadin-vertical-layout> </vaadin-horizontal-layout> `, item ); return item; }); } createItem(iconName: string, text: string) { const item = document.createElement('vaadin-context-menu-item'); const icon = document.createElement('vaadin-icon'); icon.style.color = 'var(--lumo-secondary-text-color)'; icon.style.marginInlineEnd = 'var(--lumo-space-s)'; icon.style.padding = 'var(--lumo-space-xs)'; icon.setAttribute('icon', iconName); item.appendChild(icon); if (text) { item.appendChild(document.createTextNode(text)); } return item; } onContextMenu(e: MouseEvent) { // Prevent opening context menu on header row. const target = e.currentTarget as Grid<Person>; if (target.getEventContext(e).section !== 'body') { e.stopPropagation(); } } }
Individual menu items can be styled by applying custom class names to them, and writing CSS style blocks targeting those class names.
Open in apackage com.vaadin.demo.component.contextmenu; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.contextmenu.ContextMenu; import com.vaadin.flow.component.contextmenu.MenuItem; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.router.Route; @Route("context-menu-classname") public class ContextMenuClassname extends Div { public ContextMenuClassname() { Button button = new Button("Actions"); ContextMenu menu = new ContextMenu(); menu.setTarget(button); menu.setOpenOnClick(true); menu.addItem("Share"); menu.addItem("Duplicate"); // tag::snippet[] MenuItem item = menu.addItem("Delete"); item.setClassName("text-error"); // end::snippet[] add(button); } }
context-menu-classname.tsximport { useSignal } from '@vaadin/hilla-react-signals'; import { Button } from '@vaadin/react-components/Button.js'; import { ContextMenu, type ContextMenuItem } from '@vaadin/react-components/ContextMenu.js'; function Example() { // tag::snippet[] const items = useSignal<ContextMenuItem[]>([ { text: 'Share' }, { text: 'Duplicate' }, { text: 'Delete', className: 'text-error' }, ]); // end::snippet[] return ( <ContextMenu items={items.value} openOn="click"> <Button>Actions</Button> </ContextMenu> ); }
context-menu-classname.tsimport '@vaadin/button'; import '@vaadin/context-menu'; import { html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { ContextMenuItem } from '@vaadin/context-menu'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('context-menu-classname') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } // tag::snippet[] @state() private items: ContextMenuItem[] = [ { text: 'Share' }, { text: 'Duplicate' }, { text: 'Delete', className: 'text-error' }, ]; // end::snippet[] protected override render() { return html` <vaadin-context-menu .items="${this.items}" open-on="click"> <vaadin-button>Actions</vaadin-button> </vaadin-context-menu> `; } }
Note
Theme Names, Not Class Names
In versions prior to 24.3, theme names must be used instead (i.e.,theme
property / addThemeNames
Java method). The CSS syntax for targeting a theme name is [theme~="custom-theme"]
You can disable menu items to show that they are unavailable.
Important
Open the Context Menu by right-clicking (i.e., with a mouse), or by long-pressing (i.e., on a touch screen), a Grid row. Open in apackage com.vaadin.demo.component.contextmenu; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.contextmenu.GridContextMenu; import com.vaadin.flow.component.grid.contextmenu.GridMenuItem; import com.vaadin.flow.component.grid.contextmenu.GridSubMenu; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Hr; import com.vaadin.flow.router.Route; import static org.apache.commons.io.FileUtils.byteCountToDisplaySize; @Route("context-menu-disabled") public class ContextMenuDisabled extends Div { public ContextMenuDisabled() { Grid<File> grid = new Grid(); grid.setAllRowsVisible(true); grid.setItems(getFiles()); grid.addColumn(File::getName).setHeader("Name"); grid.addColumn(File::getDisplaySize).setHeader("Size"); // tag::snippet1[] GridContextMenu<File> menu = grid.addContextMenu(); // end::snippet1[] menu.addItem("Preview", event -> { }); menu.addItem("Edit", event -> { }); menu.addSeparator(); GridMenuItem<File> export = menu.addItem("Export"); GridSubMenu<File> exportSubMenu = export.getSubMenu(); GridMenuItem<File> exportPDF = exportSubMenu .addItem("Portable Document Format (.pdf)", event -> { }); exportPDF.setEnabled(false); exportSubMenu.addItem("Rich Text Format (.rtf)", event -> { }); exportSubMenu.addItem("Plain text (.txt)", event -> { }); GridMenuItem<File> share = menu.addItem("Share"); GridSubMenu<File> shareSubMenu = share.getSubMenu(); shareSubMenu.addItem("Copy link", event -> { }); shareSubMenu.addItem("Email", event -> { }); menu.addSeparator(); GridMenuItem<File> delete = menu.addItem("Delete", event -> { }); delete.setEnabled(false); add(grid); } private File[] getFiles() { return new File[] { new File("Annual Report.pdf", 25165824), new File("Financials.pdf", 44040192) }; } private class File { private String name; private long size; File(String name, long size) { this.name = name; this.size = size; } public String getName() { return name; } public long getSize() { return size; } public String getDisplaySize() { return byteCountToDisplaySize(size); } } }
context-menu-disabled.tsximport React, { useEffect, useRef } from 'react'; import { ContextMenu } from '@vaadin/react-components/ContextMenu.js'; import { Grid, type GridElement } from '@vaadin/react-components/Grid.js'; import { GridColumn } from '@vaadin/react-components/GridColumn.js'; interface FileItem { name: string; size: string; } function Example() { const gridRef = useRef<GridElement>(null); useEffect(() => { const grid = gridRef.current; if (grid) { // Workaround: Prevent opening context menu on header row. // @ts-expect-error vaadin-contextmenu isn't a GridElement event. grid.addEventListener('vaadin-contextmenu', (e) => { if (grid.getEventContext(e).section !== 'body') { e.stopPropagation(); } }); } }, [gridRef.current]); // tag::snippet[] const items = [ { text: 'Preview' }, { text: 'Edit' }, { component: 'hr' }, { text: 'Export', children: [ { text: 'Portable Document Format (.pdf)', disabled: true }, { text: 'Rich Text Format (.rtf)' }, { text: 'Plain text (.txt)' }, ], }, { text: 'Share', children: [{ text: 'Copy link' }, { text: 'Email' }] }, { component: 'hr' }, { text: 'Delete', disabled: true }, ]; const gridItems: FileItem[] = [ { name: 'Annual Report.pdf', size: '24 MB' }, { name: 'Financials.pdf', size: '42 MB' }, ]; return ( <ContextMenu items={items}> <Grid allRowsVisible items={gridItems} ref={gridRef}> <GridColumn path="name" /> <GridColumn path="size" /> </Grid> </ContextMenu> ); // end::snippet[] }
context-menu-disabled.tsimport '@vaadin/context-menu'; import '@vaadin/grid'; import { html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { Grid } from '@vaadin/grid'; import { applyTheme } from 'Frontend/generated/theme'; interface FileItem { name: string; size: string; } @customElement('context-menu-disabled') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } // tag::snippet[] @state() private items = [ { text: 'Preview' }, { text: 'Edit' }, { component: 'hr' }, { text: 'Export', children: [ { text: 'Portable Document Format (.pdf)', disabled: true }, { text: 'Rich Text Format (.rtf)' }, { text: 'Plain text (.txt)' }, ], }, { text: 'Share', children: [{ text: 'Copy link' }, { text: 'Email' }] }, { component: 'hr' }, { text: 'Delete', disabled: true }, ]; // end::snippet[] @state() private gridItems: FileItem[] = [ { name: 'Annual Report.pdf', size: '24 MB' }, { name: 'Financials.pdf', size: '42 MB' }, ]; protected override render() { return html` <!-- tag::snippethtml[] --> <vaadin-context-menu .items=${this.items}> <vaadin-grid all-rows-visible .items=${this.gridItems} @vaadin-contextmenu=${this.onContextMenu} > <vaadin-grid-column path="name"></vaadin-grid-column> <vaadin-grid-column path="size"></vaadin-grid-column> </vaadin-grid> </vaadin-context-menu> <!-- end::snippethtml[] --> `; } onContextMenu(e: MouseEvent) { // Prevent opening context menu on header row. const target = e.currentTarget as Grid<FileItem>; if (target.getEventContext(e).section !== 'body') { e.stopPropagation(); } } }
Disable on Click (Flow)To prevent duplicate clicks while the server is processing a request, call the setDisableOnClick(true)
method on a menu item instance to disable immediately that menu item on the client-side when it’s clicked.
package com.vaadin.demo.component.contextmenu; import com.vaadin.flow.component.contextmenu.ContextMenu; import com.vaadin.flow.component.contextmenu.MenuItem; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.router.Route; @Route("context-menu-disable-on-click") public class ContextMenuDisableOnClick extends Div { public ContextMenuDisableOnClick() { Paragraph paragraph = new Paragraph(); paragraph.setText( "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."); add(paragraph); ContextMenu menu = new ContextMenu(paragraph); // tag::snippet[] MenuItem summarize = menu.addItem("Summarize with AI"); summarize.setDisableOnClick(true); // end::snippet[] summarize.setKeepOpen(true); summarize.addClickListener(event -> { // Simulate long-running operation try { Thread.sleep(2000); } catch (InterruptedException e) { // ignore } summarize.setEnabled(true); paragraph.setText("Lorem ipsum dolor sit amet."); }); } }
Left-ClickYou can use left-click to open Context Menu in situations where left-click doesn’t have any other function (e.g., a Grid without selection support).
Important
Open the Context Menu by clicking a Grid row. Open in apackage com.vaadin.demo.component.contextmenu; import com.vaadin.demo.domain.DataService; import com.vaadin.demo.domain.Person; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.contextmenu.GridContextMenu; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.router.Route; import java.util.List; @Route("context-menu-left-click") public class ContextMenuLeftClick extends Div { private List<Person> people = DataService.getPeople(5); public ContextMenuLeftClick() { Grid<Person> grid = new Grid(); grid.setAllRowsVisible(true); grid.setItems(people); grid.addColumn(person -> person.getFirstName()).setHeader("First name"); grid.addColumn(person -> person.getLastName()).setHeader("Last name"); grid.addColumn(person -> person.getEmail()).setHeader("Email"); grid.addColumn(person -> person.getAddress().getPhone()) .setHeader("Phone number"); // tag::snippet[] GridContextMenu<Person> menu = grid.addContextMenu(); menu.setOpenOnClick(true); menu.addItem("View", event -> { }); menu.addItem("Edit", event -> { }); menu.addItem("Delete", event -> { }); // end::snippet[] add(grid); } }
context-menu-left-click.tsximport React, { useEffect, useRef } from 'react'; import { useSignal } from '@vaadin/hilla-react-signals'; import { ContextMenu } from '@vaadin/react-components/ContextMenu.js'; import { Grid, type GridElement } from '@vaadin/react-components/Grid.js'; import { GridColumn } from '@vaadin/react-components/GridColumn.js'; import { getPeople } from 'Frontend/demo/domain/DataService'; import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; function Example() { const items = useSignal([{ text: 'View' }, { text: 'Edit' }, { text: 'Delete' }]); const gridItems = useSignal<Person[]>([]); const gridRef = useRef<GridElement>(null); useEffect(() => { const grid = gridRef.current; if (grid) { // Workaround: Prevent opening context menu on header row. grid.addEventListener('click', (e) => { if (grid.getEventContext(e).section !== 'body') { e.stopPropagation(); } }); } }, [gridRef.current]); useEffect(() => { getPeople({ count: 5 }).then(({ people }) => { gridItems.value = people; }); }, []); // tag::snippet[] return ( <ContextMenu openOn="click" items={items.value}> <Grid allRowsVisible items={gridItems.value} ref={gridRef}> <GridColumn path="firstName" /> <GridColumn path="lastName" /> <GridColumn path="email" /> <GridColumn header="Phone number" path="address.phone" /> </Grid> </ContextMenu> ); // end::snippet[] }
context-menu-left-click.tsimport '@vaadin/context-menu'; import '@vaadin/grid'; import { html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { ContextMenuOpenedChangedEvent } from '@vaadin/context-menu'; import type { Grid } from '@vaadin/grid'; import { getPeople } from 'Frontend/demo/domain/DataService'; import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('context-menu-left-click') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } // tag::snippet[] @state() private items = [{ text: 'View' }, { text: 'Edit' }, { text: 'Delete' }]; // end::snippet[] @state() private gridItems: Person[] = []; private contextMenuOpened?: boolean; private onClick = (e: MouseEvent) => { // Prevent opening context menu on header row click. const target = e.currentTarget as Grid<Person>; if (!this.contextMenuOpened && target.getEventContext(e).section !== 'body') { e.stopPropagation(); } }; protected override async firstUpdated() { this.gridItems = (await getPeople({ count: 5 })).people; } protected override render() { return html` <!-- tag::snippethtml[] --> <vaadin-context-menu open-on="click" .items=${this.items} @opened-changed="${(event: ContextMenuOpenedChangedEvent) => { this.contextMenuOpened = event.detail.value; }}" > <vaadin-grid all-rows-visible .items=${this.gridItems} @click=${this.onClick}> <vaadin-grid-column path="firstName"></vaadin-grid-column> <vaadin-grid-column path="lastName"></vaadin-grid-column> <vaadin-grid-column path="email"></vaadin-grid-column> <vaadin-grid-column header="Phone number" path="address.phone"></vaadin-grid-column> </vaadin-grid> </vaadin-context-menu> <!-- end::snippethtml[] --> `; } }
Custom Item DataContext Menu allows you to associate custom data with menu items. This can be useful for storing additional information about the item, such as an item type or a value. The data can then be used to trigger actions when an item is selected.
Open in apackage com.vaadin.demo.component.contextmenu; import com.vaadin.flow.component.contextmenu.ContextMenu; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.H1; import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.router.Route; @Route("context-menu-custom-item-data") public class ContextMenuCustomItemData extends Div { public ContextMenuCustomItemData() { // tag::snippet[] Div wrapper = new Div( new H1("Context Menu"), new Paragraph("Menu Bar is a horizontal button bar with hierarchical drop-down menus.") ); ContextMenu menu = new ContextMenu(); menu.setTarget(wrapper); menu.addItem("Copy as plain text", event -> { // Provide a custom value by adding a click listener that holds a reference to that value String value = "Menu Bar\n\nMenu Bar is a horizontal button bar with hierarchical drop-down menus."; copyToClipboard(value); }); menu.addItem("Copy as HTML", event -> { String value = "<h1>Menu Bar</h1><p>Menu Bar is a horizontal button bar with hierarchical drop-down menus.</p>"; copyToClipboard(value); }); menu.addItem("Copy as Markdown", event -> { String value = "# Menu Bar\n\nMenu Bar is a horizontal button bar with hierarchical drop-down menus."; copyToClipboard(value); }); // end::snippet[] add(wrapper); } private void copyToClipboard(String value) { getUI().ifPresent(ui -> ui.getPage().executeJs("window.navigator.clipboard.writeText($0);", value)); } }
context-menu-custom-item-data.tsximport React from 'react'; import { ContextMenu, type ContextMenuItem } from '@vaadin/react-components'; function Example() { // tag::snippet[] const items: Array<ContextMenuItem<{ value: string }>> = [ { text: 'Copy as plain text', value: 'Context Menu\n\nContext Menu is a component that you can attach to any component to display a context menu.', }, { text: 'Copy as HTML', value: '<h1>Context Menu</h1><p>Context Menu is a component that you can attach to any component to display a context menu.</p>', }, { text: 'Copy as Markdown', value: '# Context Menu\n\nContext Menu is a component that you can attach to any component to display a context menu.', }, ]; return ( <ContextMenu items={items} onItemSelected={(event) => { const value = event.detail.value.value; if (value) { navigator.clipboard.writeText(value); } }} > <h1>Context Menu</h1> <p> Context Menu is a component that you can attach to any component to display a context menu. </p> </ContextMenu> ); // end::snippet[] }
context-menu-custom-item-data.tsimport '@vaadin/context-menu'; import { html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { ContextMenuItem, ContextMenuItemSelectedEvent } from '@vaadin/context-menu'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('context-menu-custom-item-data') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } // tag::snippet[] @state() private items: Array<ContextMenuItem<{ value: string }>> = [ { text: 'Copy as plain text', value: 'Context Menu\n\nContext Menu is a component that you can attach to any component to display a context menu.', }, { text: 'Copy as HTML', value: '<h1>Context Menu</h1><p>Context Menu is a component that you can attach to any component to display a context menu.</p>', }, { text: 'Copy as Markdown', value: '# Context Menu\n\nContext Menu is a component that you can attach to any component to display a context menu.', }, ]; protected override render() { return html` <vaadin-context-menu .items="${this.items}" @item-selected="${this.itemSelected}"> <h1>Context Menu</h1> <p> Context Menu is a component that you can attach to any component to display a context menu. </p> </vaadin-context-menu> `; } itemSelected(e: ContextMenuItemSelectedEvent<ContextMenuItem<{ value?: string }>>) { const value = e.detail.value.value; if (value) { navigator.clipboard.writeText(value); } } // end::snippet[] }
Best PracticesContext Menu is used to provide shortcuts to the user. You shouldn’t use it as the only or primary means to complete a task. The primary way should be accessible elsewhere in the UI.
Important
Open the Context Menu by right-clicking (i.e., for desktops) or long-pressing (i.e., for mobile devices) a Grid row, or use the Menu Bar in the last column. Open in apackage com.vaadin.demo.component.contextmenu; import com.vaadin.flow.component.contextmenu.MenuItem; import com.vaadin.flow.component.contextmenu.SubMenu; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.contextmenu.GridContextMenu; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.menubar.MenuBar; import com.vaadin.flow.component.menubar.MenuBarVariant; import com.vaadin.flow.router.Route; import static org.apache.commons.io.FileUtils.byteCountToDisplaySize; @Route("context-menu-best-practices") public class ContextMenuBestPractices extends Div { public ContextMenuBestPractices() { Grid<File> grid = new Grid(); grid.setAllRowsVisible(true); grid.setItems(getFiles()); grid.addColumn(File::getName).setHeader("Name"); grid.addColumn(File::getDisplaySize).setHeader("Size"); // tag::snippet[] grid.addComponentColumn(file -> { MenuBar menuBar = new MenuBar(); menuBar.addThemeVariants(MenuBarVariant.LUMO_TERTIARY); menuBar.addItem("Preview", event -> { }); menuBar.addItem("Edit", event -> { }); menuBar.addItem("Delete", event -> { }); return menuBar; }).setWidth("70px").setFlexGrow(0); GridContextMenu<File> menu = grid.addContextMenu(); menu.addItem("Preview", event -> { }); menu.addItem("Edit", event -> { }); menu.addItem("Delete", event -> { }); // end::snippet[] add(grid); } private File[] getFiles() { return new File[] { new File("Annual Report.docx", 25165824), new File("Financials.xlsx", 44040192) }; } private class File { private String name; private long size; File(String name, long size) { this.name = name; this.size = size; } public String getName() { return name; } public long getSize() { return size; } public String getDisplaySize() { return byteCountToDisplaySize(size); } } }
context-menu-best-practices.tsximport { useSignal } from '@vaadin/hilla-react-signals'; import { ContextMenu } from '@vaadin/react-components/ContextMenu.js'; import { Grid } from '@vaadin/react-components/Grid.js'; import { GridColumn } from '@vaadin/react-components/GridColumn.js'; import { MenuBar } from '@vaadin/react-components/MenuBar.js'; interface FileItem { name: string; size: string; } function Example() { const items = useSignal([{ text: 'View' }, { text: 'Edit' }, { text: 'Delete' }]); const gridItems = useSignal<FileItem[]>([ { name: 'Annual Report.docx', size: '24 MB' }, { name: 'Financials.xlsx', size: '42 MB' }, ]); const renderMenuBar = useCallback(() => <MenuBar items={items.value} theme="tertiary" />, []); return ( // tag::snippet[] <ContextMenu items={items.value}> <Grid allRowsVisible items={gridItems.value}> <GridColumn path="name" /> <GridColumn path="size" /> <GridColumn width="70px" flexGrow={0} renderer={renderMenuBar}/> </Grid> </ContextMenu> // end::snippet[] ); }
context-menu-best-practices.tsimport '@vaadin/context-menu'; import '@vaadin/grid'; import '@vaadin/menu-bar'; import { html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { Grid } from '@vaadin/grid'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; import { applyTheme } from 'Frontend/generated/theme'; interface FileItem { name: string; size: string; } @customElement('context-menu-best-practices') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } // tag::snippet[] @state() private items = [{ text: 'View' }, { text: 'Edit' }, { text: 'Delete' }]; // end::snippet[] @state() private gridItems: FileItem[] = [ { name: 'Annual Report.docx', size: '24 MB' }, { name: 'Financials.xlsx', size: '42 MB' }, ]; protected override render() { return html` <!-- tag::snippethtml[] --> <vaadin-context-menu .items=${this.items}> <vaadin-grid all-rows-visible .items=${this.gridItems} @vaadin-contextmenu=${this.onContextMenu} > <vaadin-grid-column path="name"></vaadin-grid-column> <vaadin-grid-column path="size"></vaadin-grid-column> <vaadin-grid-column width="70px" flex-grow="0" ${columnBodyRenderer( () => html` <vaadin-menu-bar .items=${this.items} theme="tertiary"></vaadin-menu-bar> `, [] )} ></vaadin-grid-column> </vaadin-grid> </vaadin-context-menu> <!-- end::snippethtml[] --> `; } onContextMenu(e: MouseEvent) { // Prevent opening context menu on header row. const target = e.currentTarget as Grid<FileItem>; if (target.getEventContext(e).section !== 'body') { e.stopPropagation(); } } }
You should use Context Menu when there is no dedicated button for opening an overlay menu, such as right-clicking a grid row. When there is a dedicated element or component, such as an overflow menu, use Menu Bar.
IconsUse icons when applicable to help improve recognition. It’s recommended to use commonly recognized icons to avoid confusion. Use icons consistently throughout a list of options.
LabellingSuffix a menu item with "…" when the associated action won’t be executed, but instead reveal some UI, like a dialog, for completing the action.
Component Usage RecommendationsComponent for displaying a horizontal menu with multi-level sub-menus.
A generic overlay whose position is anchored to an element in the UI.
760AB528-B02C-4B6C-BB27-D7EFEDAE706E
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4