Backgrounds, borders, radius and shadows
This is the decorative half of CSS: the properties that turn a correct layout into something that looks made on purpose. None of it is complicated. The parts worth learning properly are the ones with a trap — background sizing, and shadows that look cheap.
Backgrounds
.hero {
background-color: #1a4d2e;
background-image: url("images/shop.jpg");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
Or the shorthand, which resets everything you do not mention:
.hero {
background: #1a4d2e url("images/shop.jpg") center / cover no-repeat;
}
The / before cover is required — it separates position from size and is the part
people get wrong. Position first, then /, then size.
Always set a background-color behind a background-image. The image might
fail, or not have loaded yet, and white text on nothing is invisible.
cover versus contain
background-size: cover; /* fill the box, crop the overflow */
background-size: contain; /* fit entirely, leave empty space */
background-size: 100% auto;
background-size: 200px 100px;
cover is what you want for a hero: the box is always filled. It crops, so the
edges of the image are lost — and on a narrow phone in portrait a wide landscape
photo loses most of its sides. If the subject is off-centre, say so:
background-position: 70% center;
contain never crops and leaves gaps, which is right for a logo and wrong for a
hero.
A background image is not content
This is the rule that matters. A background-image has no alt, is not announced
by a screen reader, and is not indexed. So:
Use background-image for decoration — a texture, a gradient, a hero the text
sits on top of.
Use <img> for content — a product photograph, a person, anything a visitor
would want to see or would miss if it were absent.
The test from module 1 still applies: if the image carries information, it needs an
alt, so it needs to be an <img>.
An <img> is also better for performance: it can be lazy-loaded, it can carry
srcset for different screen sizes, and the browser can prioritise it. A CSS
background is discovered later, after the stylesheet has been parsed.
Gradients
background: linear-gradient(to bottom, #1a4d2e, #0f3d20);
background: linear-gradient(135deg, #1a4d2e 0%, #2d7a4a 50%, #0f3d20 100%);
background: radial-gradient(circle at top right, #2d7a4a, #1a4d2e);
A gradient is an image, so it goes in background-image and can be layered with
one. This is the standard trick for keeping text readable on a photo:
.hero {
background:
linear-gradient(rgb(0 0 0 / 0.55), rgb(0 0 0 / 0.55)),
url("images/shop.jpg") center / cover no-repeat;
color: white;
}
Layers are listed front to back — the first one is on top. That is the opposite of what most people guess, and it is why the dark overlay goes first.
A two-stop gradient with the same colour twice is how you get a flat tint. It is
much better than a separate overlay div, and better than opacity on the image,
which would fade the text too.
One accessibility note: a gradient over a photo is how you make white text pass contrast. Check it in devtools' Accessibility pane rather than trusting your eye — a photo's brightness varies across it, and text that is readable over the dark part may fail over the light part.
Borders
border: 2px solid #ddd; /* width | style | colour */
border-width: 2px;
border-style: solid; /* dashed dotted double none */
border-color: #ddd;
border-block-end: 1px solid #eee; /* logical: the bottom, in English */
border-style is required. border: 2px #ddd with no style draws nothing,
because the default style is none. That is a common five-minute bug and devtools
shows the declaration accepted, because it is valid — just invisible.
A border adds to the element's size, so it is part of the box-sizing story from the
box-model lesson. With border-box it comes out of the content area instead.
outline is not a border
outline: 2px solid #1a4d2e;
outline-offset: 2px;
An outline is drawn outside the border, takes up no space, and does not affect layout. Which is exactly why it is used for focus rings — adding one does not shift the page.
It also means an outline can be a useful debugging tool: * { outline: 1px solid red }
shows you every box without moving anything, where border would reflow the entire
page.
border-radius
border-radius: 8px;
border-radius: 8px 8px 0 0; /* TRouBLe again: TL TR BR BL */
border-radius: 50%; /* a circle, on a square element */
border-radius: 999px; /* a pill — any large value works */
Note the clockwise order starts at top-left here, not top, which is different
from padding and margin.
border-radius: 50% makes a circle only if the element is square. On a rectangle it
gives an ellipse. For a circular avatar:
.avatar {
width: 48px;
aspect-ratio: 1;
border-radius: 50%;
object-fit: cover;
}
aspect-ratio: 1 guarantees square without hard-coding the height, and
object-fit: cover stops a non-square photo being squashed — which is the default
and looks terrible.
overflow: hidden on a rounded parent is what clips a child's corners to match.
Without it, an image inside a rounded card has square corners poking out.
Shadows
box-shadow: 0 1px 3px rgb(0 0 0 / 0.12);
/* x y blur colour */
box-shadow: 0 4px 6px rgb(0 0 0 / 0.1), 0 10px 20px rgb(0 0 0 / 0.08);
box-shadow: inset 0 2px 4px rgb(0 0 0 / 0.06);
box-shadow: 0 0 0 3px rgb(26 77 46 / 0.4); /* no blur: a ring */
Four values: horizontal offset, vertical offset, blur, colour. A fifth, spread, comes before the colour and is rarely useful.
What makes a shadow look cheap, and this is the whole reason this section exists:
- Too dark.
rgb(0 0 0 / 0.5)is a smudge. Real shadows on a white page are subtle — 0.08 to 0.15 alpha. - No vertical offset.
box-shadow: 0 0 10px blackglows in all directions, which no light source does. Light comes from above: a positiveyandxof 0. - One layer. Real shadows are a tight dark one close to the object and a wide soft one further out. Two or three layers with increasing blur and decreasing alpha is what expensive interfaces do.
/* a believable card */
.card {
box-shadow:
0 1px 2px rgb(0 0 0 / 0.08),
0 4px 12px rgb(0 0 0 / 0.06);
}
/* lifted on hover */
.card:hover {
box-shadow:
0 2px 4px rgb(0 0 0 / 0.08),
0 12px 28px rgb(0 0 0 / 0.1);
}
Shadows stack front to back like backgrounds, and they do not affect layout at all — so a shadow can overlap a neighbour, and a hover that only changes the shadow never causes reflow, which makes it cheap to animate.
text-shadow takes the same values minus inset. Use it sparingly; one legitimate
case is text over a photo where a gradient overlay is not enough.
The ring pattern
:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgb(26 77 46 / 0.4);
}
A zero-blur shadow is a solid ring. It is a common focus style — but note that
box-shadow is invisible in Windows high-contrast mode, where outline is not. For
focus specifically, prefer outline with outline-offset, or use both.
filter and backdrop-filter
filter: grayscale(1);
filter: brightness(0.8) blur(2px);
backdrop-filter: blur(10px); /* blurs what is BEHIND the element */
backdrop-filter is the frosted-glass effect on a sticky header. It is expensive to
render — noticeably so on a mid-range phone — so use it on one small element, never
on something large or animated.
filter: grayscale(1) on hover for a photo grid is a cheap, effective touch.
Check your work
The shorthand's /. Separates position from size: center / cover.
Why always set a background colour behind an image. The image may fail or not have arrived.
cover versus contain. Fill and crop, versus fit and leave gaps.
When a background image is wrong. When it carries information — that needs an
<img> with alt.
Which background layer is on top. The first one listed.
How to keep text readable on a photo. A translucent gradient layered above it,
not opacity.
Why border: 2px #ddd draws nothing. border-style defaults to none.
Why outline is used for focus. It takes no space, so adding it does not shift
the layout.
border-radius order. Clockwise from top-left.
Why border-radius: 50% is not always a circle. It needs a square element —
aspect-ratio: 1.
What clips a child's corners. overflow: hidden on the rounded parent.
Three things that make a shadow look cheap. Too dark, no vertical offset, one layer.
Why a shadow is cheap to animate. It does not affect layout.
Where box-shadow focus rings fail. Windows high-contrast mode.
Why to use backdrop-filter sparingly. It is expensive on a mid-range phone.
Practice
- Set a hero background with the shorthand. Leave out the
/beforecoverand see what happens. - Remove the
background-colorand throttle to Slow 4G. Watch the text before the image arrives. - Compare
coverandcontainon the same box at two different aspect ratios. - Set
background-position: 70% centeron an off-centre subject and resize. - Put a photo on a page as a
background-image, then as an<img>withalt. Listen to both with a screen reader. - Layer a dark gradient over a photo. Swap the layer order and explain the result.
- Check the contrast of white text over your hero in devtools' Accessibility pane, at both a dark and a light part of the photo.
- Write
border: 2px #dddand find out why nothing appears. - Add
* { outline: 1px solid red }, then try the same withborderand compare how much the page moves. - Make a circular avatar from a non-square photo. Then remove
object-fit: cover. - Round a card's corners and put an image at its top edge. Fix the poking corners.
- Write
box-shadow: 0 0 10px black, then a two-layer subtle shadow. Compare. - Animate only the shadow on hover and confirm in devtools' Performance panel that nothing reflows.
- Build a focus ring with
box-shadow, then withoutlineandoutline-offset. - Add
backdrop-filter: blur(10px)to a sticky header and scroll on a phone. Note whether it stutters.
Official documentation
- MDN — Using multiple backgrounds — Layer order, and which properties accept a list.
- MDN — box-shadow — All five values, and how multiple shadows stack.
- MDN — linear-gradient() — Angles, colour stops and the interpolation rules.
Next: typography, and what a web font actually costs.
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