Created
August 10, 2025 15:42
-
-
Save pqoqubbw/73bc0bbb835ca856bd42a0fc60128733 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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