Skip to content

Search pages

Jump to any page on the site.

Menu

Grows out of its trigger. Items fade in one after another.

Live demo

The preset here changes the whole site. Slow motion and reduced motion change this stage only.

Frame by frame

Every frame of the movement, on top of each other. Wide gaps are fast. Ghosts past the dashed outline are overshoot. It updates with the preset.

22 frames336 ms · drawn 4.0×
22 frames at 60 fps. Settles in 336 ms, no overshoot.

When to use

Dropdown menus, context menus, and popovers that hang off a trigger. The menu grows out of its trigger: it scales up from scale.enter and drops distance.sm into place, on spring.fast so it keeps up with the click. The items fade in one after another, up to stagger.cap, so the eye reads them top to bottom.

Set the transform origin to the trigger's corner. Move focus to the first item at once when the menu opens from the keyboard. The exit is a fast fade with no drop, so the menu gets out of the way.

Recipe

Dropdown menus and popovers. The menu scales up from scale.enter at its trigger and drops distance.sm into place. Items fade in one after another, up to stagger.cap.

States of the menu recipe
StatePropertyTokenNow
initialopacityliteral0
scale{scale.enter}0.96
y-{distance.sm}-5
animateopacityliteral1
scaleliteral1
yliteral0
exitopacityliteral0
scale{scale.enter}0.96
yliteral0
transitionMovement (spring){spring.fast}stiffness 715 · damping 53
transitionFade duration{duration.fast}132ms
transitionFade easing{easing.enter}cubic-bezier(0, 0, 0, 1)
transitionExit duration{duration.fast}132ms
transitionExit easing{easing.exit}cubic-bezier(0.3, 0, 1, 1)
transitionStagger delay{stagger.list}32ms
transitionStagger cap{stagger.cap}8

Tokens used

  • spring.fast stiffness 715 · damping 53
  • duration.fast 132ms
  • easing.enter cubic-bezier(0, 0, 0, 1)
  • easing.exit cubic-bezier(0.3, 0, 1, 1)
  • stagger.list 32ms
  • stagger.cap 8
  • scale.enter 0.96
  • distance.sm 5px

Code

/* menu: Dropdown menus and popovers. The menu scales up from scale.enter at its trigger and drops distance.sm into place. Items fade in one after another, up to stagger.cap.
   Use: .motif-menu on the menu (set --motif-origin to the trigger's corner or the clicked point),
   .motif-menu-item on each item. Exit with data-state="closed". */
.motif-menu {
  transform-origin: var(--motif-origin, top left);
  transition:
    opacity var(--motif-duration-fast) var(--motif-ease-enter),
    transform var(--motif-spring-fast-duration) var(--motif-spring-fast),
    filter var(--motif-duration-fast) var(--motif-ease-enter),
    clip-path var(--motif-spring-fast-duration) var(--motif-spring-fast);
}
@starting-style {
  .motif-menu {
    opacity: 0;
    transform: translateY(calc(-1 * var(--motif-distance-sm))) scale(var(--motif-scale-enter));
    filter: none;
    clip-path: none;
  }
}
.motif-menu[data-state="closed"] {
  opacity: 0;
  transform: scale(var(--motif-scale-enter));
  filter: none;
  clip-path: none;
  transition:
    opacity var(--motif-duration-fast) var(--motif-ease-exit),
    transform var(--motif-duration-fast) var(--motif-ease-exit),
    filter var(--motif-duration-fast) var(--motif-ease-exit),
    clip-path var(--motif-duration-fast) var(--motif-ease-exit);
}
.motif-menu-item {
  transition: opacity var(--motif-duration-fast) var(--motif-ease-enter);
}
@starting-style {
  .motif-menu-item {
    opacity: 0;
  }
}
.motif-menu-item {
  transition-delay: calc(var(--motif-stagger-step) * var(--motif-stagger-n, 0));
}
.motif-menu-item:nth-child(2) {
  --motif-stagger-n: 1;
}
.motif-menu-item:nth-child(3) {
  --motif-stagger-n: 2;
}
.motif-menu-item:nth-child(4) {
  --motif-stagger-n: 3;
}
.motif-menu-item:nth-child(5) {
  --motif-stagger-n: 4;
}
.motif-menu-item:nth-child(6) {
  --motif-stagger-n: 5;
}
.motif-menu-item:nth-child(7) {
  --motif-stagger-n: 6;
}
.motif-menu-item:nth-child(8) {
  --motif-stagger-n: 7;
}
/* Children past stagger.cap (8) enter with the last staggered child. */
.motif-menu-item:nth-child(n + 9) {
  --motif-stagger-n: 7;
}

/* Reduced motion: the variable overrides at the end of motif.css set
   travel to 0, scale to 1, and blur to 0, so this recipe needs no extra rule. */

Reduced motion

  • No scale and no drop. The menu and its items fade in together at duration.fast. Focus still moves to the first item at once.
  • Fades take 132ms (duration.fast), so the state change stays visible.
  • Turn on the Reduced motion box under the demo to preview it, or set your system to reduce motion.