Overview
A pricing page is one of the clearest real-world showcases for CSS Grid: three (or more) plans of roughly equal importance need to sit side by side in perfectly even columns, something that is awkward to do reliably with floats or even Flexbox once you also need one card to visually stand out from the others. This project builds a classic SaaS-style pricing table — Basic, Pro, and Enterprise — with the middle "Pro" plan highlighted as the recommended, most-popular option.
Beyond the grid layout itself, this project leans heavily on the box model: consistent padding inside every card, a shared border and border-radius, and a drop shadow that gives the cards a sense of depth against the page background. By the end, the same markup reflows from three columns, to two, to one, purely through media queries.
- A three-column pricing grid built with `display: grid` and `grid-template-columns`.
- Consistent, well-spaced plan cards using padding, borders, and border-radius.
- A visually highlighted "Pro" plan with a colored border, shadow, and a floating badge.
- A checkmark-prefixed feature list built with the `::before` pseudo-element.
- A responsive layout that goes from three columns, to two, to a single stacked column.
Prerequisites
- The CSS box model — padding, border, margin, and how they combine to size an element.
- CSS Grid basics — `display: grid`, `grid-template-columns`, and the `fr` unit.
- The `::before` pseudo-element and the `content` property.
- Positioning — `position: relative` and `position: absolute` for the featured badge.
- Media queries — applying different `grid-template-columns` values at different viewport widths.
Project Structure
The markup is fixed from here on: one `<section class="pricing">` containing an intro heading and a `.pricing__grid` with three `<article class="plan">` cards inside it. The middle card additionally carries a `plan--featured` modifier class and an extra "Most Popular" badge paragraph — everything else about the three cards is structurally identical, which is exactly what lets one shared `.plan` CSS rule style all three, with `.plan--featured` layered on top for the one exception.
<!-- Whole pricing section: intro text plus the 3-card grid --><section class="pricing"> <div class="pricing__intro"> <h1>Simple, Transparent Pricing</h1> <p>Choose the plan that fits your team. Upgrade or cancel anytime.</p> </div>
<div class="pricing__grid"> <!-- Basic plan: a plain, unhighlighted card --> <article class="plan"> <h2 class="plan__name">Basic</h2> <p class="plan__price">$9<span class="plan__period">/mo</span></p> <p class="plan__tagline">For individuals just getting started.</p> <ul class="plan__features"> <li>5 projects</li> <li>2GB storage</li> <li>Community support</li> <li>Basic analytics</li> </ul> <a href="#" class="plan__cta">Choose Basic</a> </article>
<!-- Pro plan: same structure as Basic, plus the "plan--featured" modifier and a badge --> <article class="plan plan--featured"> <p class="plan__badge">Most Popular</p> <h2 class="plan__name">Pro</h2> <p class="plan__price">$29<span class="plan__period">/mo</span></p> <p class="plan__tagline">For growing teams that need more power.</p> <ul class="plan__features"> <li>Unlimited projects</li> <li>50GB storage</li> <li>Priority support</li> <li>Advanced analytics</li> <li>Team collaboration</li> </ul> <a href="#" class="plan__cta">Choose Pro</a> </article>
<!-- Enterprise plan: back to the plain card structure --> <article class="plan"> <h2 class="plan__name">Enterprise</h2> <p class="plan__price">$79<span class="plan__period">/mo</span></p> <p class="plan__tagline">For organizations with custom needs.</p> <ul class="plan__features"> <li>Unlimited everything</li> <li>500GB storage</li> <li>Dedicated support</li> <li>Custom integrations</li> <li>SLA guarantee</li> </ul> <a href="#" class="plan__cta">Choose Enterprise</a> </article> </div></section>Step 1: Reset Defaults and Build the Page Shell
As always, we start with a small reset and a centered page container. `.pricing__intro` gets a centered text block that visually separates the heading from the grid of cards below it.
* { margin: 0; padding: 0; box-sizing: border-box; /* Padding/border count inward, so a card's declared width stays exact */}
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; background: #f1f5f9; /* Light neutral background so the white plan cards stand out clearly */ color: #1e293b; line-height: 1.5;}
.pricing { max-width: 1100px; /* Caps the section's width so cards don't stretch absurdly wide on big monitors */ margin: 0 auto; /* Centers the section horizontally on the page */ padding: 4rem 1.5rem; /* Generous top/bottom space, plus a side gutter on narrow screens */}
.pricing__intro { text-align: center; margin-bottom: 3rem; /* Separates the intro text block from the card grid below */}
.pricing__intro h1 { font-size: 2rem; margin-bottom: 0.5rem;}
.pricing__intro p { color: #64748b; /* Muted gray subtext — lower visual priority than the black heading */}Step 2: Lay Out the Three Columns with Grid
This is where Grid earns its place over Flexbox: `grid-template-columns: repeat(3, 1fr)` guarantees three perfectly equal-width columns no matter how much content is inside each card, and `gap` handles the spacing between them without any manual margin math.
.pricing__grid { display: grid; /* Grid layout: precise, even columns instead of flex-wrapping */ grid-template-columns: repeat(3, 1fr); /* Three columns, each taking an equal 1-fraction share of the width */ gap: 2rem; /* Consistent gutter between cards, both horizontally and vertically */ align-items: start; /* Cards align to the top of the row, independent of their own height */}Step 3: Style Each Plan Card
Every card shares the same box-model treatment: a white background, a subtle border, rounded corners, generous internal padding, and a soft shadow to lift it off the page. `display: flex; flex-direction: column` inside the card is what will let the call-to-action button sit pinned to the bottom in Step 5, regardless of how many feature list items a plan has.
.plan { background: #ffffff; border: 1px solid #e2e8f0; /* Subtle border defines the card's edge against the light page background */ border-radius: 12px; /* Soft rounded corners feel friendlier than sharp ones */ padding: 2rem 1.75rem; /* Inner spacing — part of the box model — keeps content off the card edges */ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08); /* Faint shadow lifts the card visually off the page */ display: flex; /* Column flex lets us push the CTA button to the bottom later (Step 5) */ flex-direction: column;}
.plan__name { font-size: 1.1rem; text-transform: uppercase; /* Small-caps styling reads as a category label rather than a headline */ letter-spacing: 0.05em; /* Slight letter spacing improves readability of the uppercase text */ color: #6366f1; margin-bottom: 0.75rem;}
.plan__price { font-size: 2.25rem; font-weight: 700; margin-bottom: 0.25rem;}
.plan__period { font-size: 1rem; font-weight: 400; color: #64748b; /* De-emphasized relative to the large price number beside it */}
.plan__tagline { color: #64748b; margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #e2e8f0; /* Divider line separates the intro text from the feature list below it */}Step 4: Highlight the Featured Plan
The "Pro" plan needs to visually pop out of the row. `transform: scale(1.05)` makes it slightly larger than its siblings, a colored border and stronger shadow reinforce the emphasis, and `position: relative` on the card creates the positioning context that lets the "Most Popular" badge float half outside the card's top edge.
.plan--featured { border: 2px solid #6366f1; /* Thicker, colored border distinguishes it from the plain gray cards */ transform: scale(1.05); /* Slightly larger scale makes it visually "pop" out of the row */ box-shadow: 0 10px 25px rgba(99, 102, 241, 0.25); /* Stronger, tinted shadow reinforces the emphasis */ position: relative; /* Positioning context for the absolutely-positioned badge below */}
.plan__badge { position: absolute; /* Removed from normal flow so it can float above the card's edge */ top: -14px; /* Sits half outside the card — a common "ribbon" placement */ left: 50%; transform: translateX(-50%); /* Centers the badge horizontally regardless of its text length */ background: #6366f1; color: #ffffff; font-size: 0.75rem; font-weight: 600; padding: 0.35rem 0.9rem; border-radius: 999px; /* Very large radius produces a fully rounded pill shape */}Step 5: Style the Feature List and Buttons
The feature list gets checkmarks added purely with CSS via `::before`, avoiding the need for an icon font or image. `flex-grow: 1` on the list is the piece that makes every card's CTA button line up at the same height, by letting the list expand to fill any leftover vertical space in shorter cards.
.plan__features { list-style: none; margin-bottom: 2rem; flex-grow: 1; /* Expands to fill leftover space, pushing every card's CTA to the same bottom line */}
.plan__features li { padding: 0.5rem 0; padding-left: 1.5rem; /* Reserves room on the left for the checkmark placed with ::before */ position: relative; /* Positioning context so the ::before checkmark can sit at a fixed offset */ color: #334155;}
.plan__features li::before { content: '\2713'; /* Unicode checkmark character, inserted without needing an <img> or icon font */ position: absolute; left: 0; color: #22c55e; /* Green reads as "included"/positive, distinct from the body text color */ font-weight: 700;}
.plan__cta { display: block; text-align: center; text-decoration: none; padding: 0.75rem 1rem; border-radius: 8px; font-weight: 600; background: #f1f5f9; color: #1e293b; border: 1px solid #cbd5e1; transition: background 0.2s ease, color 0.2s ease;}
.plan--featured .plan__cta { background: #6366f1; /* The featured plan's CTA is solid and high-contrast to match its overall emphasis */ color: #ffffff; border-color: #6366f1;}
.plan__cta:hover { background: #e2e8f0;}
.plan--featured .plan__cta:hover { background: #4f46e5; /* A darker hover shade specific to the featured plan's already-solid button */}Step 6: Make the Grid Responsive
Finally, the layout adapts at two breakpoints: below 900px the grid drops to a single stacked column (and the featured plan's scale is cancelled, since "popping out" looks odd at full width), and between 901px and 1100px it briefly shows two columns, with the featured plan spanning both so it does not sit alone in an awkward second row.
@media (max-width: 900px) { .pricing__grid { grid-template-columns: 1fr; /* Stack all three cards into a single column below tablet width */ }
.plan--featured { transform: none; /* Cancel the desktop "pop out" scale — it looks out of place at full mobile width */ order: -1; /* Moves the featured plan to the top of the stack, ahead of Basic, on mobile */ }}
@media (min-width: 901px) and (max-width: 1100px) { .pricing__grid { grid-template-columns: repeat(2, 1fr); /* Two columns on medium screens, before dropping to one */ }
.plan--featured { grid-column: span 2; /* Featured plan spans both columns so it isn't left alone in a lonely second row */ }}Complete Code
Here is the fixed HTML markup again for reference, followed by the complete stylesheet with every rule from Steps 1 through 6 assembled in order.
<section class="pricing"> <div class="pricing__intro"> <h1>Simple, Transparent Pricing</h1> <p>Choose the plan that fits your team. Upgrade or cancel anytime.</p> </div>
<div class="pricing__grid"> <article class="plan"> <h2 class="plan__name">Basic</h2> <p class="plan__price">$9<span class="plan__period">/mo</span></p> <p class="plan__tagline">For individuals just getting started.</p> <ul class="plan__features"> <li>5 projects</li> <li>2GB storage</li> <li>Community support</li> <li>Basic analytics</li> </ul> <a href="#" class="plan__cta">Choose Basic</a> </article>
<article class="plan plan--featured"> <p class="plan__badge">Most Popular</p> <h2 class="plan__name">Pro</h2> <p class="plan__price">$29<span class="plan__period">/mo</span></p> <p class="plan__tagline">For growing teams that need more power.</p> <ul class="plan__features"> <li>Unlimited projects</li> <li>50GB storage</li> <li>Priority support</li> <li>Advanced analytics</li> <li>Team collaboration</li> </ul> <a href="#" class="plan__cta">Choose Pro</a> </article>
<article class="plan"> <h2 class="plan__name">Enterprise</h2> <p class="plan__price">$79<span class="plan__period">/mo</span></p> <p class="plan__tagline">For organizations with custom needs.</p> <ul class="plan__features"> <li>Unlimited everything</li> <li>500GB storage</li> <li>Dedicated support</li> <li>Custom integrations</li> <li>SLA guarantee</li> </ul> <a href="#" class="plan__cta">Choose Enterprise</a> </article> </div></section>* { margin: 0; padding: 0; box-sizing: border-box;}
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; background: #f1f5f9; color: #1e293b; line-height: 1.5;}
.pricing { max-width: 1100px; margin: 0 auto; padding: 4rem 1.5rem;}
.pricing__intro { text-align: center; margin-bottom: 3rem;}
.pricing__intro h1 { font-size: 2rem; margin-bottom: 0.5rem;}
.pricing__intro p { color: #64748b;}
.pricing__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; align-items: start;}
.plan { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 2rem 1.75rem; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08); display: flex; flex-direction: column;}
.plan__name { font-size: 1.1rem; text-transform: uppercase; letter-spacing: 0.05em; color: #6366f1; margin-bottom: 0.75rem;}
.plan__price { font-size: 2.25rem; font-weight: 700; margin-bottom: 0.25rem;}
.plan__period { font-size: 1rem; font-weight: 400; color: #64748b;}
.plan__tagline { color: #64748b; margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #e2e8f0;}
.plan--featured { border: 2px solid #6366f1; transform: scale(1.05); box-shadow: 0 10px 25px rgba(99, 102, 241, 0.25); position: relative;}
.plan__badge { position: absolute; top: -14px; left: 50%; transform: translateX(-50%); background: #6366f1; color: #ffffff; font-size: 0.75rem; font-weight: 600; padding: 0.35rem 0.9rem; border-radius: 999px;}
.plan__features { list-style: none; margin-bottom: 2rem; flex-grow: 1;}
.plan__features li { padding: 0.5rem 0; padding-left: 1.5rem; position: relative; color: #334155;}
.plan__features li::before { content: '\2713'; position: absolute; left: 0; color: #22c55e; font-weight: 700;}
.plan__cta { display: block; text-align: center; text-decoration: none; padding: 0.75rem 1rem; border-radius: 8px; font-weight: 600; background: #f1f5f9; color: #1e293b; border: 1px solid #cbd5e1; transition: background 0.2s ease, color 0.2s ease;}
.plan--featured .plan__cta { background: #6366f1; color: #ffffff; border-color: #6366f1;}
.plan__cta:hover { background: #e2e8f0;}
.plan--featured .plan__cta:hover { background: #4f46e5;}
@media (max-width: 900px) { .pricing__grid { grid-template-columns: 1fr; }
.plan--featured { transform: none; order: -1; }}
@media (min-width: 901px) and (max-width: 1100px) { .pricing__grid { grid-template-columns: repeat(2, 1fr); }
.plan--featured { grid-column: span 2; }}Sample Run
The preview above shows three evenly sized cards side by side, with the "Pro" card in the middle sitting slightly larger, wrapped in a purple border, and carrying the floating "Most Popular" badge above it. Notice how every checkmark lines up perfectly in each list thanks to the shared `padding-left`/`::before` pattern, and how all three "Choose ..." buttons sit flush with each other along the bottom edge of the cards regardless of how many features each plan lists. Narrowing the preview past 900px collapses the grid to a single column with the Pro plan promoted to the top.
Extend This Project
- Add a monthly/yearly billing toggle (using the checkbox-hack technique from the Navbar project) that swaps the displayed prices with CSS alone.
- Add a fourth plan and change `repeat(3, 1fr)` to `repeat(4, 1fr)` to see how the grid automatically adjusts.
- Give each card a subtle `transform: translateY(-4px)` and stronger shadow on `:hover` for a tactile, interactive feel.
- Add a "Save 20%" ribbon to the yearly price using the same absolutely-positioned badge technique as the featured plan.
- Experiment with `grid-template-columns: repeat(auto-fit, minmax(260px, 1fr))` instead of a fixed `repeat(3, 1fr)`, and compare how it behaves without any media queries at all.
Summary
You built a responsive, three-column pricing table using CSS Grid for the layout and careful box-model spacing for every card, then made one plan visually stand out using scale, color, shadow, and an absolutely positioned badge. The "load the grid, style one shared card class, then layer a modifier class for the exception" pattern you used here — `.plan` plus `.plan--featured` — is one of the most common and reusable structures in real-world CSS.