Introduction: The Role of Foundational UI Components

Welcome back! In the previous lesson, you learned how to set up a consistent layout and client-side routing for your React app. Now that you have a solid structure, it’s time to focus on the building blocks of your user interface: foundational UI components.

Foundational UI components are small, reusable pieces of your app’s interface. They help you keep your code organized, make your app look consistent, and save you time when building new features. In this lesson, you will learn how to create three common UI components: a loading spinner, a search input, and a pagination control. These components are used in many real-world applications, and you will see how to build and use them in your own projects.

Building a Spinner Component

Let’s start by creating a Spinner component. A spinner is a small animation that shows users something is loading. This is helpful when you are waiting for data from a server or when an action takes a moment to complete.

Here is the code for a simple Spinner component:

// src/components/Spinner.tsx
export default function Spinner() {
  return (
    <div className="flex justify-center items-center p-4" aria-label="Loading">
      <svg
        className="animate-spin h-8 w-8 text-sky-400"
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
      >
        <circle
          className="opacity-25"
          cx="12"
          cy="12"
          r="10"
          stroke="currentColor"
          strokeWidth="4"
        ></circle>
        <path
          className="opacity-75"
          fill="currentColor"
          d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
        ></path>
      </svg>
    </div>
  );
}

Explanation:

  • The Spinner function returns a div containing an SVG. The SVG is styled to spin, creating a loading animation.
  • The aria-label="Loading" attribute helps screen readers understand what this element means, making your app more accessible.
  • The className values use Tailwind CSS utility classes for styling and animation. On CodeSignal, Tailwind is pre-installed, so you do not need to set it up yourself.

Output:
When you use <Spinner /> in your app, you will see a spinning blue circle, indicating that something is loading.

The outer div uses flex, justify-center, and items-center to center the spinner, while the p-4 padding prevents the SVG from feeling cramped; the svg spins because Tailwind’s animate-spin applies a keyframed rotation, and the color comes from text-sky-400, which sets currentColor so both the circle with stroke="currentColor" and the wedge path with fill="currentColor" share the same hue; the visual effect of a “missing slice” comes from rendering a full faint ring (opacity-25 stroke) behind a brighter arc (opacity-75 fill), which makes the rotation obvious without extra images or GIFs, and the accessible label (“Loading”) lets tests target it with queries like getByLabelText('Loading') while remaining visually minimal.

Creating a Search Input Component

Next, let’s build a SearchInput component. Search inputs are common in apps where users need to find information quickly. This component will include a search icon and be accessible to all users.

Here is the code for the SearchInput component:

// src/components/SearchInput.tsx
import { InputHTMLAttributes } from "react";

type SearchInputProps = InputHTMLAttributes<HTMLInputElement>;

export default function SearchInput(props: SearchInputProps) {
  return (
    <div className="relative">
      <span className="absolute inset-y-0 left-0 flex items-center pl-3">
        <svg className="h-5 w-5 text-slate-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
          <path fillRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clipRule="evenodd" />
        </svg>
      </span>
      <input
        type="search"
        className="w-full bg-slate-800 border border-slate-700 rounded-md py-2 pl-10 pr-4 focus:outline-none focus:ring-2 focus:ring-sky-500"
        {...props}
      />
    </div>
  );
}

Explanation:

  • The component accepts all standard input props, so you can use it just like a regular input.
  • The search icon is placed inside the input using absolute positioning.
  • The input is styled for a modern look and is accessible with proper focus and ARIA attributes.

Output:
When you use <SearchInput placeholder="Search..." />, you will see a search box with a search icon on the left.

Typing the props as InputHTMLAttributes<HTMLInputElement> means consumers can pass any standard input attribute (e.g., value, onChange, name, aria-*) without redefining them, and because the component spreads {...props} on the <input> after specifying its own attributes, a consumer-provided className or type will override the defaults; the wrapper div is relative so the icon’s container can be absolute with inset-y-0 left-0 and flex items-center to vertically center the SVG, and the input’s pl-10 reserves space so the text doesn’t overlap the icon; the SVG has aria-hidden="true" because it’s purely decorative, while the input gains its accessible name from whatever the caller provides (e.g., aria-label in the demo), and focus:ring-2 focus:ring-sky-500 ensures a clear focus outline for keyboard users.

Implementing a Pagination Component

Pagination helps users move through large sets of data, such as lists or tables. Let’s create a simple Pagination component with "Previous" and "Next" buttons.

Here is the code for the Pagination component:

// src/components/Pagination.tsx
type Props = {
  page: number;
  totalPages: number;
  onChange: (next: number) => void;
};

export default function Pagination({ page, totalPages, onChange }: Props) {
  const prev = () => onChange(Math.max(1, page - 1));
  const next = () => onChange(Math.min(totalPages, page + 1));

  return (
    <nav className="flex justify-center items-center space-x-2 mt-8" aria-label="Pagination">
      <button
        className="px-4 py-2 rounded-md bg-slate-800 hover:bg-slate-700 disabled:opacity-50"
        onClick={prev}
        disabled={page === 1}
      >
        Previous
      </button>
      <span className="px-3 py-2 rounded-md bg-sky-500 text-white font-bold">
        {page}
      </span>
      <button
        className="px-4 py-2 rounded-md bg-slate-800 hover:bg-slate-700"
        onClick={next}
        disabled={page === totalPages}
      >
        Next
      </button>
    </nav>
  );
}

Explanation:

  • The component takes the current page, the total number of pages, and a function to handle page changes.
  • The "Previous" button is disabled on the first page, and the "Next" button is disabled on the last page.
  • The current page number is shown in the middle, styled to stand out.

The local prev and next handlers compute a target page and immediately call onChange with that number, using Math.max(1, page - 1) and Math.min(totalPages, page + 1) to clamp the value so it never falls outside [1, totalPages]; the nav element with aria-label="Pagination" gives screen readers a clear region name, the disabled state is derived purely from the page and totalPages props (so a state update in the parent automatically updates button interactivity), and wiring it as onChange={setPage} works because the React state setter expects a number when updating a numeric state—matching the component’s callback shape exactly.

Output:
When you use <Pagination page={1} totalPages={5} onChange={setPage} />, you will see "Previous" and "Next" buttons with the current page number in between.

Demo: Combining Components on a Page

Now, let’s see how these components work together on a single page. Here is a demo page that uses all three components:

// src/pages/UiDemoPage.tsx
import { useState } from "react";
import Spinner from "../components/Spinner";
import SearchInput from "../components/SearchInput";
import Pagination from "../components/Pagination";

export default function UiDemoPage() {
  const [query, setQuery] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [page, setPage] = useState(1);

  const fakeSearch = () => {
    setIsLoading(true);
    setTimeout(() => setIsLoading(false), 800); // mock delay for demo
  };

  return (
    <section>
      <h1 className="text-3xl font-bold">UI Components Demo</h1>
      <p className="mt-2 text-slate-400">
        Preview foundational components with mock-only interactions.
      </p>

      <div className="mt-6 space-y-4 rounded-lg border border-slate-700 p-4">
        <div className="flex items-center gap-3">
          <SearchInput
            placeholder="Search (mock)"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            aria-label="Search demo"
          />
          <button
            className="bg-sky-500 hover:bg-sky-600 px-4 py-2 rounded-md font-semibold"
            onClick={fakeSearch}
          >
            Search
          </button>
        </div>

        {isLoading ? (
          <Spinner />
        ) : (
          <p className="text-slate-300">
            Showing mock results for: <span className="font-mono">{query || "(empty)"}</span>
          </p>
        )}

        <Pagination page={page} totalPages={5} onChange={setPage} />
      </div>
    </section>
  );
}

Explanation:

  • The page uses React’s useState to manage the search query, loading state, and current page.
  • When the "Search" button is clicked, the spinner appears for a short time to simulate loading.
  • The search input, spinner, and pagination are all displayed together, showing how these components can be reused and combined.

The fakeSearch function is intentionally simple: it sets isLoading to true immediately, then schedules a setIsLoading(false) call after 800ms using setTimeout. This introduces a mock delay that mimics the waiting period you would normally experience when making an API request. The purpose here is not to actually fetch data but to test the Spinner component in a realistic scenario—you get to see the spinner appear, remain visible for a moment, and then disappear when the "request" completes. This is valuable because it confirms the spinner correctly responds to changes in state, demonstrates how a loading indicator improves user experience during waits, and prepares you to later replace the mock delay with a real asynchronous fetch. By using a predictable timeout instead of a real network call, the demo stays deterministic and easy to test without external dependencies.

Output:
On this page, you can type in the search box, click "Search" to see the spinner, and use the pagination controls to change the page number.

Summary And What’s Next

In this lesson, you learned how to build three foundational UI components: a loading spinner, a search input, and a pagination control. You also saw how to combine them on a single page to create a smooth and interactive user experience.

These components are the building blocks for many features in modern web apps. In the next set of practice exercises, you will get hands-on experience using and customizing these components. This will help you become more comfortable building your own reusable UI elements in React. Good luck, and have fun practicing!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal