Skip to content

refactor(ui): improved toast and notification animations using motion#2453

Merged
bajrangCoder merged 2 commits into
mainfrom
refactor/motion-toast-notifications
Jul 5, 2026
Merged

refactor(ui): improved toast and notification animations using motion#2453
bajrangCoder merged 2 commits into
mainfrom
refactor/motion-toast-notifications

Conversation

@bajrangCoder

Copy link
Copy Markdown
Member

No description provided.

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces CSS-keyframe/transition-based show/hide animations on the toast and notification overlays with the motion library's animate() API, introducing spring physics for the entrance effect and a timed easing for dismissal.

  • Toast (src/components/toast/): slow-appear CSS animation and .hide transition class are dropped; JS animate() now drives opacity/transform on both show and hide, and will-change is declared in CSS instead.
  • Notification manager (src/lib/notificationManager.js): The .hiding CSS class approach is replaced with a direct animate() call on the toast overlay element; pushNotification explicitly sets initial inline styles before starting the entrance spring.
  • Stylesheets: transition: all is narrowed to specific properties to avoid CSS transition interference, @keyframes toastSlideIn is removed, and the sidebar-panel-specific slideIn animation is moved to the scoped sidebar stylesheet.

Confidence Score: 5/5

The refactor is well-scoped — CSS keyframe animations are cleanly replaced by motion's WAAPI wrapper with no broken control paths.

All animation entry and exit paths remain functionally equivalent to the old CSS approach; the toast queue and notification lifecycle are unchanged. The only new finding (an orphaned @Keyframes slow-appear in keyframes.scss) is cosmetic dead CSS with no runtime impact.

src/styles/keyframes.scss — the @Keyframes slow-appear definition should be removed since its only caller was dropped in this PR.

Important Files Changed

Filename Overview
src/components/toast/index.js Replaces CSS animation with motion spring for show and easeIn for hide; logic is correct but will-change set in CSS and JS duplicates (flagged in previous review)
src/components/toast/style.scss Removes slow-appear animation and .hide class, adds will-change; clean change
src/lib/notificationManager.js Switches hideNotificationToast from CSS class to motion animate; inline will-change set but never reset (flagged in previous review)
src/main.scss Removes toastSlideIn animation and keyframe, adds will-change to notification-item; otherwise clean
src/sidebarApps/notification/style.scss Moves slideIn CSS animation into the sidebar-scoped stylesheet, correctly scoped to the notifications-container context
src/styles/notification.scss Removes .hiding class and global slideIn animation; narrows transition to specific properties to prevent interference with JS animations

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant ToastFn as "toast()"
    participant DOM
    participant Motion as "motion.animate()"

    Caller->>ToastFn: "toast(message, duration)"
    alt No existing toast in DOM
        ToastFn->>DOM: "app.append($toast)"
        ToastFn->>DOM: "set opacity=0, transform=translateY(20px)"
        ToastFn->>Motion: "spring {stiffness:300, damping:25}"
        Motion-->>DOM: "opacity→1, transform→translateY(0)"
        ToastFn->>ToastFn: "setTimeout(hide, duration||3000)"
        ToastFn->>Motion: "easeIn 0.25s → opacity=0, translateY(15px)"
        Motion-->>ToastFn: ".then() → remove(), show next in queue"
    else Existing toast present
        ToastFn->>ToastFn: "toastQueue.push(newToast)"
    end

    Caller->>ToastFn: "pushNotification(notification)"
    ToastFn->>DOM: "container.appendChild($toastEl)"
    ToastFn->>DOM: "set opacity=0, transform=translateX(100%)"
    ToastFn->>Motion: "spring {stiffness:300, damping:26}"
    Motion-->>DOM: "opacity→1, transform→translateX(0)"
    Caller->>ToastFn: "closeNotification(notification)"
    ToastFn->>Motion: "easeIn 0.25s → opacity=0, translateX(100%)"
    Motion-->>ToastFn: ".then() → notificationElement.remove()"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant ToastFn as "toast()"
    participant DOM
    participant Motion as "motion.animate()"

    Caller->>ToastFn: "toast(message, duration)"
    alt No existing toast in DOM
        ToastFn->>DOM: "app.append($toast)"
        ToastFn->>DOM: "set opacity=0, transform=translateY(20px)"
        ToastFn->>Motion: "spring {stiffness:300, damping:25}"
        Motion-->>DOM: "opacity→1, transform→translateY(0)"
        ToastFn->>ToastFn: "setTimeout(hide, duration||3000)"
        ToastFn->>Motion: "easeIn 0.25s → opacity=0, translateY(15px)"
        Motion-->>ToastFn: ".then() → remove(), show next in queue"
    else Existing toast present
        ToastFn->>ToastFn: "toastQueue.push(newToast)"
    end

    Caller->>ToastFn: "pushNotification(notification)"
    ToastFn->>DOM: "container.appendChild($toastEl)"
    ToastFn->>DOM: "set opacity=0, transform=translateX(100%)"
    ToastFn->>Motion: "spring {stiffness:300, damping:26}"
    Motion-->>DOM: "opacity→1, transform→translateX(0)"
    Caller->>ToastFn: "closeNotification(notification)"
    ToastFn->>Motion: "easeIn 0.25s → opacity=0, translateX(100%)"
    Motion-->>ToastFn: ".then() → notificationElement.remove()"
Loading

Reviews (2): Last reviewed commit: "fix" | Re-trigger Greptile

Comment thread src/styles/notification.scss Outdated
Comment thread src/components/toast/index.js Outdated
Comment thread src/lib/notificationManager.js Outdated
Comment on lines +303 to +305
$toastEl.style.opacity = "0";
$toastEl.style.transform = "translateX(100%)";
$toastEl.style.willChange = "transform, opacity";

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.

P2 will-change never reset after animation completes

$toastEl.style.willChange = "transform, opacity" is set before the show animation but never cleared afterwards. The browser retains a dedicated compositor layer for that element for the full 5-second toast lifetime even though the layer is only needed during the ~300 ms spring animation. Resetting to "auto" in the animate callback would release the compositor resource promptly. The same issue is present in toast/index.js, though there the element is removed shortly after so the impact is smaller.

@bajrangCoder

Copy link
Copy Markdown
Member Author

@greptile

@bajrangCoder bajrangCoder merged commit c9a4765 into main Jul 5, 2026
13 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in The Code Board - Acode Jul 5, 2026
@bajrangCoder bajrangCoder deleted the refactor/motion-toast-notifications branch July 5, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant