Your layouts are only as reliable as your understanding of the CSS box model. In this project, you will build a compact product card with a circular media thumb, stacked text, pills, and a tooltip with a pointer. Along the way, you will learn how content, padding, border, and margin interact; how box-sizing changes the math; how to debug with outlines; and how to avoid layout shifts when borders change. By the end, you will know how to reason about edges and spacing with confidence.
Why the CSS Box Model Matters
The box model governs how every element takes up space. Widths that overflow, buttons that shrink when bordered, and stacks that collapse in odd ways all trace back to this model. When you understand which layer adds to total size, you can predict results before you write a line of CSS. This knowledge scales from simple shapes to complex components. For example, triangle pointers are just borders of a zero-sized box, and round avatars are a box with equal width and height plus a 50% radius. The facts are simple: get the math right, and components become stable, themeable, and reusable.
Prerequisites
You do not need a framework here. You only need a small set of fundamentals:
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
We will mark up a card with a media area, content, pills, and a button with a tooltip. The structure is minimal on purpose so that the CSS box model remains clear. The wrapper provides an easy way to center the demo and test margins around the card.
<!-- HTML -->
<main class="wrap">
<article class="card" aria-labelledby="card-title">
<div class="card__media" aria-hidden="true"></div>
<div class="card__body">
<h2 id="card-title" class="card__title">CSS Box Model Card</h2>
<p class="card__excerpt">Learn how padding, border, and margin build a resilient layout without surprises.</p>
<ul class="card__tags">
<li class="tag">box-sizing</li>
<li class="tag">padding</li>
<li class="tag">border</li>
<li class="tag">margin</li>
</ul>
<div class="card__actions">
<button type="button" class="btn" aria-describedby="tip-1">Hover for tip</button>
<span role="tooltip" id="tip-1" class="tooltip">
Borders add to size unless you use border-box.
</span>
</div>
</div>
</article>
</main>
Step 2: The Basic CSS & Styling
We will establish a consistent box model across the page, set up theme variables, and style the card wrapper. Applying border-box on all elements is the single best way to make widths intuitive. The card gets a base width, padding, border, and gap to show how the layers stack.
/* CSS */
:root {
--bg: #0f172a;
--surface: #111827;
--text: #e5e7eb;
--muted: #9ca3af;
--brand: #60a5fa;
--accent: #f59e0b;
--radius: 12px;
--space-1: 0.5rem;
--space-2: 1rem;
--space-3: 1.5rem;
--space-4: 2rem;
--border: 2px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
line-height: 1.5;
background: linear-gradient(135deg, #0b1020, #0f172a 50%, #10131b);
color: var(--text);
}
.wrap {
min-height: 100%;
display: grid;
place-items: center;
padding: var(--space-4);
}
.card {
width: 340px;
background: var(--surface);
border: var(--border) solid #1f2937;
border-radius: var(--radius);
padding: var(--space-3);
box-shadow: 0 10px 30px rgba(0,0,0,0.35);
}
.card__media {
width: 88px;
height: 88px;
background: linear-gradient(135deg, #93c5fd, #3b82f6);
border-radius: 50%;
border: 4px solid #0b1020;
outline: 2px dashed rgba(255,255,255,0.2);
outline-offset: 2px;
}
.card__body {
margin-top: var(--space-3);
}
.card__title {
font-size: 1.25rem;
margin: 0;
}
.card__excerpt {
margin: var(--space-1) 0 0 0;
color: var(--muted);
}
.card__tags {
list-style: none;
padding: 0;
margin: var(--space-2) 0 0 0;
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
}
.tag {
font-size: 0.875rem;
padding: 0.25rem 0.5rem;
border: 1px solid #374151;
border-radius: 9999px;
background: #0b1220;
}
.card__actions {
margin-top: var(--space-3);
position: relative;
display: inline-block;
}
.btn {
padding: 0.5rem 0.875rem;
background: var(--brand);
border: 2px solid #1e3a8a;
color: #071022;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
.tooltip {
position: absolute;
left: 0;
top: calc(100% + 10px);
display: block;
white-space: nowrap;
padding: 0.5rem 0.75rem;
border: 2px solid #334155;
background: #0d1629;
border-radius: 8px;
color: var(--text);
transform: translateY(6px);
opacity: 0;
pointer-events: none;
transition: transform 160ms ease, opacity 160ms ease;
}
.btn:hover + .tooltip,
.btn:focus + .tooltip {
opacity: 1;
transform: translateY(0);
}
Advanced Tip: Use CSS variables on spacing, colors, and border width to measure changes quickly. You can switch to a denser theme or a thicker border by editing a single token, and the box math remains predictable.
Step 3: Building the Content Box and Padding
This step focuses on the content box and padding. The card uses padding that keeps text away from its border. The media circle uses equal width and height with a 50% radius, then a thicker border to highlight how border adds to total size. We will isolate card internals with a simple vertical rhythm technique that builds consistent gaps using margins.
/* CSS */
.card__body > * + * {
margin-top: var(--space-2);
}
.card__media {
/* Already defined above; here we add a subtle box-shadow to show outer edge */
box-shadow: 0 0 0 6px rgba(59, 130, 246, 0.15);
}
/* Vertical spacing alternative: a utility stack you can reuse */
.stack > * + * {
margin-top: var(--space-2);
}
How This Works (Code Breakdown)
The selector .card__body > * + * adds a margin-top to every element that follows another element. This pattern keeps spacing inside the card consistent without padding each item. The margin lives on children, not the parent, so the card maintains a single padding layer against its border.
The .card__media block uses equal width and height, and border-radius: 50% to create a circle. If you want a refresher on this shape technique, see the guide on how to make a circle with CSS. The key point for the box model: the 4px border adds to the outside of the media’s content box. Since we set box-sizing: border-box globally, declared sizes include padding and border, which prevents the circle from growing beyond the assigned width. Without border-box, the element’s rendered size would be width + padding + border.
The optional .stack utility shows a reusable way to manage vertical spacing anywhere in your app. It avoids double spacing by applying margin only between siblings. You can attach .stack to .card__body, or any wrapper that needs predictable content gaps.
Step 4: Borders, Margins, and a Pointer with Zero-Sized Boxes
Now we will build the tooltip pointer with a pseudo-element that has zero width and height. The triangle is simply border geometry. This is a classic box model trick: you render only the border by making the content box zero. We will also add a hover state that thickens the button border without changing its final size, thanks to border-box.
/* CSS */
/* Tooltip arrow using borders on a zero-sized box */
.tooltip::after {
content: "";
position: absolute;
left: 16px;
top: -8px; /* move above the tooltip box */
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid #334155; /* border color matches tooltip border */
/* create an inner arrow to match background, layered via another pseudo */
}
.tooltip::before {
content: "";
position: absolute;
left: 16px;
top: -6px; /* slightly lower to sit inside the border arrow */
width: 0;
height: 0;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #0d1629; /* tooltip background */
}
/* Button border interaction without layout shift */
.btn:hover,
.btn:focus {
border-width: 3px;
}
/* Debugging utility to visualize every box layer at once */
.debug-box {
background: rgba(96,165,250,0.08); /* content fill */
padding: 12px; /* padding layer */
border: 3px solid #93c5fd; /* border layer */
outline: 2px dashed #f59e0b; /* outline does not affect layout */
outline-offset: 6px; /* offset to reveal separation */
margin: 16px; /* margin separates from siblings */
}
How This Works (Code Breakdown)
The tooltip arrow uses zero width and height, which means only borders render. Two triangles stack to fake a border with an inner fill. The ::after triangle forms the outer border, and ::before sits slightly lower to match the tooltip background. If you want a full reference on the shape technique, review the article on how to make a triangle down with CSS. The same border trick powers arrows in many components, and it is a pure box model move.
Border changes on the button rely on box-sizing: border-box. When the border width grows from 2px to 3px, the button’s content box shrinks by 1px per side, so the total outer size remains constant. This prevents layout shift. If you used content-box sizing, the button would grow and push siblings around.
The .debug-box utility is a teaching aid. The outline is outside the element’s total size and does not impact layout. The border does impact layout. The padding increases the space between content and border. The margin separates the element from neighbors. With this one class, you can wrap any element and see each layer in isolation.
Advanced Techniques: Animations, Logical Properties, and Flow Roots
Animations often break layouts when they change width, height, padding, or border. You can animate visual properties that do not trigger reflow. Here we will animate outline and background on focus for a high-contrast state, and we will demonstrate a flow root to contain floats and avoid margin surprises in more complex layouts.
/* CSS */
/* High-contrast focus without affecting layout */
.btn:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
background: #7fb1ff;
}
/* Logical spacing for better internationalization */
.card {
padding-block: var(--space-3);
padding-inline: var(--space-3);
}
/* Flow root to contain descendants and prevent margin collapse with children */
.card__body {
display: flow-root; /* establishes a new block formatting context */
}
/* Respect motion preferences for tooltip */
@media (prefers-reduced-motion: reduce) {
.tooltip {
transition: none;
}
}
Using outline for focus emphasizes interactivity without affecting layout because outline sits outside the box. Logical properties like padding-inline and padding-block align spacing with writing modes, which keeps your math correct in vertical or right-to-left layouts. The flow-root on .card__body creates a new block formatting context that contains floats and prevents certain margin interactions from leaking out of the container. You will not always need it, but it is a reliable tool when you notice odd margin behavior with descendants.
Accessibility & Performance
Box model choices can help or hurt usability. Sizing, padding, and outlines affect readability, hit areas, and motion comfort. Performance-wise, some properties trigger layout, paint, or compositing, and that changes how smooth your UI feels.
Accessibility
Increase the button’s hit area using padding rather than scaling with transforms. The tooltip is supplementary, so keep it accessible: the button has aria-describedby and the tooltip uses role="tooltip" with a clear id. When the pointer is purely decorative, mark any extra shapes aria-hidden="true" if they are not tied to the tooltip text. Respect users who prefer less movement with the prefers-reduced-motion query. Colors and outlines need adequate contrast so the boundary between border and background is visible for low-vision users.
Performance
Padding, border, and margin changes can trigger layout. Frequent size animations in large lists produce jank. Favor opacity or transform for hover flourishes. Outline and box-shadow are paint-only and can be cheaper than layout-affecting changes. The triangle pointer is border-only on an empty box, which is quick to draw. Keep shadows modest and avoid animating shadow blur. The global border-box setting reduces reflow churn during responsive adjustments because components preserve their computed outer sizes as borders change.
Edges That Behave: What You Built
You built a card that demonstrates each layer of the CSS box model in a real component: content spaced by padding, a defined border that does not cause layout shifts, margins that create clean rhythm, and a tooltip pointer powered by borders. You also learned techniques for debugging and for keeping motion and accessibility in balance. With these patterns, you can reason about spacing in any component and extend the approach to shape-driven UI, from pointers based on triangles to circular avatars, such as those covered in the triangle right and circle guides. Now you have the mental model to build precise interfaces that hold their shape across themes and content changes.