auto-fit vs auto-fill Explained
Master the subtle but crucial difference with live examples.
This is the capstone project for the CSS Intermediate course. We'll build a magazine-style blog layout from scratch, combining every technique from the previous eleven chapters: design tokens, Flexbox headers, Grid post grids, fluid typography, transforms on hover, keyframe animations, and BEM naming. Work through each step in order — by Step 9 you'll have a production-quality responsive layout.
Everything else references these tokens. Define them first and nothing downstream ever needs a raw value — dark mode, rebranding, or spacing tweaks become single-line changes.
:root {
/* ── Colors ── */
--clr-bg: #0d0f18;
--clr-surface: #1a1d27;
--clr-surface-2: #12141e;
--clr-border: #2a2d3a;
--clr-primary: #4f8ef7;
--clr-primary-dk: #3a7ae5;
--clr-accent: #c9a8ff;
--clr-text: #e2e4ec;
--clr-text-muted: #7a7f96;
/* ── Fluid spacing scale ── */
--sp-1: clamp(0.5rem, 1.5vw, 0.875rem);
--sp-2: clamp(1rem, 2.5vw, 1.5rem);
--sp-3: clamp(1.5rem, 4vw, 2.5rem);
--sp-4: clamp(2rem, 6vw, 4rem);
--gap: clamp(1rem, 2.5vw, 1.75rem);
/* ── Fluid type scale ── */
--text-sm: clamp(0.75rem, 1.5vw, 0.875rem);
--text-base: clamp(0.9rem, 2vw, 1rem);
--text-lg: clamp(1.1rem, 2.5vw, 1.25rem);
--text-xl: clamp(1.4rem, 4vw, 2rem);
--text-hero: clamp(1.8rem, 6vw, 3.5rem);
/* ── Layout ── */
--max-w: 1100px;
--header-h: 60px;
--sidebar-w: 280px;
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
background: var(--clr-bg);
color: var(--clr-text);
font-family: system-ui, -apple-system, sans-serif;
font-size: var(--text-base);
line-height: 1.6;
}
img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; }
/* Shared max-width container */
.container {
max-width: var(--max-w);
margin: 0 auto;
padding: 0 var(--sp-2);
}
/* Section spacing */
.section { padding: var(--sp-3) 0; }
.section__heading {
font-size: var(--text-xl);
font-weight: 700;
margin-bottom:var(--sp-2);
padding-bottom:var(--sp-1);
border-bottom:2px solid var(--clr-primary);
}
The header uses Flexbox with margin-left: auto on the nav to push it to the right — the simplest push technique. position: sticky; top: 0 keeps it visible without JavaScript.
.site-header {
position: sticky;
top: 0;
z-index: 100;
height: var(--header-h);
background: var(--clr-surface-2);
border-bottom:1px solid var(--clr-border);
display: flex;
align-items: center;
}
.site-header__inner {
display: flex;
align-items: center;
gap: var(--sp-2);
width: 100%;
}
.site-header__logo {
font-size: var(--text-lg);
font-weight: 800;
color: var(--clr-primary);
}
.site-header__nav {
display: flex;
align-items: center;
gap: var(--sp-2);
margin-left: auto; /* pushes nav to right edge */
}
.site-header__link { color: var(--clr-text-muted); font-size: var(--text-sm); transition: color 0.2s; }
.site-header__link:hover{ color: var(--clr-text); }
.site-header__link--cta{
background: var(--clr-primary);
color: #fff;
padding: 6px 16px;
border-radius:4px;
font-weight: 600;
transition: background 0.2s;
}
.site-header__link--cta:hover { background: var(--clr-primary-dk); }
@media (max-width: 640px) {
.site-header__nav { display: none; } /* add hamburger menu for full build */
}
.site-hero {
background: linear-gradient(135deg, var(--clr-bg) 0%, #1a2a4a 100%);
padding: var(--sp-4) var(--sp-2);
text-align: center;
}
.site-hero__inner {
max-width: 680px;
margin: 0 auto;
}
.site-hero__label {
font-size: var(--text-sm);
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--clr-primary);
margin-bottom: var(--sp-1);
}
.site-hero__heading {
font-size: var(--text-hero); /* clamp(1.8rem, 6vw, 3.5rem) */
font-weight: 800;
color: #fff;
line-height: 1.15;
margin-bottom:var(--sp-1);
}
.site-hero__sub {
font-size: var(--text-base);
color: var(--clr-text-muted);
margin-bottom:var(--sp-2);
}
.site-hero__actions {
display: flex;
gap: var(--sp-1);
justify-content: center;
flex-wrap: wrap; /* stack on very narrow screens */
}
/* Shared button styles (BEM block) */
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 22px;
border-radius:5px;
font-weight: 600;
font-size: var(--text-sm);
border: 2px solid transparent;
cursor: pointer;
transition: background 0.2s, transform 0.15s;
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }
.btn--primary { background: var(--clr-primary); color: #fff; }
.btn--primary:hover{ background: var(--clr-primary-dk); }
.btn--ghost { border-color: var(--clr-border); color: var(--clr-text-muted); }
.btn--ghost:hover { border-color: var(--clr-primary); color: var(--clr-text); }
The key trick: minmax(min(280px, 100%), 1fr). The inner min() clamps the minimum column width to 100% when the viewport is narrower than 280px — preventing overflow on tiny screens without a media query.
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
gap: var(--gap);
}
/* BEM block */
.post-card {
background: var(--clr-surface);
border: 1px solid var(--clr-border);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction:column; /* column so body can grow */
transition: transform 0.25s, box-shadow 0.25s;
}
.post-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 32px rgba(0,0,0,.3);
}
.post-card__image {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.post-card__image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s;
}
.post-card:hover .post-card__image img {
transform: scale(1.05); /* subtle zoom on hover */
}
.post-card__body { padding: var(--sp-2); flex: 1; } /* flex:1 pushes footer down */
.post-card__tag {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--clr-primary);
margin-bottom: 6px;
}
.post-card__title { font-size: var(--text-lg); font-weight: 700; margin-bottom:6px; line-height:1.3; }
.post-card__excerpt{ font-size: var(--text-sm); color: var(--clr-text-muted); line-height:1.55; }
.post-card__footer {
padding: var(--sp-1) var(--sp-2);
border-top: 1px solid var(--clr-border);
display: flex;
justify-content:space-between;
align-items: center;
font-size: var(--text-sm);
color: var(--clr-text-muted);
}
align-items: start prevents the sidebar from stretching to match the main content height — it stays at its own natural height. Without it, the sidebar would fill the entire grid row.
.content-area {
display: grid;
grid-template-columns: 1fr var(--sidebar-w); /* main grows, sidebar fixed */
gap: var(--gap);
align-items: start; /* sidebar stays own height */
}
/* Sidebar is sticky so it stays visible while scrolling main */
.content-area__sidebar {
position: sticky;
top: calc(var(--header-h) + var(--sp-2)); /* below sticky header */
}
@media (max-width: 768px) {
.content-area {
grid-template-columns: 1fr; /* one column on mobile */
}
.content-area__sidebar {
position: static; /* un-sticky on small screens */
}
}
.widget {
background: var(--clr-surface);
border: 1px solid var(--clr-border);
border-radius: 8px;
overflow: hidden;
margin-bottom: var(--sp-2);
}
.widget__heading {
font-size: var(--text-sm);
font-weight: 700;
text-transform:uppercase;
letter-spacing:0.07em;
padding: var(--sp-1) var(--sp-2);
border-bottom:1px solid var(--clr-border);
color: var(--clr-text);
}
.widget__body {
padding: var(--sp-1) var(--sp-2);
}
.widget__item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid var(--clr-border);
font-size: var(--text-sm);
color: var(--clr-text-muted);
}
.widget__item:last-child { border-bottom: none; }
.widget__count {
background: var(--clr-surface-2);
color: var(--clr-accent);
padding: 1px 7px;
border-radius: 10px;
font-size: 0.75em;
font-weight: 600;
}
/* Tag cloud widget variant */
.widget--tags .widget__body {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.widget__tag {
background: var(--clr-surface-2);
color: var(--clr-accent);
padding: 3px 10px;
border-radius: 12px;
font-size: var(--text-sm);
transition: background 0.2s;
}
.widget__tag:hover { background: var(--clr-primary); color: #fff; }
.site-footer {
background: var(--clr-surface-2);
border-top: 1px solid var(--clr-border);
padding: var(--sp-3) var(--sp-2);
}
.footer__grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr; /* brand col is wider */
gap: var(--gap);
}
/* Collapses to 2 columns on tablet */
@media (max-width: 768px) {
.footer__grid { grid-template-columns: 1fr 1fr; }
}
/* Single column on mobile */
@media (max-width: 480px) {
.footer__grid { grid-template-columns: 1fr; }
}
/* Footer bottom bar */
.footer__bottom {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--sp-1);
margin-top: var(--sp-3);
padding-top: var(--sp-2);
border-top: 1px solid var(--clr-border);
font-size: var(--text-sm);
color: var(--clr-text-muted);
}
| Breakpoint | What changes | Technique |
|---|---|---|
| ≥ 769px (Desktop) | Full layout: header nav visible, 3-col grid, 2-col content, 4-col footer | Default styles |
| ≤ 768px (Tablet) | Content area collapses to 1 col; sidebar un-stickies; footer becomes 2-col | @media (max-width: 768px) |
| ≤ 640px | Header nav hides (hamburger shown); hero buttons stack | @media (max-width: 640px) |
| ≤ 480px | Footer becomes 1 column | @media (max-width: 480px) |
| Any width | Post card grid reflows automatically | auto-fit minmax(min(280px,100%), 1fr) — no breakpoint needed |
| Any width | Type and spacing scale fluidly | clamp() on all spacing + font-size tokens |
auto-fit, clamp(), flex-wrap — not from media queries. Only structural changes (collapsing the sidebar, hiding the nav) need breakpoints. Favour intrinsic techniques; add breakpoints only when structure must change.
CSS Intermediate · Chapter 12
Grid, Flexbox, fluid type, and BEM — all working together.
Master the subtle but crucial difference with live examples.
Why your sidebar shouldn't stretch to fill its grid cell.
One property replaces a dozen breakpoints.
container-type: inline-size) on the frame div — the layout responds to the frame's width, not the viewport width. This is exactly how container queries will work in your real layouts (Chapter 2 of CSS Advanced).| Section | Tool chosen | Why not the alternative |
|---|---|---|
| Header layout | Flexbox | One-dimensional row — Grid would work but adds no value here |
| Hero buttons | Flexbox + flex-wrap | Inline-block would need media queries to stack; flex-wrap handles it automatically |
| Featured cards | Grid auto-fit | Flexbox can't guarantee equal-width columns without media queries; auto-fit does it in one line |
| Main + Sidebar | Grid (explicit tracks) | Flexbox can't hold the sidebar at a fixed 280px while the main grows — Grid 1fr var(--sidebar-w) does exactly this |
| Cards (internal) | Flexbox column | One-dimensional vertical stack; flex: 1 on body pushes footer to bottom — Grid overkill here |
| Footer | Grid (explicit tracks) | Four columns with unequal widths (brand 2fr, rest 1fr) — Grid template columns are cleaner than flex |
| Spacing | clamp() tokens | Fixed values need breakpoints for every size; clamp scales continuously |
| Typography | clamp() tokens | Same — no extra media queries needed for text sizing |
max-height transition..is-loading modifier to cards that shows a shimmer animation (from Ch 10 Exercise 1) while content loads.data-theme="light" attribute on <html>, and remap the color tokens for light mode.IntersectionObserver to add a .revealed class to cards as they enter the viewport, triggering a translateY(0) + opacity: 1 animation.1px bar at the top of the header that grows with scaleX() as the user scrolls, driven by scroll event and transform.calc() top offset. The sidebar should stick below the header without overlapping it. The header is 60px tall and you want 16px of breathing room. Write the CSS, and explain why top: calc(var(--header-h) + 16px) is better than top: 76px.--header-h: 48px), the calc value auto-updates. A hard-coded 76px would need a second override in that media query.:root {
--header-h: 60px;
--sidebar-gap: 16px;
}
.content-area__sidebar {
position: sticky;
top: calc(var(--header-h) + var(--sidebar-gap));
/* = 76px by default, but auto-updates if --header-h changes */
}
@media (max-width: 640px) {
:root { --header-h: 48px; }
/* sidebar top now becomes 64px — no second calc() needed */
}
@media (max-width: 768px) {
.content-area__sidebar { position: static; }
/* un-stick when layout collapses to 1 column */
}
.post-card--featured modifier that makes one card span all columns in the grid and display horizontally (image left, content right) rather than the default vertical stack. Use grid-column: 1 / -1 to span all columns, and switch the card to a row-direction Flexbox layout internally.flex-direction: column. Override it to row in the modifier. You'll also need to constrain the image width (flex: 0 0 40%) so it doesn't take all available space..post-card--featured {
grid-column: 1 / -1; /* span all columns */
flex-direction: row;
}
.post-card--featured .post-card__image {
flex: 0 0 40%; /* fixed width, no shrink/grow */
aspect-ratio:auto; /* remove the 16/9 ratio — let height fill */
min-height: 220px;
}
.post-card--featured .post-card__body {
padding: var(--sp-3);
}
/* On mobile, revert to stacked layout */
@media (max-width: 480px) {
.post-card--featured {
flex-direction: column;
}
.post-card--featured .post-card__image {
flex: none;
aspect-ratio:16 / 9;
}
}
opacity: 0; transform: translateY(24px) to its natural position. Use IntersectionObserver in JavaScript to add a .revealed class when each card becomes visible. Include a prefers-reduced-motion CSS override that skips the animation entirely..post-card (opacity 0, translateY), then style .post-card.revealed to restore them. The IntersectionObserver callback adds .revealed when the threshold is crossed. Use observer.unobserve(entry.target) so the animation only fires once..post-card {
opacity: 0;
transform: translateY(24px);
transition:opacity 0.4s ease, transform 0.4s ease;
}
.post-card.revealed {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.post-card { opacity: 1; transform: none; transition: none; }
.post-card.revealed{ transform: none; }
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
observer.unobserve(entry.target); // fire once only
}
});
}, { threshold: 0.15 });
document.querySelectorAll('.post-card')
.forEach((card) => observer.observe(card));
:root custom properties — one for dark (default) and one for [data-theme="light"] on <html>. The toggle button switches the attribute. Requirements: (a) only the semantic tokens change between themes (--clr-bg, --clr-surface, --clr-text, etc.) — the primitive palette (raw hex values) stays unchanged; (b) all transitions in the layout should run smoothly when the theme switches; (c) the chosen theme should persist in localStorage and be restored on page load.transition: background 0.3s, color 0.3s, border-color 0.3s to *, *::before, *::after in the base reset (or on the key layout elements). Use localStorage.setItem('theme', 'light') and document.documentElement.dataset.theme./* Semantic tokens — dark mode (default) */
:root {
--clr-bg: #0d0f18;
--clr-surface: #1a1d27;
--clr-surface-2: #12141e;
--clr-border: #2a2d3a;
--clr-text: #e2e4ec;
--clr-text-muted: #7a7f96;
}
/* Light theme — re-map semantics only */
[data-theme="light"] {
--clr-bg: #f8f9fc;
--clr-surface: #ffffff;
--clr-surface-2: #f0f1f5;
--clr-border: #dde0ea;
--clr-text: #111827;
--clr-text-muted: #6b7280;
}
/* Smooth transition on theme switch */
*, *::before, *::after {
transition: background-color 0.3s, color 0.3s, border-color 0.3s;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition: none; }
}
// Restore on load
const saved = localStorage.getItem('theme');
if (saved) document.documentElement.dataset.theme = saved;
// Toggle button click handler
document.getElementById('themeToggle')
.addEventListener('click', () => {
const next = document.documentElement.dataset.theme === 'light'
? 'dark' : 'light';
document.documentElement.dataset.theme = next;
localStorage.setItem('theme', next);
});
@layer, container queries, subgrid, :has(), scroll-driven animations, and building a complete design system.