Skip to content
buttery

v1.0.0

Source on GitHub

Version 1.0.0

MorphMenu

A row of menu triggers whose menus open in one floating panel that reshapes from menu to menu instead of closing and reopening.

import { MorphMenu } from "@butteryui/react";

Demo

When to use it

Use it for

  • Top-level navigation with a few menus of links, each with a caption
  • Menus that should feel like one surface as the pointer moves along the row

Not for

  • A page's top bar: use NavBar, which holds a MorphMenu
  • Actions on a thing: use Menu

Examples

The open menu kept in state, so the page can open one too, as Browse the guides does.

No menu open

Menus with a story

A site's menus in full: columns of links with captions, a story at the menu's end and a See all link under the columns.

Stacked

With layout="stack", the menus are sections that open in place, one at a time, as in a phone sheet.

Two menus

Two short menus and a plain link.

Variants and states

Rest on a trigger for 150ms and its menu unfolds under the row in dur-fast. Move along the row and the panel reshapes on spring.snappy while the columns crossfade, the new ones sliding in from the side you came from. Leave and it folds up after 100ms.

Props

MorphMenu

PropTypeDefaultDescription
labelstring"Main"Names the navigation for screen readers. Defaults to "Main".
valuestringThe open menu's value, or "" while none is open. Pair with onValueChange to control it.
defaultValuestring""The menu open at first when uncontrolled. Defaults to "", none.
onValueChange((value: string) => void)Called when a menu opens, another takes over, or the open one closes ("").
layout"bar" | "stack""bar""bar" (the default) puts the triggers in a row whose menus open in one shared panel. "stack" lists the menus as sections that open in place, one at a time, as in a nav bar's phone sheet.

It also takes Omit<ComponentPropsWithRef<"nav">, "defaultValue" | "dir">.

MorphMenu.Column

PropTypeDefaultDescription
titleReactNodeThe column's heading, which also names its list of links.

It also takes Omit<ComponentPropsWithRef<"div">, "title">.

MorphMenu.Entry

PropTypeDefaultDescription
title

required

ReactNodeThe link's title, in label.
captionReactNodeOne line under the title, in caption and text-secondary, truncating.

It also takes Omit<ComponentPropsWithRef<"a">, "title">.

MorphMenu.Item

PropTypeDefaultDescription
label

required

stringThe trigger's text.
valuestringIdentifies the menu in value. Defaults to a generated id.
featuredReactNodeA column at the menu's end, edge to edge on the section level, such as a story or a promotion.
footerReactNodeA row under the columns, past a divider, such as a "See all" ArrowLink.
childrenReactNodeThe menu's columns: MorphMenu.Columns.

MorphMenu.Link

It also takes ComponentPropsWithRef<"a">.

Accessibility

A navigation landmark ("Main" by default). Triggers are buttons with aria-expanded; the arrow keys move between them, Enter, Space or ArrowDown opens a menu and ArrowDown moves into an open one, and Escape closes it with focus back on its trigger. The pointer never closes or switches a menu that keyboard focus is in. Each column is a list named by its heading. In a stack, each menu is an accordion section.

KeysWhat they do
Enter or SpaceOpens or closes the focused trigger's menu, or in a stack its section
Arrow keysMove between the triggers and links in the row, without wrapping, or between the sections in a stack
Down arrowOpens the focused trigger's menu, or moves into it once it's open
Home or EndMoves to the first or last item in the row
EscapeCloses the open menu and puts focus back on its trigger
TabGoes from an open trigger into its menu, then past the menu's last link to the next trigger, or after the last menu to what follows the row

Motion

It changes shape as one surface, on `spring.smooth`, or `spring.morph` for a big change, rather than closing and reopening.

With reduced motion: The card appears and leaves with a 100ms fade, a switch swaps the columns without sliding while the card takes its new size and place at once, and the chevron turns at once.

Layout

In a row, each trigger is a 36px pill (--bt-h-button) whose press area runs the row's full height, and a row too narrow for its triggers scrolls sideways; a menu opens in one floating card 8px under the row, the menu's size, centered under its trigger and kept inside the row, and scrolls inside when the window has less room below. In a stack each section's trigger is 48px tall and full width, and entry captions truncate to one line. It's its own size container, so it takes the width its parent gives it: in a parent that sizes it to its content, such as a Stack with align="start", it has none, so give it a row of its own.

See it in the Halfmoon shop demo.