Skip to content

Menu Panel

A button with a panel under it that opens when you point at it, click it or press Enter.

$npx shadcn@latest add https://framebrick.co/r/menu-panel.json

From the Kendi template, see it in context.

Installation

$ npx shadcn@latest add https://framebrick.co/r/menu-panel.json

Usage

import { MenuPanel, MenuPanelColumn } from "@/components/ui/menu-panel";

<MenuPanel label="Categories" panelClassName="grid w-[34rem] grid-cols-2 gap-6">
  <MenuPanelColumn>…the categories…</MenuPanelColumn>
  <MenuPanelColumn>…the spotlight…</MenuPanelColumn>
</MenuPanel>

Props

PropTypeDefaultDescription
labelReactNode—The button's content.
childrenReactNode—What is in the panel: your columns. Wrap each in MenuPanelColumn to have them come in one after the other.
align"center" | "start""center"Where the panel sits under the button: centred on it, or from its left edge.
caretbooleantrueA caret after the label that turns up while the menu is open.
classNamestring—Classes for the button.
panelClassNamestring—Classes for the panel: its width, padding, layout.
  • It is Kendi's header "Categories" menu: a 720 px panel, 10 px under the button, centred on it, with the categories at the left and a Spotlight at the right. Framer: the panel fades in on spring bounce 0.2, 0.4 s, and each column fades from 0.001 and slides in from 20 px to the left on a 0.4 s tween, cubic-bezier(0.44, 0, 0.56, 1).
  • Kendi opens it by hover on a desktop and by click on a tablet, by window width. Here the input decides: a mouse opens it by pointing, a touch or a key by activating, so a laptop with a touch screen and a tablet with a mouse both work. The panel stays open while the pointer is on the button or the panel (the 10 px between them is the panel's own padding), and closes when the pointer leaves both, on Escape (the focus returns to the button), when the focus leaves, or on a press outside.
  • The button has aria-expanded and aria-controls, and its caret turns. It is a menu of links, not a dialog: the focus is not trapped and Tab moves on through the panel. The panel is in the HTML when it is closed, hidden and inert, so its links are there for a search engine but not in the way; only the opening is animated.
  • The slide is a transform, which <MotionConfig reducedMotion="user"> skips; the fades are opacity and stay with reduced motion.
  • Text in the panel needs 4.5:1 on bg-popover: the secondary text of the demo is foreground at 75%.

AI prompt

Paste into v0, Lovable, Bolt or Claude to rebuild this component in your own project.

Create a React + Tailwind CSS + Motion (motion/react) component called MenuPanel, a client component, with a helper MenuPanelColumn.
- MenuPanel props: label (the button's content), children (the panel's content), align ("center" | "start"), caret (true), className, panelClassName. A relative inline-flex wrapper holds a button (type button, aria-expanded, aria-controls the panel's id, inline-flex items-center gap-1.5 text-sm font-medium, ring-[3px] ring-ring/50 on focus-visible, a 14px caret svg that flips with -scale-y-100 while open) and the panel.
- The panel is an absolute top-full z-30 div with pt-2.5 (the gap is its own padding), centred with left-1/2 -translate-x-1/2 or left-0, holding a motion.div (rounded-xl border bg-popover p-6 text-popover-foreground shadow-lg plus panelClassName). It is always in the page: display: none and inert when closed. The motion.div animates opacity 0 to 1 with a spring (bounce 0.2, duration 0.4s) between variants closed and open; keep a state "present" that is true from opening until the closed animation completes (onAnimationComplete), and add hidden while it is false.
- Open: a mouse pointerenter on the wrapper opens it and pointerleave closes it (pointerType mouse only); a click or tap, or Enter or Space, toggles it, but a click that follows a mouse opening it by pointing is ignored. Escape closes it and focuses the button; a blur whose relatedTarget is outside closes it; a pointerdown outside the wrapper closes it.
- MenuPanelColumn is a motion.div with variants closed { opacity: 0.001, x: -20 } and open { opacity: 1, x: 0 } on a 0.4s tween with ease [0.44, 0, 0.56, 1], inheriting the variant from the panel.

Be the first to know

Get new templates and components first

One short email when we release a template or a free component. No spam — unsubscribe anytime.

We use your email only for Framebrick updates. Privacy policy