Skip to content

Instantly share code, notes, and snippets.

@pqoqubbw
Created August 10, 2025 15:42
Show Gist options
  • Save pqoqubbw/73bc0bbb835ca856bd42a0fc60128733 to your computer and use it in GitHub Desktop.
Save pqoqubbw/73bc0bbb835ca856bd42a0fc60128733 to your computer and use it in GitHub Desktop.
"use client"
import { cn } from "@/lib/utils"
import { useRef, useState } from "react"
const MOCK_STEPS = {
0: "Step 1",
1: "Step 2",
2: "Step 3",
3: "Step 4",
4: "Step 5",
}
type StepTitleProps = {
activeIndex: number
}
const StepTitle = ({ activeIndex }: StepTitleProps) => {
return (
<div className="ml-1.5 relative w-full h-[20px] overflow-hidden">
{Object.entries(MOCK_STEPS).map(([index, text]) => (
<p
key={index}
data-active={activeIndex === parseInt(index)}
aria-hidden={activeIndex !== parseInt(index)}
className={cn(
"text-[#FFF] text-sm font-mono absolute left-0 top-0 w-full transition-transform duration-300 ease-in-out data-[active=false]:pointer-events-none data-[active=false]:select-none",
activeIndex === parseInt(index) && "translate-y-0",
activeIndex > parseInt(index) && "-translate-y-full",
activeIndex < parseInt(index) && "translate-y-full"
)}
>
{text}
</p>
))}
</div>
)
}
type StepItemProps = {
activeIndex: number
index: number
onClick: () => void
}
const StepItem = ({ activeIndex, index, onClick }: StepItemProps) => {
return (
<button
type="button"
tabIndex={0}
role="tab"
data-active={activeIndex === index}
aria-current={activeIndex === index}
aria-label={`Step ${index + 1}`}
className="h-[20px] group/step-item flex items-center justify-center font-mono text-xs cursor-pointer transition-[width,background-color] duration-300 data-[active=true]:bg-[#FFF] data-[active=true]:text-[#111] data-[active=true]:w-[20px] data-[active=false]:bg-[#545454] data-[active=false]:w-[10px] outline-offset-2 focus-visible:outline-[0.5px] focus-visible:outline-[#FFF]"
onClick={onClick}
>
<span className="duration-200 select-none ease-in-out transition-opacity group-data-[active=true]/step-item:opacity-100 group-data-[active=false]/step-item:opacity-0">
{index + 1}
</span>
</button>
)
}
export default function Home() {
const [activeIndex, setActiveIndex] = useState(0)
const listRef = useRef<HTMLDivElement>(null)
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
const keys = [
"ArrowRight",
"ArrowLeft",
"ArrowDown",
"ArrowUp",
"Home",
"End",
]
if (!keys.includes(e.key)) return
e.preventDefault()
const container = listRef.current
if (!container) return
const tabs = Array.from(
container.querySelectorAll<HTMLButtonElement>(
'button[role="tab"]:not([disabled])'
)
)
if (tabs.length === 0) return
const currentIndex = tabs.findIndex((b) => b === document.activeElement)
let next = currentIndex < 0 ? 0 : currentIndex
if (e.key === "ArrowRight" || e.key === "ArrowUp")
next = (currentIndex + 1) % tabs.length
if (e.key === "ArrowLeft" || e.key === "ArrowDown")
next = (currentIndex - 1 + tabs.length) % tabs.length
if (e.key === "Home") next = 0
if (e.key === "End") next = tabs.length - 1
tabs[next]?.focus()
}
return (
<div className="flex items-center justify-center gap-[4px]">
<div
ref={listRef}
className="flex items-center justify-center gap-[4px]"
role="tablist"
onKeyDown={handleKeyDown}
>
{Array.from({ length: 5 }).map((_, index) => (
<StepItem
key={index}
activeIndex={activeIndex}
index={index}
onClick={() => setActiveIndex(index)}
/>
))}
</div>
<StepTitle activeIndex={activeIndex} />
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment