Skip to content

Commit bcd2aab

Browse files
MaxwellCalkinclaude
andcommitted
fix: smooth sliding highlight pill for hero service icons
Replace per-icon hover state (abrupt border/shadow jump) with a single shared highlight pill that slides smoothly between icons using framer-motion layoutId animation. The pill follows the cursor on hover and continues the auto-cycle on mouse leave. Fixes #3468 > This PR was authored by an AI (Claude Opus 4.6, Anthropic). See > https://www.maxcalkin.com/ai for transparency details. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8c0a2e0 commit bcd2aab

File tree

2 files changed

+27
-13
lines changed

2 files changed

+27
-13
lines changed
Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,45 @@
11
'use client'
22

33
import type React from 'react'
4+
import { motion } from 'framer-motion'
45

56
interface IconButtonProps {
67
children: React.ReactNode
78
onClick?: () => void
89
onMouseEnter?: () => void
10+
onMouseLeave?: () => void
911
style?: React.CSSProperties
1012
'aria-label': string
11-
isAutoHovered?: boolean
13+
isActive?: boolean
1214
}
1315

1416
export function IconButton({
1517
children,
1618
onClick,
1719
onMouseEnter,
20+
onMouseLeave,
1821
style,
1922
'aria-label': ariaLabel,
20-
isAutoHovered = false,
23+
isActive = false,
2124
}: IconButtonProps) {
2225
return (
2326
<button
2427
type='button'
2528
aria-label={ariaLabel}
2629
onClick={onClick}
2730
onMouseEnter={onMouseEnter}
28-
className={`flex items-center justify-center rounded-xl border p-2 outline-none transition-all duration-300 ${
29-
isAutoHovered
30-
? 'border-[#E5E5E5] shadow-[0_2px_4px_0_rgba(0,0,0,0.08)]'
31-
: 'border-transparent hover:border-[#E5E5E5] hover:shadow-[0_2px_4px_0_rgba(0,0,0,0.08)]'
32-
}`}
31+
onMouseLeave={onMouseLeave}
32+
className='relative flex items-center justify-center rounded-xl p-2 outline-none'
3333
style={style}
3434
>
35-
{children}
35+
{isActive && (
36+
<motion.div
37+
layoutId='icon-highlight-pill'
38+
className='absolute inset-0 rounded-xl border border-[#E5E5E5] shadow-[0_2px_4px_0_rgba(0,0,0,0.08)]'
39+
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
40+
/>
41+
)}
42+
<span className='relative z-[1]'>{children}</span>
3643
</button>
3744
)
3845
}

apps/sim/app/(landing)/components/hero/hero.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export default function Hero() {
149149
*/
150150
const [autoHoverIndex, setAutoHoverIndex] = React.useState(1)
151151
const [isUserHovering, setIsUserHovering] = React.useState(false)
152-
const [lastHoveredIndex, setLastHoveredIndex] = React.useState<number | null>(null)
152+
const [hoveredIndex, setHoveredIndex] = React.useState<number | null>(null)
153153
const intervalRef = React.useRef<NodeJS.Timeout | null>(null)
154154

155155
/**
@@ -225,6 +225,12 @@ export default function Hero() {
225225
}
226226
}, [isUserHovering, visibleIconCount])
227227

228+
/**
229+
* The active icon index used to position the shared highlight pill.
230+
* When the user is hovering, use their hovered icon; otherwise use auto-hover.
231+
*/
232+
const activeIconIndex = isUserHovering && hoveredIndex !== null ? hoveredIndex : autoHoverIndex
233+
228234
/**
229235
* Handle mouse enter on icon container
230236
*/
@@ -240,9 +246,10 @@ export default function Hero() {
240246
*/
241247
const handleIconContainerMouseLeave = () => {
242248
setIsUserHovering(false)
249+
setHoveredIndex(null)
243250
// Start from the next icon after the last hovered one
244-
if (lastHoveredIndex !== null) {
245-
setAutoHoverIndex((lastHoveredIndex + 1) % visibleIconCount)
251+
if (hoveredIndex !== null) {
252+
setAutoHoverIndex((hoveredIndex + 1) % visibleIconCount)
246253
}
247254
}
248255

@@ -389,9 +396,9 @@ export default function Hero() {
389396
key={service.key}
390397
aria-label={service.label}
391398
onClick={() => handleServiceClick(service.key as keyof typeof SERVICE_TEMPLATES)}
392-
onMouseEnter={() => setLastHoveredIndex(index)}
399+
onMouseEnter={() => setHoveredIndex(index)}
393400
style={service.style}
394-
isAutoHovered={!isUserHovering && index === autoHoverIndex}
401+
isActive={index === activeIconIndex}
395402
>
396403
<Icon className='h-5 w-5 sm:h-6 sm:w-6' />
397404
</IconButton>

0 commit comments

Comments
 (0)