Skip to content

feat(animations): tasteful motion refresh with reduced-motion support#3375

Open
orrgottlieb wants to merge 4 commits into
masterfrom
feat/animations-tier1
Open

feat(animations): tasteful motion refresh with reduced-motion support#3375
orrgottlieb wants to merge 4 commits into
masterfrom
feat/animations-tier1

Conversation

@orrgottlieb

@orrgottlieb orrgottlieb commented May 22, 2026

Copy link
Copy Markdown
Contributor

User description

Summary

Refreshes Vibe's animation language so popovers, Toast, Modal, Skeleton, and Menu submenus feel intentional and consistent — and respect prefers-reduced-motion across the board.

  • New motion-safe / motion-reduce mixins in @vibe/style give every component a single, dependency-free way to gate non-essential motion.
  • All animation durations and easings now use existing motion tokens (--motion-productive-*, --motion-expressive-*, --motion-timing-*) — no more hardcoded 0.2s ease.
  • Scale-pops tightened (Modal/popovers from 0.80.92–0.94) for a more refined entrance.
  • Keyframes normalized to clean from/to pairs; the natural overshoot now comes from --motion-timing-emphasize instead of bespoke 0%/50%/100% curves.
  • Menu submenus now animate — they were rendering instantly before. Fade + scale-from-origin keyed off floating-ui placement so the submenu grows out of the parent item's edge.

What changed by component

  • DialogContent (used by Tooltip, Dropdown, Dialog popovers): expand variant now combines fade + scale-from-origin (was scale-only). Both opacity-and-slide and expand paths gated on motion-safe. Tightened scale to 0.92.
  • Modal: centerPop, anchorPop, fullView enter/exit gated on motion-safe. Token-based durations. Scale tightened to 0.94.
  • Toast: slide-in/out + action-button bounce gated on motion-safe, with explicit motion-reduce fallbacks (instant opacity). Slide uses motion-timing-emphasize for natural overshoot.
  • Skeleton: shine animation now motion-safe; reduce-motion users get a stable opacity: 0.7 instead of a pulse.
  • Tab: active indicator transition gated on motion-safe.
  • MenuItemSubMenu (new behavior): wrapped in CSSTransition with placement-aware submenuExpandIn / submenuExpandOut keyframes. Uses CSS animations rather than transitions because CSSTransition + mountOnEnter can collapse the start/active class swap into one paint and skip the "from" frame on a fresh mount. Visibility-toggle render strategy replaced with mount/unmount.

Reduced-motion: the new headline capability

Before this branch, prod ignored prefers-reduced-motion. Now Toast, Skeleton, Tooltip, Menu submenu, Modal, Tabs, and Dropdown all respect it.

Note on the import pattern

A second import @import "~@vibe/style/dist/mixins/motion"; is added alongside the existing @import "~@vibe/style/dist/mixins"; in each consumer. This is intentional: rollup-plugin-postcss's Sass importer prefers a partial-prefixed lookup before directory resolution, and in worktree-based dev setups node-resolve can walk up past the worktree boundary into a stale _mixins.scss in the main repo's node_modules. Importing the partial directly bypasses that ambiguity.

Test plan

  • yarn workspace @vibe/core build — clean
  • yarn workspace @vibe/dialog build — clean
  • yarn workspace @vibe/core test — 1338 tests passing
  • yarn workspace @vibe/dialog test — 45 tests passing
  • yarn workspace @vibe/core stylelint — clean
  • yarn workspace @vibe/dialog stylelint — clean
  • Storybook visual QA: Tooltip, Menu (submenu open + close), Modal, Toast, Skeleton, Tabs
  • Toggle prefers-reduced-motion: reduce in DevTools and confirm animations are suppressed (Toast/Skeleton get explicit fallbacks; others mount at rest)

🤖 Generated with Claude Code


PR Type

Enhancement


Description

  • Implement motion-safe/motion-reduce mixins for reduced-motion support

  • Replace hardcoded animation durations with motion tokens across components

  • Tighten scale-pop ratios (0.8 → 0.92–0.94) for refined entrance animations

  • Normalize keyframes to clean from/to pairs with token-based timing

  • Animate Menu submenus with placement-aware fade + scale transitions

  • Update interaction tests to await animation completion with waitFor


Diagram Walkthrough

flowchart LR
  A["Motion Mixins<br/>motion-safe/motion-reduce"] --> B["DialogContent<br/>Popovers/Tooltip"]
  A --> C["Modal<br/>Pop animations"]
  A --> D["Toast<br/>Slide + bounce"]
  A --> E["Skeleton<br/>Shine animation"]
  A --> F["Tabs<br/>Indicator transition"]
  A --> G["Menu Submenus<br/>Expand animation"]
  B --> H["Token-based<br/>durations & easing"]
  C --> H
  D --> H
  E --> H
  F --> H
  G --> H
Loading

File Walkthrough

Relevant files
Enhancement
9 files
_motion.scss
New motion-safe and motion-reduce mixins                                 
+15/-0   
index.scss
Export motion mixins from main index                                         
+1/-0     
DialogContent.module.scss
Gate animations on motion-safe, use motion tokens               
+107/-80
Modal.module.scss
Apply motion tokens and reduce-motion support to modals   
+68/-63 
Toast.module.scss
Replace hardcoded durations with motion tokens                     
+39/-33 
Skeleton.module.scss
Gate shine animation on motion-safe with fallback               
+18/-13 
Tab.module.scss
Gate tab indicator transition on motion-safe                         
+5/-1     
MenuItemSubMenu.module.scss
New submenu animation styles with placement-aware transforms
+61/-0   
MenuItemSubMenu.tsx
Wrap submenu in CSSTransition with placement-aware animations
+34/-18 
Tests
2 files
MenuItemSubMenu.test.tsx
Update tests to assert mount/unmount instead of visibility
+6/-6     
Menu.interactions.ts
Add waitFor to await animation completion in tests             
+17/-11 

…n tokens and reduced-motion support

Introduces a motion-safe/motion-reduce mixin foundation in @vibe/style and
applies it consistently across Toast, Modal, Skeleton, Tab, and the shared
DialogContent primitive (which powers Tooltip, Menu submenus, and Dropdown).
Replaces hardcoded durations and easing with motion tokens, normalizes
keyframes to clean from/to pairs, and tightens scale-pop ratios for a more
refined feel. All non-essential animations now respect
prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@orrgottlieb orrgottlieb requested a review from a team as a code owner May 22, 2026 13:28
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (7) 📘 Rule violations (3)

Grey Divider


Action required

1. Submenu enter flash risk 🐞 Bug ≡ Correctness ⭐ New
Description
MenuItemSubMenu’s CSSTransition only provides *Active classNames (no appear/enter base classes), so
the submenu can mount at its resting (visible) styles for a paint and then snap to the keyframe
from state when enterActive is applied, producing a visible flash on open. DialogContent avoids
this by mapping an explicit appear class that sets the initial hidden/scale state before the
active transition begins.
Code

packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[R60-71]

Evidence
The submenu transition wiring lacks any non-active (base) enter/appear classes, while its SCSS
defines animations only on *.enterActive/*.appearActive. This means the element has no enforced
initial hidden state at mount time. DialogContent shows the intended pattern: it provides an
explicit appear class that sets opacity: 0/transform: scale(0.92) before appearActive
transitions to the resting state, preventing a flash.

packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[58-72]
packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss[12-21]
packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss[50-55]
packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.tsx[185-200]
packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[109-121]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`MenuItemSubMenu` wires `CSSTransition` with only `appearActive`/`enterActive`/`exitActive`. Because there is no `appear`/`enter` base class that sets the initial hidden/scale state, the submenu element can render once at its default (opacity 1 / scale 1) before the active class is applied, then immediately jump to the keyframe `from` state (opacity 0 / scale 0.92), causing a flash.

### Issue Context
This code intentionally uses keyframes to avoid missing the “from” frame on mount, but it still needs an initial base class (like DialogContent’s `expandAppear`) to ensure the element is already in the correct starting state before the `*Active` class is applied.

### Fix Focus Areas
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[58-72]
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss[50-61]

### Suggested fix
1. Add base class mappings in `CSSTransition.classNames`, e.g. `appear: styles.appear`, `enter: styles.enter` (and keep existing `appearActive`/`enterActive`/`exitActive`).
2. In the SCSS module, add `.appear` and `.enter` rules (under `@include motion-safe`) that set the initial state to match your keyframe `from` values (e.g. `opacity: 0; transform: scale(0.92);`).
3. Keep the placement transform-origin classes as-is (they only set `transform-origin`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. MenuItemSubMenu root lacks data-vibe 📘 Rule violation ◔ Observability
Description
The updated root element for MenuItemSubMenu does not include the required [data-vibe]
attribute. This breaks the standard component identification/instrumentation requirement.
Code

packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[59]

Evidence
PR Compliance ID 3 requires a root [data-vibe] attribute. The root `` added/modified in
MenuItemSubMenu includes data-popper-placement but no data-vibe.

CLAUDE.md: React Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: React Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: React Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: React Components Must Use forwardRef and Include a Root [data-vibe] Attribute
packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[58-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`MenuItemSubMenu`'s root rendered element is missing the required `[data-vibe]` attribute.
## Issue Context
Compliance requires all React component roots to include `[data-vibe]` for consistent identification/instrumentation.
## Fix Focus Areas
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[58-60]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Reduced-motion blocks clicks 🐞 Bug ≡ Correctness
Description
In DialogContent animations, pointer-events: none is applied outside motion-safe, so with
prefers-reduced-motion: reduce the popover can mount fully visible but remain non-interactive for
the full CSSTransition timeout. This is user-visible on click-trigger dialogs that rely on the
default showDelay timeout (e.g. Info).
Code

packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[R89-91]

Evidence
pointer-events: none is outside the motion-safe media query, while the transition/opacity rules
are inside it. Since DialogContent.tsx always applies these transition classes for showDelay ms
(default 100ms), reduced-motion users can get a visible dialog that cannot be clicked during that
window; click-trigger dialogs like Info don’t override showDelay, so they use the default
behavior.

packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[89-107]
packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[177-200]
packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.tsx[226-233]
packages/components/dialog/src/Dialog/Dialog.tsx[33-44]
packages/core/src/components/Info/Info.tsx[59-67]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pointer-events: none` is currently applied unconditionally on `*.AppearActive` classes. When `prefers-reduced-motion: reduce` is set, the `motion-safe` blocks don’t apply (no transition/opacity/transform rules), but the CSSTransition classes still apply and keep the content non-interactive for the whole timeout.
### Issue Context
- `DialogContent.tsx` always runs `CSSTransition` with `timeout={showDelay}`.
- Reduced-motion users should get the popover “at rest immediately”, which also implies it should be interactive immediately.
### Fix Focus Areas
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[89-106]
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[177-200]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (4)
4. DialogContent timeout mismatch 🐞 Bug ≡ Correctness
Description
DialogContent’s CSS now uses --motion-productive-long (150ms) for expand transitions, but
DialogContent.tsx still ends the transition after timeout={showDelay} (default 100ms). This
removes the active transition classes early, truncating the animation and making the new token
timing not actually take effect.
Code

packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[R111-114]

Evidence
The refreshed SCSS sets transition: ... var(--motion-productive-long) and the token value is
150ms, but the CSSTransition timeout is still showDelay (default 100ms). That means the
appearActive class (which defines the transition) can be removed at 100ms, preventing the 150ms
token-based timing from completing.

packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[109-114]
packages/style/src/motion.scss[1-6]
packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.tsx[226-233]
packages/components/dialog/src/Dialog/Dialog.tsx[33-44]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The JS-driven transition lifecycle duration (CSSTransition `timeout`) is shorter than the new CSS transition duration (150ms). This causes class removal before the CSS transition completes, effectively cutting animations short.
### Issue Context
- `Dialog.tsx` already uses `showDelay` to delay opening; `showDelay` should not also be used as the animation duration.
- Motion tokens define `--motion-productive-long: 150ms`.
### Fix Focus Areas
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.tsx[185-233]
- packages/components/dialog/src/Dialog/Dialog.tsx[33-45]
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[109-114]
- packages/style/src/motion.scss[1-6]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. SCSS imports added in Modal.module.scss 📘 Rule violation ⚙ Maintainability
Description
Multiple CSS Module stylesheets (Modal.module.scss, Skeleton.module.scss, Toast.module.scss,
DialogContent.module.scss, and Tab.module.scss) introduce new SCSS @import statements even
though .module.scss files must not contain imports. This violates the repository’s CSS Modules
styling compliance constraints and can undermine encapsulation guarantees.
Code

packages/core/src/components/Modal/Modal/Modal.module.scss[R1-2]

Evidence
Rule 6 explicitly prohibits any @import usage in .module.scss files. The PR adds import lines at
the top of several CSS Module files: two @import lines in Modal.module.scss, two in
Skeleton.module.scss, two in Toast.module.scss, and single @import lines in both
DialogContent.module.scss and Tab.module.scss, directly contradicting the rule’s requirement
that CSS Modules contain no SCSS imports.

CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports
packages/core/src/components/Modal/Modal/Modal.module.scss[1-2]
packages/core/src/components/Skeleton/Skeleton.module.scss[1-2]
packages/core/src/components/Toast/Toast.module.scss[1-2]
packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[2-2]
packages/core/src/components/Tabs/Tab/Tab.module.scss[2-2]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several `.module.scss` (CSS Modules) files add SCSS `@import` statements, but repository Rule 6 disallows any imports in `.module.scss` files.
## Issue Context
The added imports appear to be used to access motion-related mixins/helpers (e.g., `motion-safe` / `motion-reduce`, reduced-motion gating, and conditional transitions). To comply, remove per-file SCSS imports and replace mixin usage with direct media-query wrappers such as `@media (prefers-reduced-motion: no-preference)` and `@media (prefers-reduced-motion: reduce)`, or use another no-import compliant mechanism (e.g., a build-time injection approach) that does not require imports inside CSS Modules.
## Fix Focus Areas
- packages/core/src/components/Modal/Modal/Modal.module.scss[1-2]
- packages/core/src/components/Skeleton/Skeleton.module.scss[1-2]
- packages/core/src/components/Toast/Toast.module.scss[1-2]
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[2-2]
- packages/core/src/components/Tabs/Tab/Tab.module.scss[2-2]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Reduced-motion modal lingers 🐞 Bug ≡ Correctness
Description
Modal.module.scss gates all exit styles under motion-safe, but Modal.tsx still uses a 150ms
CSSTransition exit timeout. With prefers-reduced-motion: reduce, the modal stays visible and
focus-locked for the full 150ms because no reduced-motion exit styles run and unmount is delayed.
Code

packages/core/src/components/Modal/Modal/Modal.module.scss[R166-185]

Evidence
The SCSS change removes all exit-state visual changes for reduced-motion users, but the React
Transition Group timeout still governs when the modal unmounts; therefore the modal remains rendered
(and focus-locked) for 150ms after close.

packages/core/src/components/Modal/Modal/Modal.module.scss[119-185]
packages/core/src/components/Modal/Modal/Modal.tsx[138-149]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Modal.module.scss` wraps `.containerExitActive` entirely in `@include motion-safe`, so reduced-motion users get no exit animation styles. However, `Modal.tsx` still delays unmount by `timeout.exit` (150ms), causing the modal + focus lock to remain active/visible briefly after close.
### Issue Context
`CSSTransition` with `unmountOnExit` will keep children mounted until the timeout elapses, regardless of whether CSS animations are present.
### Fix Focus Areas
- packages/core/src/components/Modal/Modal/Modal.tsx[138-149]
- packages/core/src/components/Modal/Modal/Modal.module.scss[119-185]
### Suggested fix
1. Detect reduced motion in `Modal.tsx` (e.g. `window.matchMedia('(prefers-reduced-motion: reduce)')` or a small hook).
2. When reduced motion is enabled, pass `timeout={{ enter: 0, exit: 0 }}` (or `timeout={0}`) to `CSSTransition` so the modal unmounts immediately.
3. (Optional defense-in-depth) Add `@include motion-reduce` styles to `.containerExitActive` to hide/passthrough pointer events in case a non-zero timeout is ever used again.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Invisible toast intercepts clicks 🐞 Bug ≡ Correctness
Description
Toast.module.scss sets reduced-motion exit to opacity: 0 only, while Toast.tsx keeps CSSTransition
timeout={400} with unmountOnExit. This leaves an invisible, fixed-position toast mounted and
still pointer-interactive for up to 400ms, potentially blocking clicks in that area.
Code

packages/core/src/components/Toast/Toast.module.scss[R104-112]

Evidence
The reduced-motion CSS removes the slide-out animation but does not prevent the node from remaining
mounted for the 400ms transition timeout, leaving an invisible fixed element that can still capture
pointer events.

packages/core/src/components/Toast/Toast.module.scss[98-112]
packages/core/src/components/Toast/Toast.tsx[179-186]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In reduced motion, `.exitActive` only sets `opacity: 0`, but the toast remains mounted for the full CSSTransition timeout (400ms). Because the element is `position: fixed` and pointer events are not disabled, it can intercept clicks while invisible.
### Issue Context
`CSSTransition` with `unmountOnExit` keeps the DOM node around until the timeout elapses.
### Fix Focus Areas
- packages/core/src/components/Toast/Toast.module.scss[98-112]
- packages/core/src/components/Toast/Toast.tsx[179-186]
### Suggested fix
- In `Toast.module.scss` under `@include motion-reduce` for `.exitActive`, add `pointer-events: none` (and optionally `visibility: hidden` or a transform that moves it out of the way) so it can’t block interaction while waiting to unmount.
- (Optional) If you want truly instant removal for reduced-motion users, conditionally set `CSSTransition` `timeout={0}` when `prefers-reduced-motion: reduce` (ensure this won’t break SSR expectations for Toast).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

8. Submenu lingers on reduce 🐞 Bug ≡ Correctness
Description
MenuItemSubMenu always delays unmounting by 100ms on close, but its .exitActive visual changes are
only defined inside motion-safe, so under prefers-reduced-motion: reduce the submenu remains
visible and clickable until the timeout elapses.
Code

packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[R60-67]

Evidence
The component unmount is delayed by the exit timeout, while the only exit visual change
(submenuExpandOut animation) is wrapped in motion-safe (i.e., only under
prefers-reduced-motion: no-preference), so reduced-motion users get no exit styling during the
delay. Toast demonstrates the intended pattern by adding a motion-reduce fallback inside
.exitActive to hide immediately during the unmount delay.

packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[56-86]
packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss[50-61]
packages/style/src/mixins/_motion.scss[5-14]
packages/core/src/components/Toast/Toast.module.scss[98-112]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
MenuItemSubMenu uses `CSSTransition` with `unmountOnExit` and an `exit` timeout of 100ms, but its `.exitActive` class only applies an exit animation inside `@include motion-safe`. When `prefers-reduced-motion: reduce` is set, no exit styles apply during that 100ms window, so the submenu stays visible/interactable until unmount.
### Issue Context
- Exit unmount is delayed by `timeout.exit`.
- `.exitActive` currently has no `motion-reduce` fallback (unlike Toast, which explicitly sets `opacity: 0` under `motion-reduce` while waiting for unmount).
### Fix Focus Areas
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[59-85]
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss[50-61]
### Suggested fix
Add a reduced-motion fallback to `.exitActive` so the submenu becomes non-visible immediately when closing under reduced motion, e.g.:
- `@include motion-reduce { opacity: 0; }` (optionally also `pointer-events: none;`)
Alternative (more invasive): detect reduced-motion in JS and set `timeout.exit` to 0 for that case.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Skeleton uses 0.8s duration 📘 Rule violation ⚙ Maintainability
Description
Skeleton.module.scss hardcodes 0.8s in an animation shorthand instead of using motion duration
design tokens. This reduces consistency and may bypass token-based theming/standards.
Code

packages/core/src/components/Skeleton/Skeleton.module.scss[R8-10]

Evidence
PR Compliance ID 5 requires using design tokens instead of hardcoded values where tokens exist. The
updated shine-animation sets animation: shine 0.8s ..., introducing a hardcoded duration in the
changed code.

CLAUDE.md: SCSS styles must use design tokens: CLAUDE.md: SCSS styles must use design tokens: CLAUDE.md: SCSS styles must use design tokens: CLAUDE.md: SCSS styles must use design tokens: CLAUDE.md: SCSS styles must use design tokens: CLAUDE.md: SCSS styles must use design tokens: CLAUDE.md: SCSS styles must use design tokens: CLAUDE.md: SCSS styles must use design tokens
packages/core/src/components/Skeleton/Skeleton.module.scss[8-10]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The skeleton shine animation uses a hardcoded duration (`0.8s`) instead of a motion duration token.
## Issue Context
The design system defines motion duration tokens (e.g. `--motion-productive-*`, `--motion-expressive-*`) and the compliance rule requires using tokens where available.
## Fix Focus Areas
- packages/core/src/components/Skeleton/Skeleton.module.scss[8-10]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

10. Hardcoded submenu timeouts 🐞 Bug ⚙ Maintainability
Description
MenuItemSubMenu hardcodes CSSTransition timeouts (150/100ms) while its CSS animations use motion
tokens (--motion-productive-long/medium), so any token change/override can desync unmount timing
and truncate/extend the animation.
Code

packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[66]

Evidence
The TSX uses numeric timeouts while the SCSS uses CSS variables for animation duration; the token
values are defined in the style package, so changing those variables (or overriding them) would not
update the JS timeout, causing timing drift.

packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[60-71]
packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss[50-61]
packages/style/src/motion.scss[1-6]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
MenuItemSubMenu’s `CSSTransition` uses hardcoded millisecond timeouts, but the CSS uses tokenized durations via CSS variables. This duplicates timing in two places and risks drift if token values change.
### Issue Context
- CSS duration comes from `--motion-productive-long` / `--motion-productive-medium`.
- JS unmount/class timing comes from `timeout={{ appear: 150, enter: 150, exit: 100 }}`.
### Fix Focus Areas
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[60-67]
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss[50-61]
- packages/style/src/motion.scss[1-6]
### Suggested fix
Pick one source of truth:
- (Lightweight) Add a code comment tying 150/100 to the token names and update policy.
- (Better) Export JS motion constants (e.g., from a tokens package) and use those for `timeout`, keeping CSS vars as the styling layer.
- (Advanced) Read computed CSS vars at runtime (requires DOM access and caching), then pass into `timeout`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 97ae8b0

Results up to commit N/A


🐞 Bugs (6) 📘 Rule violations (3) 📎 Requirement gaps (0)


Action required
1. MenuItemSubMenu root lacks data-vibe 📘 Rule violation ◔ Observability
Description
The updated root element for MenuItemSubMenu does not include the required [data-vibe]
attribute. This breaks the standard component identification/instrumentation requirement.
Code

packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[59]

Evidence
PR Compliance ID 3 requires a root [data-vibe] attribute. The root `` added/modified in
MenuItemSubMenu includes data-popper-placement but no data-vibe.

CLAUDE.md: React Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: React Components Must Use forwardRef and Include a Root [data-vibe] Attribute
packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[58-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`MenuItemSubMenu`'s root rendered element is missing the required `[data-vibe]` attribute.
## Issue Context
Compliance requires all React component roots to include `[data-vibe]` for consistent identification/instrumentation.
## Fix Focus Areas
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[58-60]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Reduced-motion blocks clicks 🐞 Bug ≡ Correctness
Description
In DialogContent animations, pointer-events: none is applied outside motion-safe, so with
prefers-reduced-motion: reduce the popover can mount fully visible but remain non-interactive for
the full CSSTransition timeout. This is user-visible on click-trigger dialogs that rely on the
default showDelay timeout (e.g. Info).
Code

packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[R89-91]

Evidence
pointer-events: none is outside the motion-safe media query, while the transition/opacity rules
are inside it. Since DialogContent.tsx always applies these transition classes for showDelay ms
(default 100ms), reduced-motion users can get a visible dialog that cannot be clicked during that
window; click-trigger dialogs like Info don’t override showDelay, so they use the default
behavior.

packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[89-107]
packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[177-200]
packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.tsx[226-233]
packages/components/dialog/src/Dialog/Dialog.tsx[33-44]
packages/core/src/components/Info/Info.tsx[59-67]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pointer-events: none` is currently applied unconditionally on `*.AppearActive` classes. When `prefers-reduced-motion: reduce` is set, the `motion-safe` blocks don’t apply (no transition/opacity/transform rules), but the CSSTransition classes still apply and keep the content non-interactive for the whole timeout.
### Issue Context
- `DialogContent.tsx` always runs `CSSTransition` with `timeout={showDelay}`.
- Reduced-motion users should get the popover “at rest immediately”, which also implies it should be interactive immediately.
### Fix Focus Areas
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[89-106]
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[177-200]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. DialogContent timeout mismatch 🐞 Bug ≡ Correctness
Description
DialogContent’s CSS now uses --motion-productive-long (150ms) for expand transitions, but
DialogContent.tsx still ends the transition after timeout={showDelay} (default 100ms). This
removes the active transition classes early, truncating the animation and making the new token
timing not actually take effect.
Code

packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[R111-114]

Evidence
The refreshed SCSS sets transition: ... var(--motion-productive-long) and the token value is
150ms, but the CSSTransition timeout is still showDelay (default 100ms). That means the
appearActive class (which defines the transition) can be removed at 100ms, preventing the 150ms
token-based timing from completing.

packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[109-114]
packages/style/src/motion.scss[1-6]
packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.tsx[226-233]
packages/components/dialog/src/Dialog/Dialog.tsx[33-44]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The JS-driven transition lifecycle duration (CSSTransition `timeout`) is shorter than the new CSS transition duration (150ms). This causes class removal before the CSS transition completes, effectively cutting animations short.
### Issue Context
- `Dialog.tsx` already uses `showDelay` to delay opening; `showDelay` should not also be used as the animation duration.
- Motion tokens define `--motion-productive-long: 150ms`.
### Fix Focus Areas
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.tsx[185-233]
- packages/components/dialog/src/Dialog/Dialog.tsx[33-45]
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[109-114]
- packages/style/src/motion.scss[1-6]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
4. SCSS imports added in Modal.module.scss 📘 Rule violation ⚙ Maintainability
Description
Multiple CSS Module stylesheets (Modal.module.scss, Skeleton.module.scss, Toast.module.scss,
DialogContent.module.scss, and Tab.module.scss) introduce new SCSS @import statements even
though .module.scss files must not contain imports. This violates the repository’s CSS Modules
styling compliance constraints and can undermine encapsulation guarantees.
Code

packages/core/src/components/Modal/Modal/Modal.module.scss[R1-2]

Evidence
Rule 6 explicitly prohibits any @import usage in .module.scss files. The PR adds import lines at
the top of several CSS Module files: two @import lines in Modal.module.scss, two in
Skeleton.module.scss, two in Toast.module.scss, and single @import lines in both
DialogContent.module.scss and Tab.module.scss, directly contradicting the rule’s requirement
that CSS Modules contain no SCSS imports.

CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports: CLAUDE.md: Component Styles Must Use CSS Modules and Design Tokens, and .module.scss Files Must Not Use Imports
packages/core/src/components/Modal/Modal/Modal.module.scss[1-2]
packages/core/src/components/Skeleton/Skeleton.module.scss[1-2]
packages/core/src/components/Toast/Toast.module.scss[1-2]
packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[2-2]
packages/core/src/components/Tabs/Tab/Tab.module.scss[2-2]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several `.module.scss` (CSS Modules) files add SCSS `@import` statements, but repository Rule 6 disallows any imports in `.module.scss` files.
## Issue Context
The added imports appear to be used to access motion-related mixins/helpers (e.g., `motion-safe` / `motion-reduce`, reduced-motion gating, and conditional transitions). To comply, remove per-file SCSS imports and replace mixin usage with direct media-query wrappers such as `@media (prefers-reduced-motion: no-preference)` and `@media (prefers-reduced-motion: reduce)`, or use another no-import compliant mechanism (e.g., a build-time injection approach) that does not require imports inside CSS Modules.
## Fix Focus Areas
- packages/core/src/components/Modal/Modal/Modal.module.scss[1-2]
- packages/core/src/components/Skeleton/Skeleton.module.scss[1-2]
- packages/core/src/components/Toast/Toast.module.scss[1-2]
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[2-2]
- packages/core/src/components/Tabs/Tab/Tab.module.scss[2-2]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Reduced-motion modal lingers 🐞 Bug ≡ Correctness
Description
Modal.module.scss gates all exit styles under motion-safe, but Modal.tsx still uses a 150ms
CSSTransition exit timeout. With prefers-reduced-motion: reduce, the modal stays visible and
focus-locked for the full 150ms because no reduced-motion exit styles run and unmount is delayed.
Code

packages/core/src/components/Modal/Modal/Modal.module.scss[R166-185]

Evidence
The SCSS change removes all exit-state visual changes for reduced-motion users, but the React
Transition Group timeout still governs when the modal unmounts; therefore the modal remains rendered
(and focus-locked) for 150ms after close.

packages/core/src/components/Modal/Modal/Modal.module.scss[119-185]
packages/core/src/components/Modal/Modal/Modal.tsx[138-149]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Modal.module.scss` wraps `.containerExitActive` entirely in `@include motion-safe`, so reduced-motion users get no exit animation styles. However, `Modal.tsx` still delays unmount by `timeout.exit` (150ms), causing the modal + focus lock to remain active/visible briefly after close.
### Issue Context
`CSSTransition` with `unmountOnExit` will keep children mounted until the timeout elapses, regardless of whether CSS animations are present.
### Fix Focus Areas
- packages/core/src/components/Modal/Modal/Modal.tsx[138-149]
- packages/core/src/components/Modal/Modal/Modal.module.scss[119-185]
### Suggested fix
1. Detect reduced motion in `Modal.tsx` (e.g. `window.matchMedia('(prefers-reduced-motion: reduce)')` or a small hook).
2. When reduced motion is enabled, pass `timeout={{ enter: 0, exit: 0 }}` (or `timeout={0}`) to `CSSTransition` so the modal unmounts immediately.
3. (Optional defense-in-depth) Add `@include motion-reduce` styles to `.containerExitActive` to hide/passthrough pointer events in case a non-zero timeout is ever used again.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Invisible toast intercepts clicks 🐞 Bug ≡ Correctness
Description
Toast.module.scss sets reduced-motion exit to opacity: 0 only, while Toast.tsx keeps CSSTransition
timeout={400} with unmountOnExit. This leaves an invisible, fixed-position toast mounted and
still pointer-interactive for up to 400ms, potentially blocking clicks in that area.
Code

packages/core/src/components/Toast/Toast.module.scss[R104-112]

Evidence
The reduced-motion CSS removes the slide-out animation but does not prevent the node from remaining
mounted for the 400ms transition timeout, leaving an invisible fixed element that can still capture
pointer events.

packages/core/src/components/Toast/Toast.module.scss[98-112]
packages/core/src/components/Toast/Toast.tsx[179-186]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In reduced motion, `.exitActive` only sets `opacity: 0`, but the toast remains mounted for the full CSSTransition timeout (400ms). Because the element is `position: fixed` and pointer events are not disabled, it can intercept clicks while invisible.
### Issue Context
`CSSTransition` with `unmountOnExit` keeps the DOM node around until the timeout elapses.
### Fix Focus Areas
- packages/core/src/components/Toast/Toast.module.scss[98-112]
- packages/core/src/components/Toast/Toast.tsx[179-186]
### Suggested fix
- In `Toast.module.scss` under `@include motion-reduce` for `.exitActive`, add `pointer-events: none` (and optionally `visibility: hidden` or a transform that moves it out of the way) so it can’t block interaction while waiting to unmount.
- (Optional) If you want truly instant removal for reduced-motion users, conditionally set `CSSTransition` `timeout={0}` when `prefers-reduced-motion: reduce` (ensure this won’t break SSR expectations for Toast).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
7. Submenu lingers on reduce 🐞 Bug ≡ Correctness
Description
MenuItemSubMenu always delays unmounting by 100ms on close, but its .exitActive visual changes are
only defined inside motion-safe, so under prefers-reduced-motion: reduce the submenu remains
visible and clickable until the timeout elapses.
Code

packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[R60-67]

Evidence
The component unmount is delayed by the exit timeout, while the only exit visual change
(submenuExpandOut animation) is wrapped in motion-safe (i.e., only under
prefers-reduced-motion: no-preference), so reduced-motion users get no exit styling during the
delay. Toast demonstrates the intended pattern by adding a motion-reduce fallback inside
.exitActive to hide immediately during the unmount delay.

packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[56-86]
packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss[50-61]
packages/style/src/mixins/_motion.scss[5-14]
packages/core/src/components/Toast/Toast.module.scss[98-112]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
MenuItemSubMenu uses `CSSTransition` with `unmountOnExit` and an `exit` timeout of 100ms, but its `.exitActive` class only applies an exit animation inside `@include motion-safe`. When `prefers-reduced-motion: reduce` is set, no exit styles apply during that 100ms window, so the submenu stays visible/interactable until unmount.
### Issue Context
- Exit unmount is delayed by `timeout.exit`.
- `.exitActive` currently has no `motion-reduce` fallback (unlike Toast, which explicitly sets `opacity: 0` under `motion-reduce` while waiting for unmount).
### Fix Focus Areas
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[59-85]
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss[50-61]
### Suggested fix
Add a reduced-motion fallback to `.exitActive` so the submenu becomes non-visible immediately when closing under reduced motion, e.g.:
- `@include motion-reduce { opacity: 0; }` (optionally also `pointer-events: none;`)
Alternative (more invasive): detect reduced-motion in JS and set `timeout.exit` to 0 for that case.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Skeleton uses 0.8s duration 📘 Rule violation ⚙ Maintainability
Description
Skeleton.module.scss hardcodes 0.8s in an animation shorthand instead of using motion duration
design tokens. This reduces consistency and may bypass token-based theming/standards.
Code

packages/core/src/components/Skeleton/Skeleton.module.scss[R8-10]

Evidence
PR Compliance ID 5 requires using design tokens instead of hardcoded values where tokens exist. The
updated shine-animation sets animation: shine 0.8s ..., introducing a hardcoded duration in the
changed code.

CLAUDE.md: SCSS styles must use design tokens: CLAUDE.md: SCSS styles must use design tokens: CLAUDE.md: SCSS styles must use design tokens: CLAUDE.md: SCSS styles must use design tokens
packages/core/src/components/Skeleton/Skeleton.module.scss[8-10]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The skeleton shine animation uses a hardcoded duration (`0.8s`) instead of a motion duration token.
## Issue Context
The design system defines motion duration tokens (e.g. `--motion-productive-*`, `--motion-expressive-*`) and the compliance rule requires using tokens where available.
## Fix Focus Areas
- packages/core/src/components/Skeleton/Skeleton.module.scss[8-10]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments
9. Hardcoded submenu timeouts 🐞 Bug ⚙ Maintainability
Description
MenuItemSubMenu hardcodes CSSTransition timeouts (150/100ms) while its CSS animations use motion
tokens (--motion-productive-long/medium), so any token change/override can desync unmount timing
and truncate/extend the animation.
Code

packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[66]

Evidence
The TSX uses numeric timeouts while the SCSS uses CSS variables for animation duration; the token
values are defined in the style package, so changing those variables (or overriding them) would not
update the JS timeout, causing timing drift.

[packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[60-71]](https://github...

Comment on lines +1 to +2
@import "~@vibe/style/dist/mixins";
@import "~@vibe/style/dist/mixins/motion";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Scss imports added in modal.module.scss 📘 Rule violation ⚙ Maintainability

Multiple CSS Module stylesheets (Modal.module.scss, Skeleton.module.scss, Toast.module.scss,
DialogContent.module.scss, and Tab.module.scss) introduce new SCSS @import statements even
though .module.scss files must not contain imports. This violates the repository’s CSS Modules
styling compliance constraints and can undermine encapsulation guarantees.
Agent Prompt
## Issue description
Several `.module.scss` (CSS Modules) files add SCSS `@import` statements, but repository Rule 6 disallows any imports in `.module.scss` files.

## Issue Context
The added imports appear to be used to access motion-related mixins/helpers (e.g., `motion-safe` / `motion-reduce`, reduced-motion gating, and conditional transitions). To comply, remove per-file SCSS imports and replace mixin usage with direct media-query wrappers such as `@media (prefers-reduced-motion: no-preference)` and `@media (prefers-reduced-motion: reduce)`, or use another no-import compliant mechanism (e.g., a build-time injection approach) that does not require imports inside CSS Modules.

## Fix Focus Areas
- packages/core/src/components/Modal/Modal/Modal.module.scss[1-2]
- packages/core/src/components/Skeleton/Skeleton.module.scss[1-2]
- packages/core/src/components/Toast/Toast.module.scss[1-2]
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[2-2]
- packages/core/src/components/Tabs/Tab/Tab.module.scss[2-2]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 166 to 185
.containerExitActive {
.overlay {
opacity: 0;
transition: opacity 100ms cubic-bezier(0.6, 0, 1, 1);
}
@include motion-safe {
.overlay {
opacity: 0;
transition: opacity var(--motion-productive-medium) var(--motion-timing-exit);
}

.centerPop {
animation: centerPopOut 100ms cubic-bezier(0.6, 0, 1, 1) forwards;
}
.centerPop {
animation: centerPopOut var(--motion-productive-long) var(--motion-timing-exit) forwards;
}

.anchorPop {
animation: anchorPopOut 150ms cubic-bezier(0.6, 0, 1, 1) forwards;
}
.anchorPop {
animation: anchorPopOut var(--motion-productive-long) var(--motion-timing-exit) forwards;
}

.fullView {
animation: fullViewOut 100ms cubic-bezier(0.6, 0, 1, 1) forwards;
.fullView {
animation: fullViewOut var(--motion-productive-medium) var(--motion-timing-exit) forwards;
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Reduced-motion modal lingers 🐞 Bug ≡ Correctness

Modal.module.scss gates all exit styles under motion-safe, but Modal.tsx still uses a 150ms
CSSTransition exit timeout. With prefers-reduced-motion: reduce, the modal stays visible and
focus-locked for the full 150ms because no reduced-motion exit styles run and unmount is delayed.
Agent Prompt
### Issue description
`Modal.module.scss` wraps `.containerExitActive` entirely in `@include motion-safe`, so reduced-motion users get no exit animation styles. However, `Modal.tsx` still delays unmount by `timeout.exit` (150ms), causing the modal + focus lock to remain active/visible briefly after close.

### Issue Context
`CSSTransition` with `unmountOnExit` will keep children mounted until the timeout elapses, regardless of whether CSS animations are present.

### Fix Focus Areas
- packages/core/src/components/Modal/Modal/Modal.tsx[138-149]
- packages/core/src/components/Modal/Modal/Modal.module.scss[119-185]

### Suggested fix
1. Detect reduced motion in `Modal.tsx` (e.g. `window.matchMedia('(prefers-reduced-motion: reduce)')` or a small hook).
2. When reduced motion is enabled, pass `timeout={{ enter: 0, exit: 0 }}` (or `timeout={0}`) to `CSSTransition` so the modal unmounts immediately.
3. (Optional defense-in-depth) Add `@include motion-reduce` styles to `.containerExitActive` to hide/passthrough pointer events in case a non-zero timeout is ever used again.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 104 to 112
.exitActive {
animation-iteration-count: 1;
animation-fill-mode: forwards;
animation-name: slideOut;
animation-duration: 350ms;
animation-timing-function: cubic-bezier(0.6, 0, 0.4, 1);
@include motion-safe {
animation: slideOut var(--motion-productive-long) var(--motion-timing-exit) forwards;
}

@include motion-reduce {
opacity: 0;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Invisible toast intercepts clicks 🐞 Bug ≡ Correctness

Toast.module.scss sets reduced-motion exit to opacity: 0 only, while Toast.tsx keeps CSSTransition
timeout={400} with unmountOnExit. This leaves an invisible, fixed-position toast mounted and
still pointer-interactive for up to 400ms, potentially blocking clicks in that area.
Agent Prompt
### Issue description
In reduced motion, `.exitActive` only sets `opacity: 0`, but the toast remains mounted for the full CSSTransition timeout (400ms). Because the element is `position: fixed` and pointer events are not disabled, it can intercept clicks while invisible.

### Issue Context
`CSSTransition` with `unmountOnExit` keeps the DOM node around until the timeout elapses.

### Fix Focus Areas
- packages/core/src/components/Toast/Toast.module.scss[98-112]
- packages/core/src/components/Toast/Toast.tsx[179-186]

### Suggested fix
- In `Toast.module.scss` under `@include motion-reduce` for `.exitActive`, add `pointer-events: none` (and optionally `visibility: hidden` or a transform that moves it out of the way) so it can’t block interaction while waiting to unmount.
- (Optional) If you want truly instant removal for reduced-motion users, conditionally set `CSSTransition` `timeout={0}` when `prefers-reduced-motion: reduce` (ensure this won’t break SSR expectations for Toast).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@github-actions

github-actions Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Analysis

✅ No bundle size changes detected.

Unchanged Components
Component Base PR Diff
@vibe/button 17.3KB 17.29KB -9B 🟢
@vibe/clickable 5.95KB 5.96KB +3B 🔺
@vibe/dialog 52.14KB 52.24KB +95B 🔺
@vibe/icon-button 66.09KB 66.06KB -26B 🟢
@vibe/icon 12.92KB 12.89KB -32B 🟢
@vibe/layer 2.96KB 2.96KB 0B ➖
@vibe/layout 9.82KB 9.83KB +11B 🔺
@vibe/loader 5.64KB 5.65KB +10B 🔺
@vibe/tooltip 61.33KB 61.4KB +67B 🔺
@vibe/typography 63.47KB 63.46KB -17B 🟢
Accordion 6.31KB 6.29KB -14B 🟢
AccordionItem 66.43KB 66.45KB +17B 🔺
AlertBanner 70.83KB 70.85KB +16B 🔺
AlertBannerButton 18.76KB 18.76KB -2B 🟢
AlertBannerLink 15.26KB 15.26KB +4B 🔺
AlertBannerText 63.95KB 63.98KB +34B 🔺
AttentionBox 74.35KB 74.4KB +52B 🔺
Avatar 66.84KB 66.86KB +17B 🔺
AvatarGroup 93.29KB 93.55KB +270B 🔺
Badge 43.19KB 43.17KB -24B 🟢
BreadcrumbItem 64.7KB 64.69KB -15B 🟢
BreadcrumbMenu 68.57KB 68.63KB +62B 🔺
BreadcrumbMenuItem 77.07KB 77.34KB +273B 🔺
BreadcrumbsBar 5.68KB 5.68KB -1B 🟢
ButtonGroup 68.32KB 68.39KB +72B 🔺
Checkbox 66.83KB 66.85KB +26B 🔺
Chips 75.05KB 75.14KB +92B 🔺
ColorPicker 74.47KB 74.53KB +53B 🔺
ColorPickerContent 73.73KB 73.75KB +17B 🔺
Combobox 84.08KB 84.01KB -68B 🟢
Counter 42.21KB 42.28KB +65B 🔺
DatePicker 112.41KB 112.48KB +74B 🔺
Divider 5.42KB 5.46KB +44B 🔺
Dropdown 95.35KB 95.37KB +21B 🔺
EditableHeading 66.63KB 66.62KB -12B 🟢
EditableText 66.46KB 66.48KB +16B 🔺
EmptyState 70.48KB 70.53KB +46B 🔺
ExpandCollapse 66.22KB 66.31KB +86B 🔺
FormattedNumber 5.86KB 5.84KB -13B 🟢
GridKeyboardNavigationContext 4.65KB 4.65KB -4B 🟢
HiddenText 5.4KB 5.39KB -15B 🟢
Info 72.06KB 72.13KB +78B 🔺
Label 68.65KB 68.64KB -19B 🟢
Link 14.91KB 14.88KB -30B 🟢
List 72.88KB 72.93KB +53B 🔺
ListItem 65.54KB 65.52KB -23B 🟢
ListItemAvatar 66.88KB 66.95KB +62B 🔺
ListItemIcon 13.97KB 13.97KB +5B 🔺
ListTitle 65.02KB 65.06KB +44B 🔺
Menu 8.65KB 8.64KB -19B 🟢
MenuDivider 5.56KB 5.57KB +7B 🔺
MenuGridItem 7.16KB 7.19KB +36B 🔺
MenuItem 76.95KB 77.3KB +351B 🔺
MenuItemButton 70.11KB 70.12KB +10B 🔺
MenuTitle 65.35KB 65.44KB +88B 🔺
MenuButton 66.08KB 66.19KB +110B 🔺
Modal 79.14KB 79.05KB -92B 🟢
ModalContent 4.72KB 4.71KB -1B 🟢
ModalHeader 65.79KB 65.82KB +29B 🔺
ModalMedia 7.51KB 7.5KB -3B 🟢
ModalFooter 67.72KB 67.81KB +94B 🔺
ModalFooterWizard 68.6KB 68.58KB -14B 🟢
ModalBasicLayout 8.96KB 8.9KB -57B 🟢
ModalMediaLayout 8.08KB 8.06KB -19B 🟢
ModalSideBySideLayout 6.3KB 6.29KB -4B 🟢
MultiStepIndicator 52.96KB 52.95KB -11B 🟢
NumberField 72.87KB 72.84KB -34B 🟢
ProgressBar 7.34KB 7.35KB +7B 🔺
RadioButton 65.9KB 65.9KB -3B 🟢
Search 70.65KB 70.61KB -40B 🟢
Skeleton 6KB 6.05KB +45B 🔺
Slider 73.86KB 73.92KB +58B 🔺
SplitButton 66.48KB 66.6KB +124B 🔺
SplitButtonMenu 8.8KB 8.76KB -34B 🟢
Steps 71.31KB 71.31KB +4B 🔺
Table 7.26KB 7.25KB -13B 🟢
TableBody 66.68KB 66.69KB +13B 🔺
TableCell 65.22KB 65.27KB +50B 🔺
TableContainer 5.31KB 5.32KB +16B 🔺
TableHeader 5.64KB 5.64KB +1B 🔺
TableHeaderCell 72.2KB 72.22KB +30B 🔺
TableRow 5.56KB 5.55KB -8B 🟢
TableRowMenu 68.87KB 68.9KB +31B 🔺
TableVirtualizedBody 71.42KB 71.46KB +38B 🔺
Tab 64KB 64.06KB +58B 🔺
TabList 8.89KB 8.86KB -30B 🟢
TabPanel 5.3KB 5.29KB -14B 🟢
TabPanels 5.86KB 5.86KB -2B 🟢
TabsContext 5.48KB 5.51KB +29B 🔺
TextArea 66.26KB 66.38KB +121B 🔺
TextField 69.43KB 69.45KB +13B 🔺
TextWithHighlight 64.35KB 64.33KB -22B 🟢
ThemeProvider 4.36KB 4.36KB -1B 🟢
Tipseen 71.17KB 71.23KB +57B 🔺
TipseenContent 71.6KB 71.61KB +9B 🔺
TipseenMedia 71.27KB 71.31KB +34B 🔺
TipseenWizard 73.93KB 73.87KB -68B 🟢
Toast 74.1KB 73.97KB -136B 🟢
ToastButton 18.59KB 18.62KB +33B 🔺
ToastLink 15.05KB 15.08KB +31B 🔺
Toggle 66.62KB 66.72KB +108B 🔺
TransitionView 5.42KB 5.45KB +30B 🔺
VirtualizedGrid 12.54KB 12.54KB +2B 🔺
VirtualizedList 12.28KB 12.26KB -12B 🟢
List (Next) 8.17KB 8.16KB -15B 🟢
ListItem (Next) 69.88KB 69.96KB +87B 🔺
ListTitle (Next) 65.31KB 65.4KB +86B 🔺

📊 Summary:

  • Total Base Size: 4.75MB
  • Total PR Size: 4.76MB
  • Total Difference: +2.59KB

MenuItemSubMenu rendered DialogContentContainer directly with no transition
wrapper, so submenus appeared and disappeared instantly. Wrap the submenu
in CSSTransition with a placement-aware fade + scale-from-origin animation
that mirrors Tooltip's expand variant — keyed off useFloating's resolved
placement so the submenu grows out of the parent item's edge. Respects
prefers-reduced-motion via the shared motion-safe mixin.

Replaces the visibility-toggle render strategy with mount/unmount; updates
the corresponding test to assert presence rather than visibility style.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 8e58e40

Comment on lines 89 to 91
.opacitySlideAppearActive {
transition: opacity 0.2s ease, transform 0.2s ease-out;
opacity: 1;
pointer-events: none;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Reduced-motion blocks clicks 🐞 Bug ≡ Correctness

In DialogContent animations, pointer-events: none is applied outside motion-safe, so with
prefers-reduced-motion: reduce the popover can mount fully visible but remain non-interactive for
the full CSSTransition timeout. This is user-visible on click-trigger dialogs that rely on the
default showDelay timeout (e.g. Info).
Agent Prompt
### Issue description
`pointer-events: none` is currently applied unconditionally on `*.AppearActive` classes. When `prefers-reduced-motion: reduce` is set, the `motion-safe` blocks don’t apply (no transition/opacity/transform rules), but the CSSTransition classes still apply and keep the content non-interactive for the whole timeout.

### Issue Context
- `DialogContent.tsx` always runs `CSSTransition` with `timeout={showDelay}`.
- Reduced-motion users should get the popover “at rest immediately”, which also implies it should be interactive immediately.

### Fix Focus Areas
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[89-106]
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[177-200]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +111 to +114
@include motion-safe {
transition: transform var(--motion-productive-long) var(--motion-timing-enter),
opacity var(--motion-productive-long) var(--motion-timing-enter);
opacity: 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Dialogcontent timeout mismatch 🐞 Bug ≡ Correctness

DialogContent’s CSS now uses --motion-productive-long (150ms) for expand transitions, but
DialogContent.tsx still ends the transition after timeout={showDelay} (default 100ms). This
removes the active transition classes early, truncating the animation and making the new token
timing not actually take effect.
Agent Prompt
### Issue description
The JS-driven transition lifecycle duration (CSSTransition `timeout`) is shorter than the new CSS transition duration (150ms). This causes class removal before the CSS transition completes, effectively cutting animations short.

### Issue Context
- `Dialog.tsx` already uses `showDelay` to delay opening; `showDelay` should not also be used as the animation duration.
- Motion tokens define `--motion-productive-long: 150ms`.

### Fix Focus Areas
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.tsx[185-233]
- packages/components/dialog/src/Dialog/Dialog.tsx[33-45]
- packages/components/dialog/src/Dialog/components/DialogContent/DialogContent.module.scss[109-114]
- packages/style/src/motion.scss[1-6]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Submenus only animated on close — open was instant. CSSTransition with
mountOnEnter swaps the start/active classes in quick succession on a
freshly-mounted node, and the browser can land both in the same paint
cycle, skipping the "from" state entirely.

Replace the transition-based approach with CSS keyframe animations on the
active classes. Animations fire reliably on mount regardless of frame
timing. Placement classes are now applied statically to the wrapper so the
transform-origin is set before the animation runs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit ab961eb

})}
</DialogContentContainer>
)}
<div style={floatingStyles} ref={refs.setFloating} data-popper-placement={actualPlacement}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. menuitemsubmenu root lacks data-vibe 📘 Rule violation ◔ Observability

The updated root element for MenuItemSubMenu does not include the required [data-vibe]
attribute. This breaks the standard component identification/instrumentation requirement.
Agent Prompt
## Issue description
`MenuItemSubMenu`'s root rendered element is missing the required `[data-vibe]` attribute.

## Issue Context
Compliance requires all React component roots to include `[data-vibe]` for consistent identification/instrumentation.

## Fix Focus Areas
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[58-60]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

The submenu now animates closed via CSSTransition with unmountOnExit, so
the element stays mounted for ~100ms during the exit animation. Wrap the
post-close `not.toBeInTheDocument()` assertions in `waitFor` so they
await unmount instead of asserting synchronously.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 97ae8b0

Comment on lines +60 to +71
<CSSTransition
in={open}
appear
mountOnEnter
unmountOnExit
nodeRef={transitionRef}
timeout={{ appear: 150, enter: 150, exit: 100 }}
classNames={{
appearActive: styles.appearActive,
enterActive: styles.enterActive,
exitActive: styles.exitActive
}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Submenu enter flash risk 🐞 Bug ≡ Correctness

MenuItemSubMenu’s CSSTransition only provides *Active classNames (no appear/enter base classes), so
the submenu can mount at its resting (visible) styles for a paint and then snap to the keyframe
from state when enterActive is applied, producing a visible flash on open. DialogContent avoids
this by mapping an explicit appear class that sets the initial hidden/scale state before the
active transition begins.
Agent Prompt
### Issue description
`MenuItemSubMenu` wires `CSSTransition` with only `appearActive`/`enterActive`/`exitActive`. Because there is no `appear`/`enter` base class that sets the initial hidden/scale state, the submenu element can render once at its default (opacity 1 / scale 1) before the active class is applied, then immediately jump to the keyframe `from` state (opacity 0 / scale 0.92), causing a flash.

### Issue Context
This code intentionally uses keyframes to avoid missing the “from” frame on mount, but it still needs an initial base class (like DialogContent’s `expandAppear`) to ensure the element is already in the correct starting state before the `*Active` class is applied.

### Fix Focus Areas
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.tsx[58-72]
- packages/core/src/components/Menu/MenuItem/components/MenuItemSubMenu/MenuItemSubMenu.module.scss[50-61]

### Suggested fix
1. Add base class mappings in `CSSTransition.classNames`, e.g. `appear: styles.appear`, `enter: styles.enter` (and keep existing `appearActive`/`enterActive`/`exitActive`).
2. In the SCSS module, add `.appear` and `.enter` rules (under `@include motion-safe`) that set the initial state to match your keyframe `from` values (e.g. `opacity: 0; transform: scale(0.92);`).
3. Keep the placement transform-origin classes as-is (they only set `transform-origin`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant