Buttons, cards and badges are the atoms of every interface, and getting them right once pays off on every page. In this lesson you will build a button system with variants and sizes, cards with media and equal heights, a clickable card, and status badges from utilities alone.
Every button shares one skeleton — inline flex, padding, radius, weight, focus ring and disabled styling — and only the colors change per variant.
<!-- Primary -->
<button class="inline-flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white
hover:bg-blue-700 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500
focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50">Save</button>A secondary variant swaps in border border-gray-300 bg-white hover:bg-gray-50, a destructive one bg-red-600 hover:bg-red-700, and a ghost variant (text-gray-700 hover:bg-gray-100) suits toolbars. Sizes change only padding and text — px-3 py-1.5 text-sm, px-4 py-2 text-sm, px-5 py-2.5 text-base — while the radius stays identical so buttons look related.
Icon-only buttons need a square hit area and a text alternative:
<button class="inline-flex size-9 items-center justify-center rounded-md text-gray-600 hover:bg-gray-100"
aria-label="Settings">
<svg class="size-5" aria-hidden="true">...</svg>
</button>size-9 gives a 36px target, aria-label supplies the announced name, and aria-hidden hides the icon from assistive technology. A loading button keeps its text and adds a size-4 animate-spin icon, disabled and aria-busy="true", so its width never changes.
Three details make a good card: overflow-hidden so the image respects the radius, a flex column so the footer sits at the bottom, and h-full so cards in a row match heights:
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<article class="flex h-full flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
<img class="aspect-video w-full object-cover" src="cover.jpg" alt="" />
<div class="flex flex-1 flex-col p-5">
<h3 class="text-lg font-semibold">Title</h3>
<p class="mt-1 text-sm text-gray-600">Description.</p>
<div class="mt-auto pt-4 text-sm text-gray-500">3 min read</div>
</div>
</article>
</div>Grid items stretch by default, h-full passes that height to the card, flex-1 lets the body grow, and mt-auto pushes the footer down, so every card in the row ends on the same line.
When the whole card should be a link, keep one real <a> and stretch its hit area with a pseudo-element:
<article class="group relative rounded-xl border p-5 transition hover:border-blue-500 hover:shadow-md">
<h3 class="font-semibold group-hover:text-blue-600">
<a href="/post/1" class="after:absolute after:inset-0">Read the article</a>
</h3>
</article>relative on the card and after:absolute after:inset-0 on the link make the whole card clickable while the link text stays the accessible name; other interactive elements inside need relative z-10.
Badges are small inline labels: rounded, tight padding, small bold text, and a tinted background with darker text from the same family:
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800">Active</span>
<span class="inline-flex items-center gap-1 rounded-full bg-yellow-100 px-2.5 py-0.5 text-xs font-medium text-yellow-800">
<span class="size-1.5 rounded-full bg-yellow-500"></span> Pending
</span>The 100/800 pairing keeps contrast readable; in dark mode use dark:bg-green-900/40 dark:text-green-300.
<div onclick> instead of a stretched link; keyboard users cannot reach it.opacity-50 for disabled buttons without the disabled attribute itself.Which utility pushes a card's footer to the bottom in a flex column?
inline-flex items-center gap-2 rounded-md px-4 py-2 font-semibold plus focus and disabled styles) and vary only colors.aria-label and a square size-*; loading buttons keep their width.overflow-hidden rounded-xl border shadow-sm, a flex column with mt-auto, and h-full for equal heights.after:absolute after:inset-0 to make a card clickable.100 background with an 800 text color from the same family.Next lesson: Styling Forms and Inputs — build accessible text fields, selects, checkboxes and validation states.