UI UX Design and Prototyping

Build Your First Testable UI Prototype From Scratch

A polished mockup is a poor first deliverable for a small web flow: it can hide broken validation, keyboard traps, and promises the interface cannot keep. For a junior developer starting from zero, I would build a narrow, runnable prototype before drawing a full screen set. The goal is not production code. It is a working setup that lets a teammate test the riskiest interaction.

One password-reset flow is enough to expose the setup problems

Pick a password-reset request as the first slice. It has a clear starting point, an email field, a submit action, and an outcome a reviewer can understand without a tour of the product. Resist adding account creation, profile settings, or a design system at this stage: those features increase the number of decisions without helping you verify that this flow works.

Write the interaction contract before opening a design tool. In a ticket or README, name three states as your starting constraint: an untouched form, an invalid entry, and a confirmation after valid submission. Specify that the confirmation is simulated. A prototype that says an email was sent when no service exists teaches reviewers the wrong behavior. If your product must also demonstrate a server failure, add that as a fourth state deliberately rather than hiding it in a vague “error” screen.

For the first pass, use native HTML form behavior for the invalid state. An input with type=”email” and required prevents an empty or malformed value from reaching the submit handler in supporting browsers. That is a useful baseline, not a complete error design: native messages differ by browser, so record what your team sees before deciding whether custom messages are needed. Put the field name in a visible label; a placeholder disappears during entry and is a poor substitute for an enduring name.

Make one acceptance path concrete: start on the form, enter a plausible address, activate the button using only a keyboard, and reach a confirmation that explicitly says no email was sent. Also try submitting an empty value and an address without an @ sign. These actions give a reviewer something to do and give you a way to tell whether the prototype is working.

Keep UI UX Prototyping for Software Projects: Best Practices as a review reference, but do not make a polished click-through the acceptance test, because a browser form can reveal validation and focus behavior that linked frames conceal. I would not draw every success and error screen before running this path; changes to the form contract would otherwise force you to redraw screens before you have learned whether the interaction makes sense.

A runnable route beats a large design file for this first decision

Start with a React and TypeScript project generated by Vite. The commands are npm create vite@7 reset-flow — –template react-ts, then cd reset-flow, npm install, and npm run dev. Vite 7 specifies Node.js 22.12 or later on the Node 22 line, so check node –version first; a template failure caused by an older runtime is not a prototype-design problem. Open the local address printed by Vite and confirm that the starter page loads before editing a file.

Use src/App.tsx as the single route for now. One route is a scope limit, not an architectural recommendation: it keeps the first review focused on the request interaction instead of navigation scaffolding. Replace the starter component with the following code. It runs in the generated React project without a backend and makes the simulated outcome unmistakable.

import { useState } from 'react';
export default function App() {
  const [sent, setSent] = useState(false);
  return <main>
    <h1>Reset your password</h1>
    {sent ? <p role="status">Prototype only: no email was sent.</p> :
      <form onSubmit={e => { e.preventDefault(); setSent(true); }}>
        <label htmlFor="email">Email address</label>
        <input id="email" name="email" type="email" required />
        <button type="submit">Continue</button>
      </form>}
  </main>;
}

The role=”status” gives assistive technology a way to announce the changed outcome without moving focus, although you still need to check the announcement in your target browser and screen reader. The form uses the browser’s constraint validation, so clicking Continue with an empty field should leave the form in place. Entering a valid-looking address switches to the confirmation; it does not call an API, persist data, or prove that a reset email can be delivered.

Now test the layout at 320 CSS pixels wide as a narrow-width target you can adjust for your product. Use Chrome DevTools or Firefox Responsive Design Mode, but also shrink a normal browser window: an emulator preset alone may not expose a clipped button or horizontal scroll caused by your own CSS. If the generated template’s styles fight the layout, remove the starter decoration rather than building a new component library to accommodate it.

Figma and Penpot are both reasonable places to sketch what remains uncertain. Figma wins when the team already reviews a shared Figma component library; its cost is any applicable seat licensing plus the work of translating frames into browser behavior. Penpot wins when self-hosting and open-source control are requirements; its cost is hosting and maintenance time, while translation into working UI still remains. For this particular form, I would choose neither as the source of truth yet, because the runnable route answers the first interaction questions more directly.

Realistic states matter more than visual completeness

Once the route works, inspect each state rather than decorating only the untouched form. On the initial view, the heading should explain the task and the button should say what advances it. On invalid entry, the user needs to retain their value and understand what to correct. On confirmation, the message must distinguish a simulated transition from a completed service request. These details determine what the prototype claims to do.

The code above deliberately uses the browser’s invalid-entry message, which is enough to start a conversation but may not match your product’s copy or localization requirements. If a reviewer cannot understand the browser message, add a visible error associated with the field using aria-describedby, and focus the field when submission fails. Do not add aria-invalid=”true” merely because an input exists; set it when the value is actually invalid, because announcing an error before the user acts creates noise.

Ask a teammate to perform the path without explaining the screen. Give them a task—request a password reset for a test address—and observe where they pause. Record what happened, not a verdict such as “confusing.” For example, note whether they tried to press Enter, whether they noticed the confirmation, and whether they believed an email would arrive. Five sessions can be a practical recruitment target for this local review, but five people are not a statistical guarantee that the flow is usable for everyone.

Use the observation to change one thing at a time. If someone mistakes the confirmation for a real send, change the copy before changing colors. If a keyboard user cannot tell where focus is, add a visible focus style before adjusting spacing. WCAG 2.2 AA’s 4.5:1 minimum contrast ratio for normal text is a standards-based check for text you introduce; it does not tell you whether the wording itself is clear. A contrast checker such as axe DevTools can catch some problems, while the task observation catches misunderstandings the checker cannot.

Use UI UX Design and Prototyping for Modern Software Teams to challenge your screen-level decisions, but keep observed behavior ahead of visual consensus, because agreement around a frame cannot show whether someone expects a real email. That distinction is particularly useful when a teammate proposes adding a success animation: the animation may be attractive, yet correcting a false service claim is the more urgent change.

A small test and an honest handoff make the prototype reusable

After the first review, install @playwright/test as a development dependency with npm install -D @playwright/test, then run npx playwright install chromium. Add a browser test that loads the local route, checks that the heading and Email address label are visible, fills a valid address, activates Continue, and checks for “Prototype only: no email was sent.” Run the Vite server in a separate terminal while developing the test. Pin the Playwright package version in your lockfile so a teammate runs the same browser-test dependency you reviewed.

Test the invalid path separately. Submit an empty field and confirm that the confirmation does not appear; then try malformed text. Playwright is useful for repeatable state checks, but it will not prove that a screen reader announces the status correctly or that the copy sets the right expectation. Perform those checks manually with keyboard navigation and a screen reader available to your team, such as NVDA on Windows or VoiceOver on macOS.

Run npm run build as a final setup check. A successful development preview can still leave a TypeScript or build failure for the next developer. Commit the source, test, package manifest, and lockfile; leave generated build output out of the handoff. In the README, state how to start the app, what the simulated submit does, which states have been checked, and what remains unimplemented. That last line prevents a convincing prototype from being mistaken for a finished password-reset feature.

The next useful step is one observed run

Create the Vite project, paste the component into src/App.tsx, and ask one teammate to request a reset without your help. Watch whether they can submit the form and whether they expect an email. Write down the first mismatch you observe before changing the interface. That single run gives your next design or code change a reason grounded in behavior.