ScreenshotNeo

BlogHow-to

How to Build Reusable UI Components

Learn to design reusable UI components with clear APIs, accessible behavior, layered styles, and tests that cover real page contexts.

By the ScreenshotNeo team4 October 20269 min read

Build reusable UI components around one clear responsibility and a small, predictable API. Keep shared foundations separate from component styles and optional behavior; make accessibility behavior part of each component’s contract; document its states and interactions; and test it both by itself and in realistic pages.

A component is reusable when it serves a distinct interface need without hiding important behavior or forcing every page into the same workflow. This guide uses a plain HTML, CSS, and JavaScript button as a runnable example, then explains how to adapt the method to other components and frameworks.

1. Decide what should be a component

Start with a repeated interface need, not with a desire to make every element generic. Name the single job the component serves, its inputs, its states, and what remains the responsibility of the page or application around it. WCAG 2.2 describes a user interface component as a part of content perceived as a single control for a distinct function; that is a useful boundary to consider when defining a component. W3C WCAG 2.2.

  • Good boundary: a button provides an action; a page decides what the action does.
  • Risky boundary: a generic “action panel” that bundles page-specific navigation, business rules, data fetching, and visual layout behind many flags.
  • Make variants explicit: prefer meaningful options such as variant="primary" over unrelated booleans that can combine into unclear states.

Before implementation, write a short contract: purpose, supported content, options, states, events or callbacks, accessibility behavior, and known limits. If the contract needs many options to cover unrelated use cases, split the component or compose smaller parts.

2. Build a small, platform-familiar API

Choose names and behavior that developers already expect from the platform. Keep ordinary content as ordinary content, use native controls where they fit, and avoid encoding complex data into awkward strings. For Web Components, W3C TAG guidance recommends familiar platform patterns and a JavaScript API for complex data such as objects, arrays, or streams. W3C TAG Design Principles.

Question Practical choice
What does the component do? Give it one clear responsibility and name it after that function.
How are options represented? Use a small set of documented values with sensible defaults.
How does a caller provide content? Use children/slots or native content when possible; use structured JavaScript data for complex values.
How does it communicate? Follow framework conventions for events, callbacks, controlled state, and refs.
What can callers customize? Expose stable options. Avoid requiring callers to target internal markup or override private styles.

Do not add an option just because one page might someday need it. A public API is a compatibility promise: every option adds documentation and testing work. When a need is specific to one workflow, compose the reusable component in that workflow instead of teaching the component about the whole application.

3. Runnable example: an accessible button

This small example uses a native <button>, so keyboard activation, focus behavior, and button semantics come from the browser. Save it as index.html and open it in a browser. The component accepts a label, a visual variant, and a disabled state; the page owns the action.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Reusable button example</title>
  <style>
    :root {
      --button-radius: 0.375rem;
      --button-font: system-ui, sans-serif;
      --color-primary: #174ea6;
      --color-primary-hover: #123d82;
      --color-on-primary: #fff;
      --color-secondary: #e8eef8;
      --color-on-secondary: #172b4d;
      --color-focus: #f9ab00;
    }

    .button {
      border: 0;
      border-radius: var(--button-radius);
      cursor: pointer;
      font: 600 1rem/1.2 var(--button-font);
      min-height: 2.75rem;
      padding: 0.65rem 1rem;
    }

    .button--primary {
      background: var(--color-primary);
      color: var(--color-on-primary);
    }

    .button--primary:hover:not(:disabled) {
      background: var(--color-primary-hover);
    }

    .button--secondary {
      background: var(--color-secondary);
      color: var(--color-on-secondary);
    }

    .button:focus-visible {
      outline: 3px solid var(--color-focus);
      outline-offset: 2px;
    }

    .button:disabled {
      cursor: not-allowed;
      opacity: 0.6;
    }
  </style>
</head>
<body>
  <main>
    <h1>Reusable button</h1>
    <button class="button button--primary" type="button" data-action-button>
      Save changes
    </button>
    <p id="status" role="status"></p>
  </main>
  <script>
    const button = document.querySelector('[data-action-button]');
    const status = document.querySelector('#status');

    button.addEventListener('click', () => {
      status.textContent = 'Save action requested.';
    });
  </script>
</body>
</html>

The data-action-button attribute is a JavaScript hook, separate from the styling classes. USWDS says it prefers data attributes for JavaScript hooks because classes are more likely to be overwritten accidentally. USWDS developer documentation. In a component framework, express the same contract using that framework’s props and event conventions.

Adapt the contract to a framework

Keep the same boundary in React, Vue, Svelte, or a Web Component: the button renders a control and reports activation; its caller decides what activation means. Prefer native button elements over clickable generic containers. If a control is a link, render a link; do not disguise navigation as a button.

For a Web Component that needs structured configuration, accept it through a JavaScript property rather than trying to serialize nested values into an HTML attribute. Keep primitive attributes for simple, declarative values such as a label or variant where that fits the element’s design.

4. Organize foundations, styles, and enhancement

Keep shared foundations such as tokens, typography, and reset rules distinct from component styles. Add layout patterns and component styles in a deliberate layer; put optional JavaScript-enhanced behavior in a layer that can be loaded when needed. The W3C Design System is one example: it separates settings, functions, mixins, base styles, layouts, core component styles, and JavaScript-enhanced advanced components, and makes core styles available independently. Treat that as an example architecture, not a universal directory requirement. W3C Design System styles.

A modest project might use a structure like this:

src/
  styles/
    tokens.css
    base.css
  components/
    Button/
      Button.css
      Button.js
      Button.md
  patterns/
  pages/

Keep page layout and product-specific flows outside generic components. Use design tokens for shared decisions such as color, spacing, and type; document which styles are supported customization points. Avoid coupling callers to internal class names unless those names are explicitly part of the public styling contract.

5. Make accessibility behavior part of the contract

Document how the component works with pointer, keyboard, and assistive technology. Include its accessible name, role, states, focus behavior, keyboard interactions, and any announcements. Prefer native HTML semantics because they provide a well-understood baseline. Custom widgets need more care: a clickable div does not automatically behave like a button.

  • Check that the control has a meaningful accessible name and correct semantics.
  • Verify keyboard access, expected keys, visible focus, and a usable focus order.
  • Expose state in a way assistive technology can perceive, such as the appropriate native state or ARIA attribute.
  • Keep instructions and errors associated with the relevant control.
  • Do not communicate state or meaning through color alone.

The W3C WCAG 3.0 source in this area is a Working Draft, not a final recommendation. It advises component libraries to define usage and pointer, keyboard, and assistive technology interactions, and to test accessibility using established platform conventions. Check the document’s status before treating draft wording as normative. WCAG 3.0 Working Draft.

6. Document usage, states, and limits

Put enough information next to the component that a developer can use it correctly without reading its implementation. A useful component page includes:

  • Purpose and situations where it is appropriate.
  • Minimal examples and examples of each supported variant.
  • Options, types, defaults, events, and state ownership.
  • Pointer, keyboard, and assistive technology behavior.
  • Loading, empty, error, disabled, and boundary states where relevant.
  • Responsive behavior, content constraints, and known limitations.
  • Do and do not guidance for common misuse.

Keep examples aligned with the actual API. If a component requires a hidden setup step, put it in the example and documentation instead of relying on tribal knowledge.

7. Test the component alone and in real pages

Component-level checks catch local behavior; they do not show whether the component works well in its surrounding layout and workflow. Test both. USWDS specifically advises teams to do their own user testing at page level to gauge usability in context. USWDS developer documentation.

  1. Check rendering: defaults, variants, long content, empty content, and supported edge states.
  2. Check interaction: pointer use, keyboard use, focus transitions, disabled behavior, and repeated activation.
  3. Check accessibility: name, role, state, focus, and interaction patterns appropriate to the control.
  4. Check integration: render it in representative pages with surrounding copy, layout, validation, and responsive constraints.
  5. Get user feedback: observe the page-level task, not just whether the isolated component passes a technical check.

Automated checks can help catch regressions, but they do not replace testing with people or checking the component in the page where it will be used. Choose checks that match the risks and behavior of the component.

8. Common mistakes and fixes

Problem Why it happens Fix
The component has too many flags Unrelated use cases accumulated behind one abstraction. Split responsibilities or compose smaller components in the page.
Keyboard users cannot activate it A generic element was made clickable without native control behavior. Use a native button or link that matches the action.
Callers override many internal styles Customization points or design tokens are missing, or the component owns too much layout. Expose stable variants and tokens; move page layout outward.
Styles break after a refactor Consumers depended on undocumented internal classes. Document supported styling hooks and treat them as API, or remove the dependency.
Behavior differs across pages State ownership and event contracts are unclear. Specify controlled versus internal state and standardize event behavior.
A passing isolated test still feels wrong Surrounding content, density, or workflow changes usability. Test the component in representative pages and conduct page-level user testing.

9. Performance, reliability, and maintenance

Reusable components can reduce duplicated code, but an abstraction also adds API, documentation, and testing cost. Keep the implementation as small as the responsibility allows. Avoid adding JavaScript for behavior native HTML already supplies, and keep optional enhancement separable where that helps loading or deployment. Do not claim a speed improvement without measuring the pages that use the component.

For reliability, make defaults safe, keep behavior predictable, handle empty and boundary inputs, and avoid hidden side effects. Treat public props, events, CSS hooks, and accessibility behavior as compatibility surfaces. When changing them, update examples and validate representative consumers. Cost is mostly engineering maintenance: an overly broad component can cost more to understand and adapt than a few focused implementations.

10. Capture reusable components in realistic pages

Visual review can help spot layout problems across viewport sizes, content lengths, and states. For a browser-based capture workflow, capture representative pages with the component in context and review the output alongside interaction and accessibility checks. A screenshot can show visual regressions; it cannot establish keyboard behavior or screen reader usability.

Or skip the browser setup

If you need screenshots of component demos or pages for visual review, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. See the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
  • Cookie banners are accepted and removed before capture, along with known newsletter popups and chat widgets; each step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers say the page verdict and whether it was billed.
  • An MCP server lets AI agents using Claude, Cursor, or another MCP client take screenshots.
  • 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000.

Sign up for 1,000 free screenshots a month, with no card.

FAQ

Should every repeated element become a component?

No. Extract a component when it has a clear responsibility or repeated behavior worth keeping consistent. Tiny one-off markup may be clearer left in its page.

Should a component include its page layout?

Usually, keep page-specific layout outside the component. The component should own its control or pattern; the page should compose it into the workflow.

Are Web Components required for a reusable component library?

No. Choose an approach compatible with your framework and target platforms. The important parts are a clear API, platform-familiar behavior, accessible interactions, documentation, and contextual testing.