Skip to content

gh-15916: Fix sidebar, toolbar and URL-bar jumps in compact mode - #15775

Open
yosofbadr wants to merge 21 commits into
zen-browser:devfrom
yosofbadr:fix/sidebar-animation
Open

yosofbadr wants to merge 21 commits into
zen-browser:devfrom
yosofbadr:fix/sidebar-animation

Conversation

@yosofbadr

@yosofbadr yosofbadr commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #15916.

Summary

I recently switched from Arc to Zen and noticed that, in the "Sidebar and top toolbar" layout, toggling the sidebar with Cmd+S makes its contents jump vertically. Back, forward and reload also slide too far into the sidebar, and the compact-mode button shifts sideways.

It is a small visual issue, but I use this shortcut a lot and the movement makes the browser feel less polished.

This fix keeps the sidebar header controls steady while the sidebar slides. It reserves the header's space so the tab list does not jump, and keeps the navigation buttons within their intended path. The controls stay in normal layout so toolbar sizing and overflow still work. Temporary styles are restored after the transition.

Before and after

Side-by-side comparison of the "Sidebar and top toolbar" layout at half speed, before on the left and after on the right.

sidebar-comparison.mp4

Before, normal speed:

sidebar-before.mp4

After, normal speed:

sidebar-after.mp4

Both recordings use the same runtime, settings and fresh temporary profiles.

  • Before: the tab list jumps and toolbar controls shift during the transition.
  • After: the tab list stays at the same height and the controls move only where needed to open or close the sidebar.

The issue occurs in "Sidebar and top toolbar". The "Only sidebar" layout already behaves as intended and is unchanged by this fix.

The fix is limited to macOS, a left sidebar, left-to-right layout and "Sidebar and top toolbar", with only the sidebar hidden in compact mode. macOS reparents its left-hand window controls when compact mode changes. Windows and most Linux configurations place their caption controls differently, so this compensation is deliberately scoped to the layout I reproduced. Other layouts keep their existing animation path.

Right sidebar follow-up

These recordings show the right-sidebar extension prepared locally, which has not been pushed yet. Both use the same existing Firefox 157 runtime, settings and fresh temporary profiles. The animations are slowed to 25% speed for clarity.

Before, the tab list jumps 42 pixels and the header shifts during reopening. After, the tab list stays level and the header reaches its final position before the temporary styles are removed.

Side-by-side comparison, before on the left and after on the right:

right-sidebar-comparison.mp4

Before:

right-sidebar-before.mp4

After:

right-sidebar-after.mp4

The local extension passed 915 compact-mode checks, including a check for the header's position immediately before animation cleanup. JavaScript lint and the incremental UI build passed.

Top-bar follow-up

Comparison of the top-bar follow-up, before on top and after below. Both use the same runtime, settings and fresh temporary profiles, first at normal speed and then with the animation slowed to 25% speed. The before build already includes the right-sidebar fix.

top-bar-comparison-original-order.mp4

Updated recording of the top-bar fix with a right sidebar, first at normal speed and then with the animation slowed to 25% speed.

top-bar-after-original-order.mp4

Testing

The reviewed implementation passed 436 browser checks across compact mode and the existing URL-bar suites. The new tests sample the animation itself and cover expanded and collapsed sidebars, custom spacing, disabled animations, style cleanup and the RTL fallback. JavaScript lint and the incremental UI build passed.

I also tested it in my daily-use profile. Windows and Linux have not been tested. These results are from the existing local Firefox 157 runtime, rather than a fresh full build of the latest upstream base.

@mr-cheffy

Copy link
Copy Markdown
Member

Could I see the difference of before and after?

@yosofbadr

yosofbadr commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Could I see the difference of before and after?

Yes! Sorry I am still preparing the recordings. (Didn't expect you to be so fast 😂)

Edit: Updated PR with recordings.

@yosofbadr yosofbadr changed the title no-bug: Keep sidebar controls stable during compact-mode transitions no-bug: Fix sidebar jitter during compact-mode transitions Oct 6, 2026
@yosofbadr
yosofbadr marked this pull request as ready for review October 7, 2026 11:29
@yosofbadr
yosofbadr requested a review from mr-cheffy as a code owner October 7, 2026 11:29
@yosofbadr yosofbadr changed the title no-bug: Fix sidebar jitter during compact-mode transitions gh-13651: Fix sidebar jitter during compact-mode transitions Oct 13, 2026
@yosofbadr yosofbadr changed the title gh-13651: Fix sidebar jitter during compact-mode transitions gh-15916: Fix sidebar jitter during compact-mode transitions Oct 13, 2026
@yosofbadr

Copy link
Copy Markdown
Contributor Author

I updated the PR to also fix similar jitter when sidebar is on the right side.

@yosofbadr

Copy link
Copy Markdown
Contributor Author

@mr-cheffy The before/after recordings are in the PR description now. I added them after my earlier reply, so you may not have been notified. Since then I also fixed the same jitter for the right-side sidebar. Ready for another look when you have time.

yosofbadr and others added 4 commits October 16, 2026 12:47
Extend the compact-mode toolbar transition to sidebars on the right. The
right header changes width when it moves into the navbar, so measure its
destination and animate it along the path between the two layouts. The
tab list no longer jumps when the sidebar reopens.
Animate the navbar inset and release header or collapsed-sidebar space with the sidebar. Keep the menu before the sidebar button so the controls do not cross. Measure control destinations after applying the final margins and cancel temporary animations before restoring styles.

Regression checks reproduce the previous 5 px navigation snap and 30 px URL-bar edge jump. The toolbar suite passes 1,327 checks in both headless and headed runs; related width and URL-bar suites pass 136 checks. UI build and implementation ESLint pass. CSS lint has the same existing line 262 error on the baseline.
Remove the CSS override that moved the sidebar toggle after the menu in compact mode. Keep the toolbar margin animations and restore the original button positions in each layout. Replace the test that enforced the new order with checks for the original expanded and compact orders.

The new order checks fail with the override. After removal, 1,331 headless toolbar checks and 1,467 headed toolbar, width and URL-bar checks pass. The incremental UI build passes.
Keep four short explanations for layout constraints and remove comments that repeat the code. Preserve existing comments and the test copyright notice.

JavaScript token comparison and CSS comparison confirm no executable changes. Implementation ESLint, incremental UI build and 1,331 compact-mode checks pass.
@yosofbadr

Copy link
Copy Markdown
Contributor Author

Noticed another small visual issue with the URL bar "snapping" during right side bar animation, addressed this too. I feel now the animations across URL and left/right sidebar is much cleaner.

@yosofbadr yosofbadr changed the title gh-15916: Fix sidebar jitter during compact-mode transitions gh-15916: Fix sidebar, toolbar and URL-bar jumps in compact mode Oct 16, 2026
@mr-cheffy

Copy link
Copy Markdown
Member

This looks like a very complicated fix to such a minor issue... I'll clone it and check it out tomorrow

@yosofbadr

Copy link
Copy Markdown
Contributor Author

Hi @mr-cheffy, have you had a chance to try it out?

On the complexity: I agree it looks heavy for the size of the issue, so here is what the code is actually doing. When compact mode toggles, _updateEvent moves the window buttons and the sidebar header between the sidebar and the nav bar. The sidebar animates, but those controls change parents in one frame, which is the jump. The helper does three things to hide that: it holds the moved controls in place with a transform, animates the nav bar's inset so the URL bar follows the sidebar instead of snapping, and reserves the header height so the tab list stays level. The right-sidebar branch needs the same three things but has to measure the idle layout first, because its header finishes in a different place. That branch is most of the length.

I tried a smaller version that dropped the nav-bar inset animation. It passed the existing tests but brought back a URL-bar jump in narrow windows, so I kept it.

@Wety14

Wety14 commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Hi @yosofbadr, great work on stabilizing the toolbar controls in the "Sidebar and top toolbar" layout!

I tested your PR across different configurations and noticed that in "Single toolbar" (URL bar inside the sidebar) and on other platforms, the transition was still feeling jerky. I put together a small patch to make it completely smooth and progressive across all setups without altering your toolbar compensation logic:

  1. Smooth timing & easing (0.22s easeOut): The original 0.1s easeIn felt abrupt and caused web contents to resize too sharply. 0.22s with easeOut starts immediately (no perceived delay) while gently decelerating as the web viewport resizes.
  2. Fix 1-frame layout flash when opening the sidebar: When un-compacting, removing the fixed positioning before setting the negative margin compressed the web view for a single frame. Applying the negative margin synchronously before toggling animation attributes eliminates this flicker.
  3. Clean sidebarWidth calculation: In single-toolbar/cross-platform modes, subtracting 3 * elementSeparation caused the sidebar to start 24px misplaced on re-entry. Normalized it to elementSeparation.
  4. Remove duplicate right-side transform: Removed the redundant translateX(100%) on right-side tabs that caused a double-displacement.
  5. Containment & cleanup: Ensured overflow: clip is set on #browser during both open/close phases and fixed the pointerEvents cleanup typo.
Click to view smooth-compact-mode.patch
From 6d089d81877261c944f51d8f79870bc06e024aab Mon Sep 17 00:00:00 2001
From: Wety14 <sacha.darleg@gmail.com>
Date: Sat, 31 Oct 2026 14:12:34 +0100
Subject: [PATCH] feat(compact-mode): smooth sidebar transition and progressive
 viewport resize

---
 src/zen/compact-mode/ZenCompactMode.mjs | 71 +++++++++++++------------
 1 file changed, 38 insertions(+), 33 deletions(-)

diff --git a/src/zen/compact-mode/ZenCompactMode.mjs b/src/zen/compact-mode/ZenCompactMode.mjs
index 66e194b79..b9b5e0f4b 100644
--- a/src/zen/compact-mode/ZenCompactMode.mjs
+++ b/src/zen/compact-mode/ZenCompactMode.mjs
@@ -633,28 +633,21 @@ window.gZenCompactModeManager = {
 
   animateCompactMode(isCompactMode, toolbarTransition = null) {
     return new Promise(resolve => {
-      document.documentElement.setAttribute("zen-compact-animating", "true");
       const canHideSidebar = this.canHideSidebar;
       const canAnimate = this._canAnimateSidebar;
       delete this._preventAnimateCollapse;
       delete this._wasInCompactMode;
-      // Do this so we can get the correct width ONCE compact mode styled have been applied
-      if (canAnimate) {
-        this.sidebar.setAttribute("animate", "true");
-      } else {
-        this.sidebar.style.transition = "none";
-      }
+
       if (this._ignoreNextHover) {
         this._setElementExpandAttribute(this.sidebar, false);
       }
-      this.sidebar.style.removeProperty("margin-right");
-      this.sidebar.style.removeProperty("margin-left");
-      this.sidebar.style.removeProperty("transform");
+
       // Reparenting controls invalidates layout; compensation needs fresh bounds.
       let sidebarWidth = toolbarTransition
         ? this.sidebar.getBoundingClientRect().width
         : window.windowUtils.getBoundsWithoutFlushing(this.sidebar).width;
       const elementSeparation = ZenThemeModifier.elementSeparation;
+
       if (!canAnimate) {
         this.sidebar.removeAttribute("animate");
         document.documentElement.removeAttribute("zen-compact-animating");
@@ -667,14 +660,37 @@ window.gZenCompactModeManager = {
         resolve();
         return;
       }
-      sidebarWidth -=
-        elementSeparation * (toolbarTransition || isCompactMode ? 1 : 3);
+
+      sidebarWidth -= elementSeparation;
+
+      const browser = document.getElementById("browser");
+      if (canHideSidebar) {
+        browser?.style.setProperty("overflow", "clip");
+      }
+
+      // To avoid layout flicker when showing the sidebar, apply the initial negative
+      // margin before toggling animating classes / losing fixed positioning styles.
+      if (!isCompactMode && canHideSidebar) {
+        delete this._ignoreNextHover;
+        if (this.sidebarIsOnRight) {
+          this.sidebar.style.marginRight = `-${sidebarWidth}px`;
+        } else {
+          this.sidebar.style.marginLeft = `-${sidebarWidth}px`;
+        }
+      } else {
+        this.sidebar.style.removeProperty("margin-right");
+        this.sidebar.style.removeProperty("margin-left");
+      }
+      this.sidebar.style.removeProperty("transform");
+
+      document.documentElement.setAttribute("zen-compact-animating", "true");
+      this.sidebar.setAttribute("animate", "true");
+
       const animationOptions = {
-        ease: isCompactMode ? "easeIn" : "easeOut",
-        type: "spring",
-        bounce: 0,
-        duration: 0.1,
+        ease: "easeOut",
+        duration: 0.22,
       };
+
       if (canHideSidebar && isCompactMode) {
         toolbarTransition?.start(sidebarWidth, animationOptions);
         this._setElementExpandAttribute(this.sidebar, false);
@@ -692,8 +708,9 @@ window.gZenCompactModeManager = {
           )
           .then(() => {
             this.sidebar.style.transition = "none";
-            this.sidebar.style.pointEvents = "none";
+            this.sidebar.style.pointerEvents = "none";
             this.sidebar.removeAttribute("animate");
+            browser?.style.removeProperty("overflow");
 
             if (this._ignoreNextHover) {
               setTimeout(() => {
@@ -705,7 +722,7 @@ window.gZenCompactModeManager = {
             this.sidebar.style.removeProperty("margin-right");
             this.sidebar.style.removeProperty("margin-left");
             this.sidebar.style.removeProperty("transform");
-            this.sidebar.style.removeProperty("point-events");
+            this.sidebar.style.removeProperty("pointer-events");
 
             requestAnimationFrame(() => {
               this.sidebar.style.removeProperty("transition");
@@ -714,31 +731,18 @@ window.gZenCompactModeManager = {
             resolve();
           });
       } else if (canHideSidebar && !isCompactMode) {
-        // Shouldn't be ever true, but just in case
-        delete this._ignoreNextHover;
-        document.getElementById("browser").style.overflow = "clip";
-        if (this.sidebarIsOnRight) {
-          this.sidebar.style.marginRight = `-${sidebarWidth}px`;
-        } else {
-          this.sidebar.style.marginLeft = `-${sidebarWidth}px`;
-        }
         toolbarTransition?.start(sidebarWidth, animationOptions);
         gZenUIManager.motion
           .animate(
             this.sidebar,
             this.sidebarIsOnRight
-              ? {
-                  marginRight: [`-${sidebarWidth}px`, 0],
-                  ...(!toolbarTransition && {
-                    transform: ["translateX(100%)", "translateX(0)"],
-                  }),
-                }
+              ? { marginRight: [`-${sidebarWidth}px`, 0] }
               : { marginLeft: [`-${sidebarWidth}px`, 0] },
             animationOptions
           )
           .then(() => {
             this.sidebar.removeAttribute("animate");
-            document.getElementById("browser").style.removeProperty("overflow");
+            browser?.style.removeProperty("overflow");
             this.sidebar.style.transition = "none";
             this.sidebar.style.removeProperty("margin-right");
             this.sidebar.style.removeProperty("margin-left");
@@ -750,6 +754,7 @@ window.gZenCompactModeManager = {
             resolve();
           });
       } else {
+        browser?.style.removeProperty("overflow");
         this.sidebar.removeAttribute("animate"); // remove the attribute if we are not animating
         document.documentElement.removeAttribute("zen-compact-animating");
         delete this._ignoreNextHover;
-- 
2.54.0 (Apple Git-157)

@yosofbadr

Copy link
Copy Markdown
Contributor Author

@Wety14 thanks for the patch. I tested it on macOS against this PR's compact-mode code, ported to a slightly older nightly base. I ran the compact-mode tests and logged the sidebar and content positions on each animation frame.

The patch as posted causes a regression. It measures the sidebar width before setting the animate and zen-compact-animating attributes, when the sidebar reads 246px instead of 230px. With the default 8px separation:

  • On show, the animation starts 16px away from the resting position in all layouts I tested.
  • On hide, multi-toolbar mode overshoots by 16px and then snaps back.
  • browser_compact_mode_controls.js has 104 failures, in the right-sidebar, collapsed right-sidebar and custom-spacing tasks. It passes without the patch.

On the individual points:

  1. Easing and duration: the movement per frame is smaller, but the animation takes about 220ms instead of 100ms. I think this is a design decision for the maintainers, separate from this PR.
  2. Margin before attributes: I couldn't reproduce the flash. The first sampled frame on show is already at the right position. This only samples layout, so it can't rule out a paint-level flash. Do you have a recording?
  3. Removing the 3x factor: this needs a matching change to the measurement, because the 3x offsets the stale width. Setting the attributes first and subtracting the separation once removed the 16px jump and overshoot in my test, with your 220ms timing still applied.
  4. Removing translateX: I measured no difference in the frame data, so I can't confirm it fixes anything.
  5. overflow: clip and the pointerEvents typo: I saw no change in the measured positions. The typo fix looks correct either way.

The patch also removes this.sidebar.style.transition = "none" from the path used when animation is disabled, which looks unintended. The existing tests don't catch this.

This branch has not been deployed

No deployments
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.

Sidebar, toolbar and URL bar jump during compact-mode transitions on macOS

3 participants