Compare viewport-driven type with a constrained article component as the same headline moves through narrow and wide slots.
Live demo
Responsive typography lab
HTML used in this demo
<main class="type-lab" data-type-lab data-copy="short" data-rhythm="relaxed">
<header class="lab-header">
<p class="eyebrow">Responsive typography</p>
<h1>Fluid type still needs a layout boundary.</h1>
<p>The left card follows the viewport. The right card responds to the space its article actually receives.</p>
</header>
<form class="control-panel" aria-label="Typography test controls">
<label>
<span>Article slot <strong data-slot-label>620px</strong></span>
<input type="range" name="slot" min="320" max="840" value="620" />
</label>
<label>
<span>Measure cap <strong data-measure-label>62ch</strong></span>
<input type="range" name="measure" min="44" max="76" value="62" />
</label>
<label>
<span>Headline copy</span>
<select name="copy">
<option value="short">Short production heading</option>
<option value="long">Long editorial heading</option>
</select>
</label>
<label>
<span>Text rhythm</span>
<select name="rhythm">
<option value="relaxed">Relaxed reading</option>
<option value="compact">Compact dashboard</option>
</select>
</label>
</form>
<section class="type-stage" data-stage aria-label="Typography comparison">
<article class="sample sample--viewport">
<p class="eyebrow">Viewport sized</p>
<h2 data-headline>Readable CSS starts with constraints</h2>
<p>The type scales with the browser window, even when this article is placed in a narrow component slot.</p>
<a href="#preview">Read article</a>
</article>
<article class="sample sample--component">
<p class="eyebrow">Component constrained</p>
<h2 data-headline>Readable CSS starts with constraints</h2>
<p>The article owns its measure, heading range, and rhythm, so the same content can survive different layout slots.</p>
<a href="#preview">Read article</a>
</article>
</section>
<aside class="diagnosis" aria-live="polite">
<p class="eyebrow">Reading the result</p>
<h2 data-verdict>The constrained card keeps the hierarchy usable.</h2>
<p data-explain>Its heading responds to the article container and the measure cap protects body text from stretching too far.</p>
</aside>
</main>
const lab = document.querySelector('[data-type-lab]');
const form = lab.querySelector('form');
const stage = lab.querySelector('[data-stage]');
const slotLabel = lab.querySelector('[data-slot-label]');
const measureLabel = lab.querySelector('[data-measure-label]');
const headlines = Array.from(lab.querySelectorAll('[data-headline]'));
const verdict = lab.querySelector('[data-verdict]');
const explain = lab.querySelector('[data-explain]');
const copy = {
short: 'Readable CSS starts with constraints',
long: 'Readable CSS starts with constraints that survive narrow cards, long words, and real editorial headlines',
};
function render() {
const data = new FormData(form);
const slot = Number(data.get('slot') || 620);
const measure = Number(data.get('measure') || 62);
const copyMode = data.get('copy') || 'short';
const rhythm = data.get('rhythm') || 'relaxed';
lab.dataset.copy = copyMode;
lab.dataset.rhythm = rhythm;
stage.style.setProperty('--slot-width', slot + 'px');
stage.style.setProperty('--measure', measure + 'ch');
slotLabel.textContent = slot + 'px';
measureLabel.textContent = measure + 'ch';
headlines.forEach((headline) => {
headline.textContent = copy[copyMode];
});
if (slot < 430 && copyMode === 'long') {
verdict.textContent = 'The viewport-sized card breaks down first.';
explain.textContent = 'The heading is still tied to the browser, so the narrow article slot gets oversized type and aggressive wrapping.';
return;
}
if (measure > 68 && slot > 700) {
verdict.textContent = 'The measure cap is now the risk.';
explain.textContent = 'Wide text can look calm while becoming harder to scan. A fluid system still needs a readable maximum.';
return;
}
if (rhythm === 'compact') {
verdict.textContent = 'Compact rhythm only works when the content is short.';
explain.textContent = 'Reduced line height can suit dense UI, but long-form copy needs more breathing room to stay readable.';
return;
}
verdict.textContent = 'The constrained card keeps the hierarchy usable.';
explain.textContent = 'Its heading responds to the article container and the measure cap protects body text from stretching too far.';
}
form.addEventListener('input', render);
render();
What this demo is for
Use this demo when typography looks fluid but still fails because heading scale, measure, and line height are not owned by the component.
Try this
Set the slot width near 340px with the long headline and compare the two cards.
Increase the measure cap and watch when readable line length becomes less important than the surrounding layout slot.
Switch between relaxed and compact rhythm to see why line height is part of the system, not a decorative tweak.