Skip to content

Overlay presets

Animate Base UI overlays and panels with CSS utilities that follow speed and reduced-motion settings.

Use a preset on any Base UI part that mounts and unmounts: popups, backdrops, sheets, toasts and collapsible panels. Base UI marks the start and end of each transition with data-starting-style and data-ending-style, and the preset animates between them. Each preset is a CSS transition, not a keyframe animation, so reopening an overlay mid-close continues from where it is. For motion Base UI doesn't mount, use a block instead: Sliding Indicator for selection indicators and Collapse for disclosure outside Base UI.

Installation

With the package, the presets ship inside its stylesheet, so there's nothing to add:

app/globals.css
@import '@youj/react/styles.css';

With source copied in, they're their own item, youj-presets, and there's usually nothing to add either. Every UI item that animates with a preset depends on it, and the CLI writes the utilities straight into your app's stylesheet. To use the presets on overlays of your own before you've added one of those items, add the item directly:

npx @youj/cli add youj-presets

Usage

Put the preset on the part that appears, and spread useYoujScope() on its portal:

This example uses Base UI's primitives directly, so install @base-ui/react in your app (pnpm add @base-ui/react), as pnpm won't resolve it through Youj.

import { Popover } from '@base-ui/react/popover'
import { useYoujScope } from '@youj/core'

export function SharePopover() {
  const scope = useYoujScope()
  return (
    <Popover.Root>
      <Popover.Trigger>Share</Popover.Trigger>
      <Popover.Portal {...scope}>
        <Popover.Positioner sideOffset={6}>
          <Popover.Popup className="yj-pop rounded-lg border bg-overlay p-4 shadow-md">…</Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  )
}

Every @youj/react overlay already does both. The preview above is those components, one preset each. Open them at ¼ speed to see each one move.

Why every portal needs the scope

YoujProvider sets the Speed and reduced-motion settings as CSS custom properties on its wrapper element. A portal renders into <body>, outside that wrapper, so without the scope an overlay ignores Speed and a provider's reducedMotion="always". useYoujScope() returns the same properties for the portal element. Call it inside the component that renders the portal, so it reads the nearest provider.

Examples

The presets

UtilityPut it onMotion
yj-popPopover, Menu, Select, Combobox, Context Menu and Preview Card popupsScales from 0.96 and fades in from the anchor point in --transform-origin. Leaves faster than it came.
yj-tooltipTooltip popupsScales from 0.97 and travels 3px from the trigger's side, mirrored under right-to-left text.
yj-dialogDialog and Alert Dialog popupsRises 6px while scaling from 0.96. A parent with a nested dialog open recedes one step per level: 0.875rem down, 6% smaller, and dimmed under a wash that keeps its text above 4.5:1. Retune the wash with --yj-dialog-dim.
yj-backdropEvery backdropFades. Thins as a drawer is swiped away, tracking the finger. Bring your own color, such as bg-scrim.
yj-sheet-top, -right, -bottom, -leftDialog-based sheets and Drawer popupsSlides fully off its edge. With Drawer it follows the finger, and a flick leaves faster.
yj-sheet-start, yj-sheet-endThe same, in right-to-left-aware layoutsAs above, flipped under dir="rtl".
yj-toast, yj-toast-topToast roots, stacked against the bottom or top edgePeeks and shrinks behind the front toast, spreads on hover or focus, rises in, and leaves in the swipe direction.
yj-toast-contentToast contentHides the text of toasts behind the front one until the stack spreads.
yj-collapseCollapsible and Accordion panelsHeight to and from the size Base UI measures, with a fade. The panel clips while it moves, so keep focus rings inside its padding.
yj-fadeTabs panels, item indicators, scroll arrowsOpacity only.

Replace shadcn's tw-animate classes (data-open:animate-in, fade-in-0, zoom-in-95, slide-in-from-*, duration-*) with the preset. Don't combine them. The presets animate transform and opacity and leave the individual translate and scale properties to you, so centering with -translate-x-1/2 -translate-y-1/2 keeps working.

Sheet classes must appear in full somewhere in your source, for example in a lookup object, because Tailwind can't see a class built with a template string.

const side = { top: 'yj-sheet-top inset-x-0 top-0', right: 'yj-sheet-right inset-y-0 right-0', bottom: 'yj-sheet-bottom inset-x-0 bottom-0', left: 'yj-sheet-left inset-y-0 left-0' }

Tuning the toast stack

yj-toast owns the stack's geometry: position, width and a height clamped to the front toast while collapsed. The viewport only needs a position and a width. Two custom properties set the spacing:

PropertyDefaultSets
--yj-toast-gap0.75remSpace between toasts when the stack spreads.
--yj-toast-peek0.75remHow far each toast behind shows while collapsed.

Instant when Base UI asks

Base UI sets data-instant when a transition shouldn't play: opening from the keyboard, closing with Escape, or moving between grouped tooltips. yj-fade, yj-pop and yj-tooltip honor it with a zero duration, so menus, popovers and tooltips opened from the keyboard never wait on an animation. The dialog, backdrop, sheet, toast and collapse presets still play their transitions.

Reference

MotionPop in, pop out, tooltip in… · reduced motion

Motion

Motion tokens
MomentTokens
Pop inyj-pop: opacity, scale 0.96 → 1
duration.fastease.enter

140ms · cubic-bezier(0.16, 1, 0.3, 1)

140mscubic-bezier(0.16, 1, 0.3, 1)
Pop outyj-pop, yj-tooltip
duration.instantease.exit

80ms · cubic-bezier(0.4, 0, 1, 1)

80mscubic-bezier(0.4, 0, 1, 1)
Tooltip inyj-tooltip: opacity, 3px travel, scale 0.97 → 1
duration.fastease.enter

140ms · cubic-bezier(0.16, 1, 0.3, 1)

140mscubic-bezier(0.16, 1, 0.3, 1)
Dialog inyj-dialog: opacity, rise 6px, scale 0.96 → 1
duration.baseease.enter

220ms · cubic-bezier(0.16, 1, 0.3, 1)

220mscubic-bezier(0.16, 1, 0.3, 1)
Dialog out
duration.fastease.exit

140ms · cubic-bezier(0.4, 0, 1, 1)

140mscubic-bezier(0.4, 0, 1, 1)
Nested recedeparent dialog behind a child
duration.baseease.standard

220ms · cubic-bezier(0.2, 0, 0, 1)

220mscubic-bezier(0.2, 0, 0, 1)
Backdropyj-backdrop in; out on fast
duration.baseease.standard

220ms · cubic-bezier(0.2, 0, 0, 1)

220mscubic-bezier(0.2, 0, 0, 1)
Sheet inyj-sheet-*: off the edge to rest
duration.slowease.enter

340ms · cubic-bezier(0.16, 1, 0.3, 1)

340mscubic-bezier(0.16, 1, 0.3, 1)
Sheet outshorter after a flick
duration.baseease.exit

220ms · cubic-bezier(0.4, 0, 1, 1)

220mscubic-bezier(0.4, 0, 1, 1)
Toastyj-toast: rise, stack and spread
duration.slowease.enter

340ms · cubic-bezier(0.16, 1, 0.3, 1)

340mscubic-bezier(0.16, 1, 0.3, 1)
Toast heightcollapsed and spread heights
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)
Toast contentyj-toast-content
duration.baseease.standard

220ms · cubic-bezier(0.2, 0, 0, 1)

220mscubic-bezier(0.2, 0, 0, 1)
Collapseyj-collapse: height and opacity
duration.baseease.standard

220ms · cubic-bezier(0.2, 0, 0, 1)

220mscubic-bezier(0.2, 0, 0, 1)
Fadeyj-fade
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)

Reduced motion

Every preset duration is multiplied by --yj-motion, which is 0 under reduced motion. Overlays appear and disappear at once, and Base UI unmounts them immediately because there's nothing to wait for.

Reduced motion

Under reduced motion every preset's duration is zero, so overlays appear and disappear at once, panels open to full height immediately, and toasts take their place in the stack without travel. The overlays still open, close, trap focus and announce themselves. Only the movement goes. It follows the OS setting, and a provider's reducedMotion="always" too, as long as the portal carries the scope.

AccessibilityKeyboard, ARIA, focus

Accessibility

  • The presets are visual only. Roles, focus trapping, focus return and Escape come from Base UI, and they work the same with or without motion.
  • Exits are shorter than entrances (instant for popups, fast for dialogs), so a dismissed overlay is out of the way almost at once and never covers what the reader went back to.
  • Swipe gestures on drawers and toasts track the finger even under reduced motion, because that movement is direct manipulation, not animation.