Summit Themes
Blog

How to build a team grid section with a cta card using Tailwind CSS

A team section does two jobs at once: it builds trust by putting real faces to the product, and it can quietly nudge visitors toward an action — joining the team, booking a call, or reaching out. The cleanest way to handle both in a single layout is to treat one grid cell as a CTA card. It lives in the same grid as the team member cards, so it feels native rather than bolted on, but its content is a prompt rather than a bio.

This tutorial builds a responsive four-column team grid with a CTA card occupying the last slot. Everything uses Tailwind CSS v4 utility classes with the CSS-first @theme configuration approach. No JavaScript required.

What we are building

The finished section has:

  • A heading and subheading centered above the grid.
  • A ul with role="list" as the grid container — semantically correct for a list of people.
  • Three team member cards, each with a portrait image, name, role, and subtle social link.
  • One CTA card in the final slot, visually distinct (darker background, large heading, a button), but the same height and width as every other card.

Setting up the @theme layer

In Tailwind v4, custom design tokens live in CSS using @theme instead of a JavaScript config file. Open your main CSS file (typically global.css or styles.css) and add any custom tokens you need. For this component the defaults are mostly fine, but here is how you would define a brand accent color and a custom grid breakpoint if your design needs them:

@import "tailwindcss";

@theme {
  --color-brand: oklch(55% 0.22 260);
  --color-brand-dark: oklch(40% 0.22 260);
  --breakpoint-3xl: 1920px;
}

With those tokens declared, you can now use bg-brand, text-brand, hover:bg-brand-dark, and 3xl:grid-cols-5 anywhere in your markup. For this tutorial we will stick with Tailwind's built-in palette so the snippet is self-contained, but the pattern is the same.

The HTML structure

Start with the outer section wrapper, a text header, and the grid list. The role="list" attribute on ul is necessary because some screen-reader/browser combinations strip list semantics from a ul that has its list-style removed (which Tailwind's reset does). Adding the role back is a one-attribute fix.

<section class="bg-white py-24 sm:py-32">
  <div class="mx-auto max-w-7xl px-6 lg:px-8">

    <!-- Section header -->
    <div class="mx-auto max-w-2xl text-center">
      <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
        Meet the team
      </h2>
      <p class="mt-4 text-lg text-gray-600">
        We're a small crew that cares deeply about the work. If that sounds like you, we're hiring.
      </p>
    </div>

    <!-- Team grid -->
    <ul
      role="list"
      class="mx-auto mt-20 grid max-w-2xl grid-cols-1 gap-6 sm:grid-cols-2 lg:mx-0 lg:max-w-none lg:grid-cols-4"
    >

      <!-- Team member card (repeat for each person) -->
      <li class="flex flex-col overflow-hidden rounded-2xl bg-gray-50">
        <img
          src="/images/team/sarah.jpg"
          alt="Sarah Kim"
          class="aspect-[3/4] w-full object-cover"
        />
        <div class="flex flex-1 flex-col p-6">
          <h3 class="text-base font-semibold text-gray-900">Sarah Kim</h3>
          <p class="text-sm text-gray-500">Lead designer</p>
          <div class="mt-auto pt-4">
            <a
              href="https://linkedin.com/in/sarah-kim"
              class="text-sm text-gray-400 hover:text-gray-700 transition-colors"
              rel="noopener noreferrer"
              target="_blank"
            >
              LinkedIn →
            </a>
          </div>
        </div>
      </li>

      <!-- Second team member -->
      <li class="flex flex-col overflow-hidden rounded-2xl bg-gray-50">
        <img
          src="/images/team/marcus.jpg"
          alt="Marcus Webb"
          class="aspect-[3/4] w-full object-cover"
        />
        <div class="flex flex-1 flex-col p-6">
          <h3 class="text-base font-semibold text-gray-900">Marcus Webb</h3>
          <p class="text-sm text-gray-500">Front-end engineer</p>
          <div class="mt-auto pt-4">
            <a
              href="https://github.com/marcuswebb"
              class="text-sm text-gray-400 hover:text-gray-700 transition-colors"
              rel="noopener noreferrer"
              target="_blank"
            >
              GitHub →
            </a>
          </div>
        </div>
      </li>

      <!-- Third team member -->
      <li class="flex flex-col overflow-hidden rounded-2xl bg-gray-50">
        <img
          src="/images/team/priya.jpg"
          alt="Priya Nair"
          class="aspect-[3/4] w-full object-cover"
        />
        <div class="flex flex-1 flex-col p-6">
          <h3 class="text-base font-semibold text-gray-900">Priya Nair</h3>
          <p class="text-sm text-gray-500">Content strategist</p>
          <div class="mt-auto pt-4">
            <a
              href="https://twitter.com/priyanair"
              class="text-sm text-gray-400 hover:text-gray-700 transition-colors"
              rel="noopener noreferrer"
              target="_blank"
            >
              Twitter →
            </a>
          </div>
        </div>
      </li>

      <!-- CTA card (final grid slot) -->
      <li class="flex flex-col justify-between rounded-2xl bg-gray-900 p-8 text-white">
        <div>
          <p class="text-sm font-semibold uppercase tracking-widest text-gray-400">
            We're hiring
          </p>
          <h3 class="mt-4 text-2xl font-bold leading-snug">
            Want to join the crew?
          </h3>
          <p class="mt-3 text-sm text-gray-400">
            We work async, ship fast, and take craft seriously. Open roles below.
          </p>
        </div>
        <div class="mt-8">
          <a
            href="/careers"
            class="inline-block rounded-lg bg-white px-5 py-2.5 text-sm font-semibold text-gray-900 hover:bg-gray-100 transition-colors"
          >
            See open roles
          </a>
        </div>
      </li>

    </ul>
  </div>
</section>

Breaking down the key decisions

The grid column classes

grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gives you a single column on mobile, two on small screens, and four on large screens. The four-column layout is where the CTA card shines: it fills a natural slot without any gap or awkward empty cell, because three team members plus one CTA card equals exactly four. If you have five or seven members, adjust accordingly — either add a fifth column breakpoint or let the CTA card span two columns with sm:col-span-2.

The aspect-ratio on portrait images

aspect-[3/4] uses Tailwind v4's arbitrary value syntax to enforce a portrait crop on every card. Pair it with object-cover so the image fills the area regardless of the source dimensions. This keeps all portrait images the same height without fixed pixel values, which would break at different font scales or viewport widths.

Keeping card heights consistent with flex

The team member li uses flex flex-col. The inner content wrapper also uses flex flex-1 flex-col. The social link div at the bottom gets mt-auto, which pushes it to the bottom regardless of how long the bio name and title are. This means cards with longer names stay aligned with cards with shorter ones — no JavaScript height-matching needed.

The CTA card uses flex flex-col justify-between on the li itself since there is no image to account for. The top block holds the eyebrow text, heading, and body copy. The bottom block holds the button. justify-between spreads them apart so the button always sits at the card's base, matching the social links on the team cards visually.

The CTA card's visual contrast

bg-gray-900 against the bg-gray-50 of the photo cards creates clear hierarchy without introducing a jarring accent color. The button reverses the palette: bg-white text-gray-900. This kind of inverted contrast is easier to read than a colored button on a dark card, and it requires zero extra token decisions.

Why not use a div instead of ul/li?

A grid of people is a list of people. Using ul with role="list" and li items gives screen readers the item count ("list, 4 items") and lets users navigate by list item. A div grid works visually but loses that semantic signal. Since the CTA card is in the same list, assistive technology will announce it as item four — which is accurate; it is a card in a set of cards.

Responsive behavior at each breakpoint

At mobile (grid-cols-1): all four cards stack vertically. The CTA card appears last and reads like a natural footer to the team section.

At sm (grid-cols-2): the first two team members appear in row one, the third member and the CTA card in row two. The CTA card height matches the third member card because both use the same flex layout — the CTA card just has no image, so its internal content spreads to fill the space.

At lg (grid-cols-4): all four appear in a single row. This is the intended final state for desktops.

Adapting this pattern

A few common variations worth knowing:

  • More team members: If you have seven members and want the CTA card in the eighth slot, change the grid to lg:grid-cols-4 and let two rows fill naturally. No layout change needed.
  • CTA card spanning two columns: Add sm:col-span-2 to the CTA card li if you want it to be wider than the team cards at tablet widths.
  • A colored accent instead of dark: Replace bg-gray-900 with bg-brand (after defining --color-brand in your @theme block). The button then becomes bg-white text-brand.
  • No image cards: If your team section uses text-only cards (name, role, bio text), remove the img element and the aspect-[3/4] wrapper. The flex flex-col layout on the li still keeps card heights consistent within a row.

Accessibility checklist

  • Every img has a descriptive alt attribute with the person's name.
  • The ul has role="list" to restore list semantics stripped by Tailwind's reset.
  • External links use rel="noopener noreferrer" and target="_blank" together — never one without the other on external links.
  • The CTA button is an a tag pointing to a real URL, not a button element, because it navigates rather than triggering an in-page action.
  • Color contrast: white text on gray-900 and dark text on white both clear WCAG AA at normal text sizes.

The team-grid-with-CTA-card pattern is one of the more efficient layouts you can build in a service business website. It puts social proof and a conversion prompt in the same visual unit, keeps the DOM flat and semantic, and requires no JavaScript. Once you have the base structure above, swapping in real photos, names, and a different CTA destination is a ten-minute job.