Code that survives the content changing
Your page works. You built it with "Sharma Kirana", a two-line description and four products. Then the shop is called "Shree Ganesh Provision Stores and General Merchants", the description runs to six lines, one product has no photograph, and somebody adds a twenty-third item.
This lesson is about the page still working. It is the difference between a demo and something you can hand over.
The content will not be what you used
You designed with content you invented, which means you designed with content that fits. Real content does not.
The eight cases worth testing deliberately, every time:
- A much longer name or title. Four words where you assumed two.
- A much shorter one. One word, where your layout assumed a two-line heading.
- Nothing at all. An empty description, a missing price, a product with no photo.
- Far more items. Twenty-three cards where you built three.
- One item. A grid of one looks wrong if you never checked.
- No items. The empty state, which is almost always forgotten.
- An unbroken string. A URL, an email address, a long product code.
- A different language or script. Devanagari is taller than Latin and changes line heights; German compound words are long.
Make yourself a test fixture with all eight and load it. Ten minutes, and it finds everything.
The specific failures, and their fixes
Long text overflowing
.card__title {
overflow-wrap: break-word; /* break a long word rather than overflow */
hyphens: auto; /* with lang set, break at sensible points */
}
overflow-wrap: break-word is the one from the module 2 reset. It only breaks when a word
genuinely cannot fit, which is what you want — unlike word-break: break-all, which breaks
mid-word constantly and is almost never right.
For a single line that must not wrap:
.card__title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
All three are required — text-overflow does nothing without overflow: hidden and
nowrap, which is why it "does not work" for most people the first time.
For a clamp at several lines:
.card__desc {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
Ugly prefixed syntax, works everywhere, and there is no better answer yet.
But think before truncating. An ellipsis hides information, and on a product name that may be the part the customer needed. Wrapping to two lines is usually better than cutting. Truncate when the layout genuinely cannot give more room, not as a default.
Flex and grid items that will not shrink
From modules 3 and 4, and this is where they bite in practice:
.row { display: flex; gap: 1rem; }
.row__text { flex: 1; min-width: 0; } /* without min-width:0 it overflows */
.grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } /* not just 1fr */
A long unbroken string in a flex item or a 1fr grid track pushes the whole layout wider,
because the default minimum size is the content's minimum. min-width: 0 and
minmax(0, 1fr) are the fixes, and they belong in your defaults rather than being remembered
each time.
Images that are not there, or are the wrong shape
.card__image {
width: 100%;
aspect-ratio: 3 / 2;
object-fit: cover;
background: var(--colour-surface); /* visible if the image fails */
}
aspect-ratio plus object-fit: cover means every image occupies the same space whatever its
real proportions, so titles line up — module 3's point. The background is what shows if the
file 404s, instead of a broken-image icon on a transparent gap.
And a placeholder for a genuinely missing image, which is better than an empty box:
<img class="card__image" src="images/atta.jpg" alt="A 10kg bag of atta"
width="600" height="400" onerror="this.src='images/placeholder.svg'">
That is the one line of JavaScript this course suggests, and it is optional.
The empty state
The most commonly forgotten case, and :has() from module 6 handles it with no JavaScript:
<ul class="cards" id="results"></ul>
<p class="empty" hidden>No products match. Try a different area.</p>
.cards:empty { display: none; }
.cards:empty + .empty { display: block; }
Or with :has() on the container:
.results:not(:has(.card)) .empty { display: block; }
An empty state should say what happened and what to do next. "No results" is a dead end; "No products match. Try a different area." is not.
Note :empty is strict in one direction and not the other: an element containing a single
space is not empty, which catches people, while an element containing only an HTML comment
is. So whitespace from your template's indentation will silently stop :empty matching.
One item, and too many items
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1.5rem;
}
auto-fit from module 3 handles both ends: one card stretches to fill rather than sitting in
a third of the row, and twenty-three wrap without a media query. auto-fill would leave the
single card at column width — which is sometimes what you want, and is a decision to make
rather than a default to inherit.
For a genuinely different treatment at low counts, :has() does quantity queries:
.cards:not(:has(> :nth-child(2))) .card { max-width: 24rem; } /* exactly one */
Zoom, and the 400% requirement
WCAG asks that content reflows at 320px equivalent, which is what 400% zoom on a 1280px window produces. That is not an edge case — it is how many people with low vision browse.
What breaks at 400%:
Fixed pixel widths. A width: 400px sidebar at 400% zoom is 1600 effective pixels of a
1280 window. Use max-width and rem.
Fixed heights on text containers. Text at 4× needs four times the vertical space; a
height: 200px box clips it. min-height, always.
Sticky headers. A header that is 15% of the viewport at 100% zoom can be 60% at 400%, leaving almost no content visible. Consider un-sticking it at small heights:
@media (min-height: 30rem) {
.site-header { position: sticky; top: 0; }
}
Anything positioned absolutely at pixel offsets. It ends up somewhere unrelated.
Test it: zoom to 400% and try to use the page. Then zoom text only, if your browser offers it
separately, because that is a different and harsher test — your px sizes will not move at
all.
Do not depend on JavaScript for content
A static site should not, and this course has not needed to. But the principle is worth stating because it is the same resilience argument:
If content only appears after JavaScript runs, it is missing for anybody whose script failed — a flaky connection, a blocked CDN, an extension, an error in a different script on the page. Scroll-triggered reveals from module 6 are the common version: content that is absent until you scroll.
Write the content in the HTML and enhance it. That is what progressive enhancement means, and for a static site it costs nothing because there is nothing to enhance.
The graceful degradation of CSS itself
CSS fails in a useful way if you let it, and @supports is the explicit tool:
.cards {
display: flex;
flex-wrap: wrap; /* works everywhere */
}
@supports (grid-template-columns: subgrid) {
.cards { display: grid; grid-template-rows: subgrid; }
}
The base styles are the version that works; the enhancement is conditional. This is mobile-first thinking applied to features rather than widths, and it is the same argument.
Remember from module 6 that an unsupported selector invalidates its whole rule — so a
:has() rule simply does not apply, which is a clean fallback if your base styles are
sensible. An unsupported property is dropped individually, leaving the rest of the rule.
The test fixture
Build this once and keep it:
<section class="cards">
<article class="card"><h3 class="card__title">Atta</h3></article>
<article class="card"><h3 class="card__title">Shree Ganesh Provision Stores and General Merchants Premium Chakki Fresh Wholewheat Atta</h3></article>
<article class="card"><h3 class="card__title">contact@shreeganeshprovisionstores-kothrud.co.in</h3></article>
<article class="card"><h3 class="card__title">किराना सामान</h3></article>
<article class="card"><h3 class="card__title"></h3></article>
<article class="card"><h3 class="card__title">Tur Dal</h3><img class="card__image" src="does-not-exist.jpg" alt="Tur dal"></article>
</section>
Six cards: normal, far too long, an unbroken string, another script, empty, and a broken image. Load it at 320px, at 1440px and at 400% zoom. If that renders, your component is finished. If it does not, you have found the bugs before your client did.
Check your work
Why the content you designed with is misleading. You invented content that fits.
The eight cases. Longer, shorter, absent, many, one, none, unbroken string, another script.
What overflow-wrap: break-word does that word-break: break-all does not. Breaks only
when a word cannot fit, rather than constantly.
The three declarations text-overflow: ellipsis needs. overflow: hidden and
white-space: nowrap as well.
Why to think before truncating. An ellipsis hides the part the reader may have needed.
The two shrink fixes. min-width: 0 on a flex item, minmax(0, 1fr) on a grid track.
What a background on an image slot buys. Something sensible when the file 404s.
What an empty state must say. What happened and what to do next.
Why :empty catches people. A single space means not empty — but a comment alone does
count as empty.
How auto-fit handles both ends. One card stretches; twenty-three wrap, with no query.
What 400% zoom is equivalent to. Reflow at about 320px — and it breaks fixed pixel widths, fixed heights and sticky headers.
Why a fixed height on text is worse at 400%. The text needs four times the space and gets clipped.
Why content must be in the HTML. A failed script means missing content.
How an unsupported selector fails versus an unsupported property. The whole rule is invalidated; a single declaration is dropped.
Practice
- Build the six-card test fixture and load it. Write down every failure.
- Put a 60-character product name in a card built for 12 characters.
- Put an email address with no spaces in a flex row. Fix it with
min-width: 0. - Do the same in a
1frgrid track, then useminmax(0, 1fr). - Apply
text-overflow: ellipsiswith only that one declaration, then add the other two. - Clamp a description to three lines, then decide whether wrapping would have been better.
- Break an image's
srcand look at the card. Add a background colour and compare. - Add the
onerrorplaceholder and break the src again. - Render a grid with zero items, then one, then twenty-three. Fix each.
- Write an empty state that says what to do next. Show it with
:emptyand again with:has(). - Put a single space inside an element and confirm
:emptyno longer matches. Then put only an HTML comment inside and confirm it does. - Zoom your page to 400% and try to complete a task.
- Find a fixed pixel width that breaks at 400% and convert it to
max-widthinrem. - Put six lines of text in a
height: 100pxbox, then change tomin-height. - Un-stick your header below
min-height: 30remand check it at 400% zoom. - Put a Devanagari heading next to a Latin one and compare their line heights.
- Wrap a
subgridenhancement in@supportsand confirm the flex fallback works.
Official documentation
- MDN — overflow-wrap — And how it differs from
word-break. - MDN — text-overflow — Including the declarations it depends on.
- MDN — :empty — Exactly what counts as empty.
- MDN — @supports — Feature queries, including
selector(). - W3C — Understanding WCAG 1.4.10 Reflow — The 320px requirement and what 400% zoom means.
- W3C — Understanding WCAG 1.4.4 Resize Text — Why fixed pixel sizes fail text-only zoom.
Next: performance, on the phone your visitor actually has.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship