Profile picture

Arkadiusz Kulpa

AI & ML Engineer

HomeAboutBlogChat
Profile picture

Arkadiusz Kulpa

AI & ML Engineer

HomeAboutBlogChat

Taming Responsive Layout with CSS Design Tokens and Fluid Scaling

2026-02-255 min read
cssresponsive-designdesign-tokensnextjsarchitecturedevlog

Taming Responsive Layout with CSS Design Tokens and Fluid Scaling

Something was wrong with the blog page. As I slowly narrowed the browser window, the post cards would shrink, then suddenly grow wider at a certain breakpoint, then shrink again. It was subtle, but once I noticed it, I could not unsee it.

This "grow then shrink" bug turned out to be a symptom of a deeper problem: the CSS architecture was a patchwork of ad-hoc media queries with hardcoded values that conflicted with each other. Fixing the bug properly meant rethinking the entire approach to responsive layout.

The Root Causes

Investigating with browser DevTools revealed four interacting problems:

  1. Fixed padding jumps. .layout__main used padding-left: 60px at desktop and padding-left: 40px at 401px. This meant available content width increased at 401px — a narrower viewport giving more space to content.

  2. Hardcoded max-widths. .home__posts had max-width: 800px hardcoded in the CSS. As padding decreased at breakpoints, the cards would hit this ceiling and stop growing, while the surrounding space changed.

  3. Competing container systems. Three different container classes (.container, .content-container, .post-container) each defined their own widths and padding, often contradicting each other at the same breakpoints.

  4. Design tokens existed but were underused. The 01-tokens.css file already had CSS custom properties for spacing and sizing, but the layout CSS was not using them. The tokens were a good idea, poorly adopted.

The Fix: A Design Token System

Rather than patching individual breakpoints, I built a proper token-based system. The key insight was replacing discrete media query jumps with continuous scaling using CSS clamp().

Fluid Spacing Tokens

/* 01-tokens.css */
:root {
  --content-width-prose: 50rem;      /* 800px — optimal for reading */
  --content-width-standard: 56.25rem; /* 900px — standard content */
  --content-width-wide: 68.75rem;    /* 1100px — wide layouts */
  --content-width-full: 90rem;       /* 1440px — maximum */

  --container-padding-x: clamp(1rem, 4vw, 3rem);
  --layout-main-padding-x: clamp(1rem, 4vw, 4rem);
}

clamp() takes three values: a minimum, a preferred value (usually viewport-relative), and a maximum. As the viewport changes, the value scales smoothly between the min and max. No breakpoints, no jumps.

For example, clamp(1rem, 4vw, 4rem) means:

  • On a 320px screen: 4vw = 12.8px, but the minimum is 1rem (16px), so you get 16px
  • On a 1000px screen: 4vw = 40px, which is between 16px and 64px, so you get 40px
  • On a 1920px screen: 4vw = 76.8px, but the maximum is 4rem (64px), so you get 64px

Smooth, proportional, no surprises.

Semantic Container Variants

Instead of three competing container classes, I created semantic variants of a single .container base:

.container--prose    { max-width: var(--content-width-prose); }
.container--standard { max-width: var(--content-width-standard); }
.container--wide     { max-width: var(--content-width-wide); }
.container--full     { max-width: var(--content-width-full); }

Now a page's layout intent is clear from the HTML:

<main class="layout__main">
  <div class="container container--prose">
    <article class="card">...</article>
  </div>
</main>

You can see at a glance that this page uses the "prose" width (800px max) — optimised for reading long-form content.

Fixing the Layout Main

The critical fix was replacing the hardcoded padding on .layout__main with the fluid token:

/* Before — causes grow/shrink */
.layout__main {
  padding-left: 60px;
  padding-right: 60px;
}
@media (min-width: 401px) {
  .layout__main {
    padding-left: 40px;  /* Narrower viewport, LESS padding = MORE content width */
    padding-right: 40px;
  }
}

/* After — smooth scaling */
.layout__main {
  padding: var(--layout-main-padding-y) var(--layout-main-padding-x);
}

One line replacing two conflicting media query blocks. The padding now scales smoothly from 16px to 64px as the viewport grows. Content width changes proportionally, never jumping.

Removing Conflicting Overrides

The 07-responsive.css file had a @media (min-width: 401px) block that overrode .layout__main padding with fixed values. This was the primary cause of the grow/shrink behaviour. With fluid padding in place, the entire block was deleted — it was not just unnecessary, it was actively harmful.

Before and After

Before: Resize the browser from 1920px to 320px and watch the blog cards shrink, then grow wider at ~400px, then shrink again on mobile. Jarring and unprofessional.

After: Cards smoothly and continuously shrink as the viewport narrows. Padding reduces proportionally. No jumps, no surprises. The reading experience feels natural at every width.

At wide screens (1440px+), blog cards sit at a comfortable 800-900px width with generous whitespace. At tablet (768-1024px), they fill 70-80% of the viewport. On mobile (320-768px), they use 90-100% of available width with minimal padding.

What I Learned

clamp() eliminates most responsive breakpoints. The majority of padding, margin, and font-size declarations that traditionally need media queries can be replaced with a single clamp() value. Fewer breakpoints means fewer edge cases and fewer conflicts.

Design tokens are only useful if you actually use them. Having tokens defined in a CSS file is not enough. Every layout declaration needs to reference them. The moment a developer hardcodes 800px instead of using var(--content-width-prose), the system starts to fragment.

Semantic class names communicate intent. When a future developer (or future me) sees .container--prose, they know immediately what width to expect. When they see max-width: 800px, they have no idea why that number was chosen.

What Came Next

The layout system was solid, but the blog post reading experience still had rough edges. The table of contents sidebar appeared too early on tablets, and clicking a TOC link scrolled the heading behind the fixed navbar. Small issues, but they undermined the polish.

That story is in Fixing the Blog Reading Experience: TOC Sidebar and Scroll Behaviour.


This post is part of a series documenting the development of arkadiuszkulpa.co.uk.

← Back to Blog