Keyboard accessible by default with semantic markup, visible focus treatment, and tokenized states. Add descriptive labels for icon-only or decorative usage.
Dependency notes
Check Installation for shared peers. This component keeps styling in Tailwind classes and the --zui-* token contract.
Navigation
Circular Menu
The Circular Menu component arranges its items on a circle around a center trigger. Item count, radius, start angle, arc sweep, direction, item size, and continuous rotation are all props, so the same component covers a radial action launcher, a floating action button that fans out, and a decorative spinning orbit.
Circular menu playground
Every part of the ring is a prop: item count, radius, start angle, arc sweep, how it opens, whether it spins, and how labels are revealed. Toggle Show output / Show code and the snippet updates to match the selected variant.
Open the ring, then walk it with the arrow keys — Home and End jump to the first and last item, Escape closes and returns focus to the trigger.
All appearances
Every shipped appearance token at a glance, each shown as an always-open ring. Click any swatch to load it into the playground above.
default
primary
secondary
success
destructive
warning
info
blue
violet
emerald
amber
rose
slate
zinc
gradient-blue
gradient-violet
gradient-rose
glass
Quarter arc
Set sweep below 360 and the items spread across an arc instead of a closed ring, with the first and last item landing exactly on the arc ends. Pair it with startAngle to aim the fan — this one opens up and to the right, the usual floating-action-button shape.
Orbit
trigger="always" keeps the ring open, and spin rotates it continuously. The static entry does this in pure CSS: the ring turns while each item counter-rotates so icons stay upright, hovering pauses it, and prefers-reduced-motion stops it entirely.
Controlled state
Drive the ring from your own state with open / onOpenChange. Here closeOnSelect is off, so picking an item reports through onSelect and leaves the ring open for a second choice.
Last action: none
Circular Menu API
Generated from the package prop types and variant definitions.
radiusRing radius in pixels. Defaults to the size variant's radius.
number | undefined
none
refAllows getting a ref to the component instance.
Once the component unmounts, React will set `ref.current` to `null`
(or call the ref with `null` if you passed a callback ref).
Ref<HTMLDivElement> | undefined
none
showSpokesDraw a line from the center to each item.
boolean | undefined
none
size
CircularMenuSize | undefined
none
spinRotate the whole ring continuously.
boolean | undefined
none
spinDurationSeconds per revolution. Defaults to `12`.
number | undefined
none
spinPauseOnHoverDefaults to `true`.
boolean | undefined
none
staggerPer-item delay in **seconds** (the static entry's `stagger` is in ms).
number | undefined
none
startAngleAngle of the first item in degrees. `0` = 12 o'clock, positive = clockwise.
number | undefined
none
sweepArc covered by the items in degrees. `360` closes the ring.
number | undefined
none
triggerDefaults to `"click"`. `"hover"` still opens on click and on focus.
CircularMenuTriggerMode | undefined
none
Content
Prop
Type
Default
children
ReactNode
none
Inherited HTML props
Prop
Type
Default
className
string | undefined
none
id
string | undefined
none
onClick
MouseEventHandler<HTMLDivElement> | undefined
none
style
CSSProperties | undefined
none
title
string | undefined
none
CircularMenuListAnimated
CircularMenuListAnimatedProps
animated
Behavior
Prop
Type
Default
animation
CircularMenuAnimation | undefined
none
staggerPer-item delay in seconds.
number | undefined
none
Inherited HTML props
Prop
Type
Default
children
ReactNode
none
className
string | undefined
none
id
string | undefined
none
onClick
MouseEventHandler<HTMLDivElement> | undefined
none
style
CSSProperties | undefined
none
title
string | undefined
none
CSS variable overrides
Circular Menu CSS variables
Override these circular-menu variables on :root, a theme selector, or a component wrapper. Layout variables (size, radius, item-size, item-x/-y/-angle, open) are written inline by the component from its props, so change those through the props rather than in CSS.
A square box owns the ring's coordinate space. The center trigger sits at its middle, and each item is placed at a solved polar offset: 0 degrees points at 12 o'clock and positive angles run clockwise. Offsets are written as CSS custom properties, so items fan out from the center when the ring opens and collapse back into it when it closes. When boxSize is omitted it is derived as 2 * (radius + itemSize / 2), which is the smallest square that fits the ring plus one disc of overhang.
Rings and arcs
sweep controls how much of the circle the items cover and direction controls which way they run. A closed ring spaces items evenly all the way around; an open arc places the first item at startAngle and the last one exactly at the end of the arc. Combined with radius and itemSize, this covers everything from a tight six-item wheel to a wide quarter-circle fan anchored in a corner.
Compound API
Use the items prop for the shorthand, or compose the parts: CircularMenu.Root, CircularMenu.Trigger, CircularMenu.List, CircularMenu.Item, CircularMenu.ItemIcon, CircularMenu.ItemLabel, and CircularMenu.Spoke. The root owns open state, geometry, and keyboard handling; the list solves one position per child and publishes it to that item, so items never take an index prop. Items with an href render as anchors.
Rotation
spin rotates the whole ring as one rigid body, spinDuration sets the seconds per revolution, and spinPauseOnHover pauses it while the pointer is over the menu. itemRotation decides whether items counter-rotate to stay upright or follow the ring around. trigger="always" keeps the ring open, which turns the component into a decorative orbit.
Keyboard interaction
Tab to the trigger, then press Enter, Space, or an arrow key to open. Arrow Right and Down move to the next item, Arrow Left and Up to the previous one, and both wrap around the ring. Home and End jump to the first and last item. Escape closes the ring and returns focus to the trigger, and tabbing away closes it too.
Accessibility
The trigger is a semantic button and the ring uses menu and menuitem roles with a roving tabindex, so only one item is in the tab order at a time. A closed ring is inert and aria-hidden. trigger="hover" also opens on click and on focus, because hover alone is not operable by keyboard or touch. Labels can be revealed on hover and focus, placed outside the disc, or hidden visually while still naming the item. Reduced-motion preferences are respected throughout.
FAQ
Can the circular menu render a half circle or a quarter arc instead of a full ring?
Yes. Set sweep to the arc you want in degrees and startAngle to aim it. A sweep of 360 divides the circle by the item count so the first and last item do not collide; any smaller sweep divides by count - 1 so both endpoints land exactly on the arc ends.
Does the circular menu work with a keyboard?
Yes. The center trigger is a real button with aria-haspopup and aria-expanded, and the ring is a role="menu" with roving tabindex. Arrow keys move around the ring and wrap, Home and End jump to the first and last item, Enter or Space selects, and Escape closes and returns focus to the trigger. The closed ring is inert, so it never takes focus.
How does the continuous rotation work without framer-motion?
The static entry rotates the ring with a CSS animation and counter-rotates each icon and label in the opposite direction, so glyphs stay upright. Hovering pauses it and prefers-reduced-motion stops it. The animated entry does the same with a framer-motion motion value, so a spinning ring never re-renders React.
Can I customize the appearance?
Yes. The component ships 18 appearance palettes (default, primary, secondary, success, destructive, warning, info, several accent colors, three gradients, and glass) across the trigger, item discs, and spokes, plus sm/md/lg sizes. Every color is a --zui-circular-menu-* CSS variable you can override, and layout values like radius and item size are both props and custom properties.