Menubar
A horizontal menu bar with dropdown menus, wrapping Morphos's Menubar primitive.
Installation
npx kosmesis add menubarpnpm dlx kosmesis add menubaryarn dlx kosmesis add menubarbunx kosmesis add menubarInstall the following dependencies:
npm install @morphos/layoutpnpm add @morphos/layoutyarn add @morphos/layoutbun add @morphos/layoutCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { Menubar as MorphosMenubar, MenubarContent as MorphosMenubarContent, MenubarItem as MorphosMenubarItem, MenubarSeparator as MorphosMenubarSeparator, MenubarTrigger as MorphosMenubarTrigger, type MenubarContentProps as MorphosMenubarContentProps, type MenubarItemProps as MorphosMenubarItemProps, type MenubarSeparatorProps as MorphosMenubarSeparatorProps, type MenubarTriggerProps as MorphosMenubarTriggerProps} from "@morphos/layout";import { cn } from "@/lib/utils";/** * Extends (not wraps) Morphos's `Menubar` so `new Menubar({ "aria-label": "..." })` still yields * a real instance with `.activeMenu`/`.toggle()` — what `MenubarMenu` needs via its `menubar` * prop. */@Component()export class Menubar extends MorphosMenubar { render() { return ( <div id={this.id} role="menubar" class={cn("flex h-9 items-center gap-1 rounded-md border bg-background p-1 shadow-xs", this.class)} aria-label={this["aria-label"]} > {this.children} </div> ); }}/** * `MenubarMenu`'s `render()` is a no-op Fragment — re-exported directly. Each menu needs its own * instance (`@State() fileMenu = new MenubarMenu({ menubar: this.menubar, value: "file" })`), * shared between the `<MenubarMenu>` mounted in the tree and the `MenubarTrigger`/`MenubarContent` * that reference it via their `menu` prop. */export { MenubarMenu, type MenubarMenuProps } from "@morphos/layout";export type MenubarTriggerProps = MorphosMenubarTriggerProps;@Component()export class MenubarTrigger extends StatelessComponent<MenubarTriggerProps> { render() { const { class: cls, ...rest } = this.props; return ( <MorphosMenubarTrigger class={cn( "flex items-center rounded-sm px-3 py-1 text-sm font-medium outline-none select-none hover:bg-accent hover:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground", cls, )} {...rest} /> ); }}export type MenubarContentProps = MorphosMenubarContentProps;@Component()export class MenubarContent extends StatelessComponent<MenubarContentProps> { render() { const { class: cls, ...rest } = this.props; return ( <MorphosMenubarContent class={cn( "z-50 min-w-48 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md", cls, )} {...rest} /> ); }}export interface MenubarItemProps extends MorphosMenubarItemProps { variant?: "default" | "destructive"; inset?: boolean;}@Component()export class MenubarItem extends StatelessComponent<MenubarItemProps> { render() { const { class: cls, variant = "default", inset, ...rest } = this.props; return ( <MorphosMenubarItem class={cn( "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none", "hover:bg-accent hover:text-accent-foreground", "data-disabled:pointer-events-none data-disabled:opacity-50", inset && "pl-8", variant === "destructive" && "text-destructive hover:bg-destructive/10", cls, )} {...rest} /> ); }}export type MenubarSeparatorProps = MorphosMenubarSeparatorProps;@Component()export class MenubarSeparator extends StatelessComponent<MenubarSeparatorProps> { render() { const { class: cls, ...rest } = this.props; return <MorphosMenubarSeparator class={cn("-mx-1 my-1 h-px bg-border", cls)} {...rest} />; }}Install the following dependencies:
npm install @morphos/layout @praxisjs/csspnpm add @morphos/layout @praxisjs/cssyarn add @morphos/layout @praxisjs/cssbun add @morphos/layout @praxisjs/cssCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { Menubar as MorphosMenubar, MenubarContent as MorphosMenubarContent, MenubarItem as MorphosMenubarItem, MenubarSeparator as MorphosMenubarSeparator, MenubarTrigger as MorphosMenubarTrigger, type MenubarContentProps as MorphosMenubarContentProps, type MenubarItemProps as MorphosMenubarItemProps, type MenubarSeparatorProps as MorphosMenubarSeparatorProps, type MenubarTriggerProps as MorphosMenubarTriggerProps} from "@morphos/layout";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class MenubarStyles extends Stylesheet { $root = this.css({ display: "flex", height: "2.25rem", alignItems: "center", gap: "0.25rem", borderRadius: `calc(${t.radius} - 2px)`, border: `1px solid ${t.border}`, backgroundColor: t.background, padding: "0.25rem", boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)", }); $trigger = this.css({ display: "flex", alignItems: "center", borderRadius: "0.125rem", padding: "0.25rem 0.75rem", fontSize: "0.875rem", fontWeight: 500, outline: "none", userSelect: "none", }) .hover({ backgroundColor: t.accent, color: t.accentForeground }) .on("&[data-open]", { backgroundColor: t.accent, color: t.accentForeground }); $content = this.css({ zIndex: 50, minWidth: "12rem", overflow: "hidden", borderRadius: `calc(${t.radius} - 2px)`, border: `1px solid ${t.border}`, backgroundColor: t.popover, padding: "0.25rem", color: t.popoverForeground, boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)", }); $item = this.css({ position: "relative", display: "flex", cursor: "default", alignItems: "center", gap: "0.5rem", borderRadius: "0.125rem", padding: "0.375rem 0.5rem", fontSize: "0.875rem", outline: "none", userSelect: "none", }) .hover({ backgroundColor: t.accent, color: t.accentForeground }) .on("&[data-disabled]", { pointerEvents: "none", opacity: 0.5 }); $itemInset = this.css({ paddingLeft: "2rem" }); $itemDestructive = this.css({ color: t.destructive }).hover({ backgroundColor: `color-mix(in oklab, ${t.destructive} 10%, transparent)` }); $separator = this.css({ margin: "0.25rem -0.25rem", height: "1px", backgroundColor: t.border });}/** * Extends (not wraps) Morphos's `Menubar` so `new Menubar({ "aria-label": "..." })` still yields * a real instance with `.activeMenu`/`.toggle()` — what `MenubarMenu` needs via its `menubar` * prop. */@Component()export class Menubar extends MorphosMenubar { @Styled(MenubarStyles) $s!: MenubarStyles; render() { return ( <div id={this.id} role="menubar" class={cx(this.$s.$root, this.class)} aria-label={this["aria-label"]}> {this.children} </div> ); }}/** * `MenubarMenu`'s `render()` is a no-op Fragment — re-exported directly. Each menu needs its own * instance (`@State() fileMenu = new MenubarMenu({ menubar: this.menubar, value: "file" })`), * shared between the `<MenubarMenu>` mounted in the tree and the `MenubarTrigger`/`MenubarContent` * that reference it via their `menu` prop. */export { MenubarMenu, type MenubarMenuProps } from "@morphos/layout";export type MenubarTriggerProps = MorphosMenubarTriggerProps;@Component()export class MenubarTrigger extends StatelessComponent<MenubarTriggerProps> { @Styled(MenubarStyles) $s!: MenubarStyles; render() { const { class: cls, ...rest } = this.props; return <MorphosMenubarTrigger class={cx(this.$s.$trigger, cls)} {...rest} />; }}export type MenubarContentProps = MorphosMenubarContentProps;@Component()export class MenubarContent extends StatelessComponent<MenubarContentProps> { @Styled(MenubarStyles) $s!: MenubarStyles; render() { const { class: cls, ...rest } = this.props; return <MorphosMenubarContent class={cx(this.$s.$content, cls)} {...rest} />; }}export interface MenubarItemProps extends MorphosMenubarItemProps { variant?: "default" | "destructive"; inset?: boolean;}@Component()export class MenubarItem extends StatelessComponent<MenubarItemProps> { @Styled(MenubarStyles) $s!: MenubarStyles; render() { const { class: cls, variant = "default", inset, ...rest } = this.props; return ( <MorphosMenubarItem class={cx(this.$s.$item, inset && this.$s.$itemInset, variant === "destructive" && this.$s.$itemDestructive, cls)} {...rest} /> ); }}export type MenubarSeparatorProps = MorphosMenubarSeparatorProps;@Component()export class MenubarSeparator extends StatelessComponent<MenubarSeparatorProps> { @Styled(MenubarStyles) $s!: MenubarStyles; render() { const { class: cls, ...rest } = this.props; return <MorphosMenubarSeparator class={cx(this.$s.$separator, cls)} {...rest} />; }}Examples
Usage
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarTrigger,
} from "@/components/ui/menubar";
@Component()
class AppMenubar extends StatefulComponent {
@State() menubar = new Menubar({ "aria-label": "Application menu" });
@State() fileMenu = new MenubarMenu({ menubar: this.menubar, value: "file" });
render() {
return (
<Menubar aria-label="Application menu">
<MenubarMenu menubar={this.menubar} value="file">
<MenubarTrigger menu={this.fileMenu}>File</MenubarTrigger>
<MenubarContent menu={this.fileMenu}>
<MenubarItem menu={this.fileMenu} label="New File" onSelect={() => {}} />
<MenubarSeparator />
<MenubarItem menu={this.fileMenu} label="Quit" onSelect={() => {}} />
</MenubarContent>
</MenubarMenu>
</Menubar>
);
}
}Each MenubarMenu needs its own sibling instance, same as NavigationMenuItem.
Props
Menubar
Extends Morphos's Menubar so new Menubar({ "aria-label": "..." }) still yields a real instance
with .activeMenu/.toggle() — what MenubarMenu needs via its menubar prop.
| Prop | Type | Default |
|---|---|---|
aria-label | string | — |
MenubarMenu
Re-exported directly from @morphos/layout — its render() is a no-op Fragment. Each menu needs
its own instance, shared between the <MenubarMenu> mounted in the tree and the
MenubarTrigger/MenubarContent that reference it via their menu prop.
| Prop | Type | Default |
|---|---|---|
menubar | Menubar | — required |
value | string | — required |
side | "top" | "right" | "bottom" | "left" | "bottom" |
align | "start" | "center" | "end" | "start" |
sideOffset | number | 4 |
MenubarTrigger
| Prop | Type | Default |
|---|---|---|
menu | MenubarMenu | — required |
MenubarContent
| Prop | Type | Default |
|---|---|---|
menu | MenubarMenu | — required |
MenubarItem
| Prop | Type | Default |
|---|---|---|
menu | MenubarMenu | — required |
value | string | — |
label | string | — |
disabled | boolean | — |
onSelect | () => void | — |
variant | "default" | "destructive" | "default" |
inset | boolean | — |
MenubarSeparator takes no props beyond class.