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 */}
+
{/* Service integration buttons */}
{serviceIcons.slice(0, visibleIconCount).map((service, index) => {
const Icon = service.icon
return (
{
+ iconRefs.current[index] = el
+ }}
aria-label={service.label}
onClick={() => handleServiceClick(service.key as keyof typeof SERVICE_TEMPLATES)}
onMouseEnter={() => setLastHoveredIndex(index)}
style={service.style}
- isAutoHovered={!isUserHovering && index === autoHoverIndex}
>