From ec35f01c707072a17928e11ff13628e6c619372c Mon Sep 17 00:00:00 2001 From: Maxwell Calkin Date: Sun, 8 Mar 2026 23:23:12 -0400 Subject: [PATCH] fix(landing): replace per-icon hover with smooth sliding pill highlight 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 #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 --- .../hero/components/icon-button.tsx | 23 +++----- .../app/(landing)/components/hero/hero.tsx | 59 ++++++++++++++++++- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/apps/sim/app/(landing)/components/hero/components/icon-button.tsx b/apps/sim/app/(landing)/components/hero/components/icon-button.tsx index c8e523e2f86..722476b9653 100644 --- a/apps/sim/app/(landing)/components/hero/components/icon-button.tsx +++ b/apps/sim/app/(landing)/components/hero/components/icon-button.tsx @@ -1,5 +1,6 @@ 'use client' +import { forwardRef } from 'react' import type React from 'react' interface IconButtonProps { @@ -8,31 +9,23 @@ interface IconButtonProps { onMouseEnter?: () => void style?: React.CSSProperties 'aria-label': string - isAutoHovered?: boolean } -export function IconButton({ - children, - onClick, - onMouseEnter, - style, - 'aria-label': ariaLabel, - isAutoHovered = false, -}: IconButtonProps) { +export const IconButton = forwardRef(function IconButton( + { children, onClick, onMouseEnter, style, 'aria-label': ariaLabel }, + ref +) { return ( ) -} +}) diff --git a/apps/sim/app/(landing)/components/hero/hero.tsx b/apps/sim/app/(landing)/components/hero/hero.tsx index 546dc47627f..e793f8517bc 100644 --- a/apps/sim/app/(landing)/components/hero/hero.tsx +++ b/apps/sim/app/(landing)/components/hero/hero.tsx @@ -152,6 +152,18 @@ export default function Hero() { const [lastHoveredIndex, setLastHoveredIndex] = React.useState(null) const intervalRef = React.useRef(null) + /** + * Refs for smooth sliding pill highlight + */ + const iconContainerRef = React.useRef(null) + const iconRefs = React.useRef<(HTMLButtonElement | null)[]>([]) + const [pillStyle, setPillStyle] = React.useState({ + opacity: 0, + width: 0, + height: 0, + transform: 'translateX(0px)', + }) + /** * Handle service icon click to populate textarea with template */ @@ -225,6 +237,40 @@ export default function Hero() { } }, [isUserHovering, visibleIconCount]) + /** + * Compute the active icon index and update the pill position + */ + const activeIndex = isUserHovering ? lastHoveredIndex : autoHoverIndex + + const updatePillPosition = React.useCallback(() => { + if (activeIndex == null) { + setPillStyle((prev) => ({ ...prev, opacity: 0 })) + return + } + + const button = iconRefs.current[activeIndex] + const container = iconContainerRef.current + if (!button || !container) return + + const containerRect = container.getBoundingClientRect() + const buttonRect = button.getBoundingClientRect() + const offsetX = buttonRect.left - containerRect.left + + setPillStyle({ + width: buttonRect.width, + height: buttonRect.height, + transform: `translateX(${offsetX}px)`, + opacity: 1, + }) + }, [activeIndex]) + + React.useEffect(() => { + updatePillPosition() + + window.addEventListener('resize', updatePillPosition) + return () => window.removeEventListener('resize', updatePillPosition) + }, [updatePillPosition]) + /** * Handle mouse enter on icon container */ @@ -377,21 +423,30 @@ export default function Hero() { Build and deploy AI agent workflows

+ {/* Sliding highlight pill */} +