Summit Themes
Blog

How to build a responsive four-step onboarding section with Tailwind CSS

An onboarding section — the "how it works" or "get started in four steps" block you see on most service landing pages — is deceptively simple to design but easy to get wrong in the markup. The numbers need to look good, the connector lines between steps need to disappear gracefully on mobile, and the whole thing needs to be readable at any viewport width. Tailwind CSS v4 makes this straightforward once you understand how its grid utilities and CSS-first configuration work together.

This guide builds the section from scratch: semantic HTML first, then the Tailwind layout, then the responsive connector, then optional custom tokens using the @theme directive. Every code block is copy-pasteable and tested against the v4 API. The result is a reusable component that works in any HTML file, Astro component, or framework template.

What we are building

A four-column grid on wide screens that collapses to a single column on mobile. Each step has a numbered circle, a heading, and a short description. Between the circles, a horizontal dashed line connects the steps on desktop — it disappears entirely on mobile so the vertical stack reads cleanly. No JavaScript, no external icons, just HTML and Tailwind classes.

The HTML structure

Start with semantic, accessible markup. An ordered list is the right element for a sequence of steps — screen readers understand the numbering, and you can visually replace the default counter with your own styled badge.

<section class="py-20 px-6">
  <div class="max-w-5xl mx-auto">

    <h2 class="text-3xl font-bold text-center mb-4">
      How it works
    </h2>
    <p class="text-center text-gray-500 mb-16 max-w-xl mx-auto">
      From first contact to live site — here is what to expect.
    </p>

    <ol class="grid grid-cols-1 md:grid-cols-4 gap-8 relative">

      <li class="flex flex-col items-center text-center">
        <div class="step-badge">1</div>
        <h3 class="mt-4 font-semibold text-lg">Book a call</h3>
        <p class="mt-2 text-sm text-gray-500">
          A 20-minute discovery call to understand your business and goals.
        </p>
      </li>

      <li class="flex flex-col items-center text-center">
        <div class="step-badge">2</div>
        <h3 class="mt-4 font-semibold text-lg">Choose your theme</h3>
        <p class="mt-2 text-sm text-gray-500">
          Pick the design that fits your niche. We customize it to match your brand.
        </p>
      </li>

      <li class="flex flex-col items-center text-center">
        <div class="step-badge">3</div>
        <h3 class="mt-4 font-semibold text-lg">We fill in the details</h3>
        <p class="mt-2 text-sm text-gray-500">
          Your services, service areas, photos, and copy — all in one pass.
        </p>
      </li>

      <li class="flex flex-col items-center text-center">
        <div class="step-badge">4</div>
        <h3 class="mt-4 font-semibold text-lg">Go live</h3>
        <p class="mt-2 text-sm text-gray-500">
          Your site is deployed to a fast global CDN and ready to take leads.
        </p>
      </li>

    </ol>
  </div>
</section>

The step-badge class is a placeholder — we will define it using Tailwind's @layer in a moment. The relative on the <ol> is important: the connector line will be positioned absolutely relative to it.

Setting up Tailwind CSS v4

In v4, there is no tailwind.config.js. Configuration lives in your CSS file using the @import and @theme directives. A minimal setup looks like this:

/* src/styles/global.css */
@import "tailwindcss";

@theme {
  --color-brand: #2563eb;
  --color-brand-light: #dbeafe;
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
}

That single @import "tailwindcss"; line pulls in the full Tailwind base, components, and utilities. The @theme block registers custom design tokens that become utility classes automatically — bg-brand, text-brand, bg-brand-light are all usable immediately with no additional config.

If you are using Vite, install the Vite plugin and add it to vite.config.ts:

// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [tailwindcss()],
});

No PostCSS config needed. The plugin handles class detection, CSS transforms, and HMR automatically.

Styling the step badge

The numbered circle should be visually prominent — large enough to read at a glance but not so large that it crowds the text. Use @layer components in your CSS to define the reusable badge style, keeping the markup clean:

/* src/styles/global.css */
@import "tailwindcss";

@theme {
  --color-brand: #2563eb;
  --color-brand-light: #dbeafe;
}

@layer components {
  .step-badge {
    @apply flex items-center justify-center
           w-12 h-12
           rounded-full
           bg-brand text-white
           text-lg font-bold
           shrink-0
           z-10 relative;
  }
}

The z-10 relative pair is deliberate: the badge sits above the connector line we are about to add. shrink-0 prevents the circle from collapsing when the flex container squeezes on small screens.

Adding the horizontal connector line

The connector is the trickiest part. On desktop, a line should run between the centre of each numbered badge. The cleanest technique is a single pseudo-element on the <ol> itself — one horizontal rule that spans the full width of the grid, vertically centered at the badge height.

Add this to your stylesheet:

@layer components {
  .steps-connector {
    @apply hidden md:block
           absolute
           top-6
           left-[calc(12.5%+1.5rem)]
           right-[calc(12.5%+1.5rem)]
           h-px
           border-t-2 border-dashed border-brand-light
           z-0;
  }
}

Then add a <div class="steps-connector"></div> inside the <ol>, before the list items:

<ol class="grid grid-cols-1 md:grid-cols-4 gap-8 relative">
  <div class="steps-connector" aria-hidden="true"></div>

  <li>...</li>
  <li>...</li>
  <li>...</li>
  <li>...</li>
</ol>

The top-6 (1.5rem) positions the line at the vertical centre of a 3rem (h-12) badge. The left and right values use calc() to inset the line by half a column width plus the badge radius, so the line starts and ends at the centre of the first and last badge rather than running edge to edge. aria-hidden="true" keeps it invisible to screen readers — the line is purely decorative.

Making it responsive

The layout relies on two Tailwind grid classes that do most of the heavy lifting:

  • grid-cols-1 — single column by default (mobile). Each step stacks vertically.
  • md:grid-cols-4 — four equal columns at the md breakpoint (768px and above).

The connector div is hidden by default and md:block at the md breakpoint, so it only appears when the grid switches to four columns. You get a clean vertical stack on phone and a horizontal row on tablet and desktop with zero media query boilerplate in your CSS.

If you want the step items to align better on mid-size screens (say, two columns on tablet, four on desktop), adjust the grid classes:

<ol class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8 relative">

If you use the two-column intermediate breakpoint, hide the connector until lg by changing md:block to lg:block on the connector div.

The complete component

Putting it all together, here is the full, self-contained snippet:

<!-- CSS (src/styles/global.css) -->
<!--
@import "tailwindcss";

@theme {
  --color-brand: #2563eb;
  --color-brand-light: #dbeafe;
}

@layer components {
  .step-badge {
    @apply flex items-center justify-center
           w-12 h-12 rounded-full
           bg-brand text-white
           text-lg font-bold
           shrink-0 z-10 relative;
  }

  .steps-connector {
    @apply hidden md:block
           absolute top-6
           left-[calc(12.5%+1.5rem)]
           right-[calc(12.5%+1.5rem)]
           h-px border-t-2 border-dashed border-brand-light
           z-0;
  }
}
-->

<section class="py-20 px-6">
  <div class="max-w-5xl mx-auto">

    <h2 class="text-3xl font-bold text-center mb-4">
      How it works
    </h2>
    <p class="text-center text-gray-500 mb-16 max-w-xl mx-auto">
      From first contact to live site — here is what to expect.
    </p>

    <ol class="grid grid-cols-1 md:grid-cols-4 gap-8 relative">
      <div class="steps-connector" aria-hidden="true"></div>

      <li class="flex flex-col items-center text-center">
        <div class="step-badge">1</div>
        <h3 class="mt-4 font-semibold text-lg">Book a call</h3>
        <p class="mt-2 text-sm text-gray-500">
          A 20-minute discovery call to understand your business and goals.
        </p>
      </li>

      <li class="flex flex-col items-center text-center">
        <div class="step-badge">2</div>
        <h3 class="mt-4 font-semibold text-lg">Choose your theme</h3>
        <p class="mt-2 text-sm text-gray-500">
          Pick the design that fits your niche. We customize it to match your brand.
        </p>
      </li>

      <li class="flex flex-col items-center text-center">
        <div class="step-badge">3</div>
        <h3 class="mt-4 font-semibold text-lg">We fill in the details</h3>
        <p class="mt-2 text-sm text-gray-500">
          Your services, service areas, photos, and copy — all in one pass.
        </p>
      </li>

      <li class="flex flex-col items-center text-center">
        <div class="step-badge">4</div>
        <h3 class="mt-4 font-semibold text-lg">Go live</h3>
        <p class="mt-2 text-sm text-gray-500">
          Your site is deployed to a fast global CDN and ready to take leads.
        </p>
      </li>
    </ol>

  </div>
</section>

Using it as an Astro component

If you are working in an Astro project, extract this into a .astro component and accept the steps as a prop so the section is reusable across pages:

---
// src/components/OnboardingSteps.astro
interface Step {
  number: number;
  heading: string;
  body: string;
}

interface Props {
  title?: string;
  subtitle?: string;
  steps: Step[];
}

const {
  title = "How it works",
  subtitle = "From first contact to live site.",
  steps,
} = Astro.props;
---

<section class="py-20 px-6">
  <div class="max-w-5xl mx-auto">
    <h2 class="text-3xl font-bold text-center mb-4">{title}</h2>
    <p class="text-center text-gray-500 mb-16 max-w-xl mx-auto">{subtitle}</p>

    <ol class="grid grid-cols-1 md:grid-cols-4 gap-8 relative">
      <div class="steps-connector" aria-hidden="true"></div>

      {steps.map((step) => (
        <li class="flex flex-col items-center text-center">
          <div class="step-badge">{step.number}</div>
          <h3 class="mt-4 font-semibold text-lg">{step.heading}</h3>
          <p class="mt-2 text-sm text-gray-500">{step.body}</p>
        </li>
      ))}
    </ol>
  </div>
</section>

Call it on any page by importing the component and passing an array of step objects. The number, heading, and body are data — the layout and styling never change.

A few things worth knowing

The connector line calc() values

The left-[calc(12.5%+1.5rem)] expression works because each column in a four-column grid is 25% wide, and the badge is centred inside that column. Half of 25% is 12.5%, which is where the centre of the first badge falls. Adding 1.5rem (half of w-12, which is 3rem) nudges the line start to the edge of the badge circle rather than its centre, so the line genuinely connects circle to circle rather than running underneath them. Adjust this value if you change w-12.

Gap and the connector

Tailwind's gap-8 adds gutters between grid cells, but it does not affect the absolute-positioned connector div. The connector spans the full grid width based on the <ol>'s bounding box, which is what you want — the calculation above handles the inset manually.

Accessibility

Use a real <ol> (ordered list), not a <div> grid. Screen readers announce "list of four items" and convey sequence automatically. The numbered badge is redundant for screen readers — which is fine, redundancy does not harm accessibility. The aria-hidden="true" on the connector div prevents it from being announced as an empty element.

Wrapping up

The pattern here — semantic ordered list, CSS grid for the two-breakpoint layout, an absolutely-positioned decorative connector, and component tokens via @theme — is stable and easy to maintain. When the step content changes you touch the data. When the brand color changes you update one token in the CSS file. The grid and the connector are written once. That separation is what makes Tailwind v4 particularly good for sections like this: the design system lives in CSS, the structure lives in HTML, and neither reaches into the other.