Intro: GWDC Hackathon
Last week, I joined the GWDC Hackathon. Our team of four built an AI agent blockchain wallet.
I had never worked with blockchain before, so it was a real challenge. I wrote most of my code with the help of AI.
We had a blockchain developer on the team and finished the project on time. We did not win first place, but I learned a lot.
What We Built
I was in charge of server development. I connected the frontend to the server and made sure the smart contract worked well on the server side.
If you want to know how we built the project in three days, check out my post below.
GWDC Hackathon Blog Post Link
Why I Wanted to Rebuild the Frontend
I was not a frontend developer at the event. But afterwards, I really wanted to rebuild the frontend myself.
The project had a dashboard, an AI chat, and more. I had never built anything like it, so I wanted to make my own version.
It was also a great chance to dive into TypeScript and BFF (Backend for Frontend). The app handles large amounts of data and many data types. Even the frontend needs to follow the right wallet authentication flow to avoid security issues.
Honestly, it made my brain freeze. I spent a whole week just understanding the flow, and I still could not do it alone. To understand blockchain, you need to dig into EIPs (Ethereum Improvement Proposals) and other standards.
In the end, I built the BFF and APIs with AI. In the next post, I will share how blockchain technology works from the frontend side.
Flow of an AI Agent Blockchain Wallet Blog Post Link
So now, instead of talking about difficult, confusing blockchain standards, let’s dive into something that is fun!
Step Indicator Dots
The dots on the left are the part we’re going to build first.

"use client"
import React from 'react'
const StepIndicatorDots = ({ currentStep = 1, totalSteps = 4, className = '' }) => {
const activeStep = Math.min(Math.max(currentStep, 1), totalSteps)
return (
<div
className={`step-indicator-dots ${className}`.trim()}
aria-label={`Step ${activeStep} of ${totalSteps}`}
>
{Array.from({ length: totalSteps }, (_, index) => {
const num = index + 1
const state = num === activeStep ? 'current' : num < activeStep ? 'complete' : 'upcoming'
return <span
key={num}
className={`step-indicator-dot ${state}`}
aria-current={number === activeStep ? 'step' : undefined}
>{num}</span>
})}
</div>
)
}
export default StepIndicatorDotsArray.from({ length: totalSteps }, (_, index) => {…})
- Creates a new array with
totalStepsitems. - The first argument is an object with a
lengthproperty, which tells it how many items to make. - The second argument is a map function that runs for each item, where
_is the value (alwaysundefinedhere, so we skip it) andindexis the position starting from 0, which is why we useindex + 1as the step number.
ARIA Basics for Accessibility
- aria-label: Gives an element a name that screen readers can read out loud. Use it when the visible content alone does not explain what the element is, like an icon button with no text (
aria-label="Close menu"). - aria-current: Tells screen readers which item is the current one in a set. Common values are
pagefor navigation links,stepfor progress steps, anddatefor calendars. Sighted users see the active style, and this gives the same information to screen reader users. - role: Tells screen readers what kind of element something is. A plain
divorspanhas no meaning on its own, so some screen readers ignore itsaria-label. Adding a role likerole="group"orrole="navigation"makes sure the label is actually read. - Tip: Use native HTML elements first when you can. A
<button>or<nav>already has a built in role, so you need fewer ARIA attributes.
StepIndicatorDots Flow
Props (currentStep, totalSteps, className)
→ Clamp currentStep between 1 and totalSteps → activeStep
→ Array.from creates an array with totalSteps items
→ For each item, index + 1 becomes the step number (num)
→ Compare num with activeStep to set the state
num < activeStep→completenum === activeStep→currentnum > activeStep→upcoming
→ Render a<span>dot with that state as a CSS class
→ Addaria-current="step"only to the current dot
The increment of the current step happens on the parent component.
1. Initialize the step state: The flow starts at step 1
const [step, setStep] = useState(1)2. Move to step 2: when the user chooses a sign-in method
function chooseMethod(value) {
setMethod(value)
setMessage('')
setCodeError(false)
setStep(2)
}3. Show the session check, then move to step 4
The current === 3 check prevents the timer from overwriting the step if the flow has already moved elsewhere.
function startSessionCheck() {
setMessage('')
setCodeError(false)
setStep(3)
window.setTimeout(() => {
setStep((current) => current === 3 ? 4 : current)
}, 900)
}4. Pass the current step to the indicator
<StepHeader step={step} />
Step Progress Bar
Now, let’s move on to the second part on the right.
import React from 'react'
const StepProgress = ({ currentStep = 1, totalSteps = 4, label = '', className = '' }) => {
const activeStep = Math.min(Math.max(currentStep, 1), totalSteps)
const progress = (activeStep / totalSteps) * 100
return (
<div className={`step-progress ${className}`.trim()} aria-label={`${label || 'Progress'}: ${activeStep} of ${totalSteps}`}>
<div className="step-progress-copy"><span>{label}</span><span>{activeStep}/{totalSteps}</span></div>
<div className="step-progress-track" role="progressbar" aria-valuemin="0" aria-valuemax={totalSteps} aria-valuenow={activeStep}>
<span className="step-progress-fill" style={{ width: `${progress}%` }} />
</div>
</div>
)
}
export default StepProgress
Progress Calculation
const progress = (activeStep / totalSteps) * 100
- Divides the current step by the total steps to get a ratio between 0 and 1.
- Multiplies it by 100 to turn it into a percentage.
- This value is used as the width of the fill bar (
width: ${progress}%), so the bar grows as the user moves forward.
ARIA for Progress Bars
- role="progressbar": Tells screen readers that the element shows progress. Without it, the track is just an empty
divwith no meaning. - aria-valuenow: The current value. Here it is
activeStep, so screen readers know where the user is. - aria-valuemin / aria-valuemax: The start and end of the range. Together with
aria-valuenow, screen readers can announce progress like "2 of 4." - Tip: The visual width of the bar means nothing to screen readers. These attributes give the same information in a form they can read.
StepProgress Flow
Props (currentStep, totalSteps, label, className)
→ Clamp currentStep between 1 and totalSteps → activeStep
→ Calculate progress as a percentage
→ Render the label and activeStep/totalSteps text
→ Render the track with role="progressbar" and the aria-value attributes
→ Set the fill bar width to progress%


