fix(landing): replace per-icon hover with smooth sliding pill highlight#3480
fix(landing): replace per-icon hover with smooth sliding pill highlight#3480MaxwellCalkin wants to merge 1 commit intosimstudioai:mainfrom
Conversation
Replace the abrupt per-icon border/shadow hover effect with a single highlight pill that smoothly translates to the active icon. The pill follows both the auto-hover cycle and manual cursor hover, using CSS transitions for fluid movement between icons. Changes: - icon-button.tsx: Convert to forwardRef, remove per-icon hover/auto-hover border styling (now handled by parent pill element) - hero.tsx: Add refs for icon buttons and container, compute pill position via getBoundingClientRect, render absolute-positioned pill with transition-all duration-300, recalculate on window resize Fixes simstudioai#3468 --- > [!NOTE] > This PR was authored by an AI (Claude Opus 4.6, Anthropic). See https://github.com/anthropics/claude-code > for details on the tool used. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR SummaryLow Risk Overview
Written by Cursor Bugbot for commit ec35f01. This will update automatically on new commits. Configure here. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Greptile SummaryThis PR replaces individual per-icon border/shadow hover effects in the landing hero icon row with a single absolute-positioned "pill" element that smoothly slides between icons using CSS transforms, creating a fluid highlight that tracks both the auto-cycle and the user's cursor. Key changes:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant Container as Icon Container
participant State as React State
participant Pill as Pill <div>
Note over State: autoHoverIndex cycles every 2s
State->>Pill: activeIndex = autoHoverIndex → updatePillPosition()
Pill-->>Pill: translateX to autoHoverIndex button
User->>Container: onMouseEnter (container padding area)
Container->>State: isUserHovering = true
State->>Pill: activeIndex = lastHoveredIndex (null) → opacity 0
Note over Pill: ⚠️ Brief flash: pill disappears
User->>Container: onMouseEnter (icon button N)
Container->>State: lastHoveredIndex = N
State->>Pill: activeIndex = N → updatePillPosition()
Pill-->>Pill: translateX to button N (opacity 1)
User->>Container: onMouseLeave
Container->>State: isUserHovering = false, autoHoverIndex = N+1
State->>Pill: activeIndex = autoHoverIndex → updatePillPosition()
Pill-->>Pill: slides to autoHoverIndex button
Last reviewed commit: ec35f01 |
| /** | ||
| * Compute the active icon index and update the pill position | ||
| */ | ||
| const activeIndex = isUserHovering ? lastHoveredIndex : autoHoverIndex |
There was a problem hiding this comment.
Pill disappears on first container entry
When a user moves the cursor into the container padding area (the pt-[18px] / pt-[32px] zone above the icons) before hovering any individual icon, isUserHovering becomes true while lastHoveredIndex is still null. This makes activeIndex === null, which causes updatePillPosition to fade the pill to opacity: 0 — creating a visible flash where the pill vanishes before reappearing on the first icon hover.
The fix is to fall back to autoHoverIndex when the user hasn't yet hovered over a specific icon:
| const activeIndex = isUserHovering ? lastHoveredIndex : autoHoverIndex | |
| const activeIndex = isUserHovering && lastHoveredIndex !== null ? lastHoveredIndex : autoHoverIndex |
| > | ||
| {/* Sliding highlight pill */} | ||
| <div | ||
| className='pointer-events-none absolute top-[18px] left-0 rounded-xl border border-[#E5E5E5] shadow-[0_2px_4px_0_rgba(0,0,0,0.08)] transition-all duration-300 ease-in-out sm:top-[32px]' |
There was a problem hiding this comment.
transition-all animates width/height on initial render
The pill initialises with width: 0, height: 0 in state. Because transition-all duration-300 transitions every CSS property, the very first call to updatePillPosition will animate the pill growing from zero width/height to the button dimensions, causing a "scale-up" entrance rather than a clean fade-in slide. Consider scoping the transition to only the properties that should animate:
| className='pointer-events-none absolute top-[18px] left-0 rounded-xl border border-[#E5E5E5] shadow-[0_2px_4px_0_rgba(0,0,0,0.08)] transition-all duration-300 ease-in-out sm:top-[32px]' | |
| className='pointer-events-none absolute top-[18px] left-0 rounded-xl border border-[#E5E5E5] shadow-[0_2px_4px_0_rgba(0,0,0,0.08)] transition-[transform,opacity] duration-300 ease-in-out sm:top-[32px]' |
If you also want the opacity fade on first appearance, transition-[transform,opacity] handles that without animating width/height.
| > | ||
| {/* Sliding highlight pill */} | ||
| <div | ||
| className='pointer-events-none absolute top-[18px] left-0 rounded-xl border border-[#E5E5E5] shadow-[0_2px_4px_0_rgba(0,0,0,0.08)] transition-all duration-300 ease-in-out sm:top-[32px]' |
There was a problem hiding this comment.
Hardcoded top value duplicates container padding
The pill's top-[18px] sm:top-[32px] directly mirrors the container's pt-[18px] sm:pt-[32px]. These two values must always stay in sync — if the container padding is ever adjusted, the pill will be misaligned. Consider computing offsetY from getBoundingClientRect() in updatePillPosition (similar to how offsetX is already computed) so the vertical position is always derived from the actual button position rather than hardcoded.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| transform: `translateX(${offsetX}px)`, | ||
| opacity: 1, | ||
| }) | ||
| }, [activeIndex]) |
There was a problem hiding this comment.
Pill stays visible at stale position after icon count shrinks
Medium Severity
When visibleIconCount decreases on window resize (e.g., 13→6), autoHoverIndex is not clamped to the new range. If autoHoverIndex exceeds the new visibleIconCount, the corresponding icon ref is null (unmounted), so updatePillPosition exits early without updating — leaving the pill visible (opacity: 1) at its last stale position, potentially floating in empty space to the right of the now-narrower icon row. This persists for up to 2 seconds until the next interval tick wraps the index back in range.
Additional Locations (1)
|
|
||
| const updatePillPosition = React.useCallback(() => { | ||
| if (activeIndex == null) { | ||
| setPillStyle((prev) => ({ ...prev, opacity: 0 })) |
There was a problem hiding this comment.
Pill disappears on first mouse entry into container
Medium Severity
On the very first user hover interaction, lastHoveredIndex is null. When the mouse enters the container (especially through the pt-[18px]/pt-[32px] padding zone above the buttons), isUserHovering becomes true and activeIndex resolves to lastHoveredIndex which is null. This triggers the activeIndex == null branch in updatePillPosition, fading the pill out to opacity: 0. The pill then reappears only after the user hovers over a specific icon button, causing a visible flash instead of the intended smooth sliding motion.
Additional Locations (1)
|
Closing in favor of #3481 which uses framer-motion's layoutId for a cleaner implementation of the same fix. |


Summary
Replaces the abrupt per-icon border/shadow hover effect on the landing hero icon row with a single highlight pill that smoothly translates between icons. The pill follows both the auto-hover cycle and manual cursor hover using CSS
transition-all duration-300, creating a fluid, continuous highlight rather than a jarring jump.Fixes #3468
Changes
icon-button.tsx: Convert toforwardRefto expose button DOM refs for position measurement. Remove the per-iconisAutoHoveredprop andhover:border/hover:shadowclasses — highlighting is now handled by the parent pill element.hero.tsx: AddiconRefsandiconContainerReffor measuring each button's position viagetBoundingClientRect. Compute the active index (user hover takes priority over auto-hover), then position an absolute pill element withtranslateX. Recalculates on window resize for responsive correctness.How it works
<div>pill sits atposition: absoluteinside the icon containerwidth,height, andtransform: translateX(...)are updatedtransition-all duration-300 ease-in-outon the pill creates the smooth sliding motionNote
This PR was authored by an AI (Claude Opus 4.6, Anthropic). See https://github.com/anthropics/claude-code for details on the tool used.