Skip to content
academia.sh

Lesson 24 / 26

Border, Shadow, and Shape

Border components, the corner radius's scaling rule, the inner-radius calculation, and the area shadow layers occupy.

Contents

The boxes’ inside and back are complete. Their edges remained: how the border gets drawn, how the corners get rounded, and how far shadows falling outside the box extend.

These three subjects get taken up together, because they share the same boundary: the border box. The border gets drawn on that boundary, the corner radius rounds that boundary, the shadow falls outside that boundary.

The Border’s Three Components

border is a shorthand and sets three longhands: -width, -style, -color.

When border-style does not get written, its initial value is none, and the border does not get drawn. This is the most frequent reason a border does not show up: the thickness and color got written, but the style did not.

Style values include solid, dashed, dotted, double, groove, ridge, inset, outset, and hidden. The last four give the border calculated light–dark tones and produce an embossed effect; they do not get used in designed interfaces because color control falls outside the scale.

When border-color does not get written, currentcolor gets used — that is, the element’s text color. This behavior, mentioned in the Color Representations lesson, means that keeping the border tied to the text color is the default.

The four edges can get set separately and get written with logical names: border-block-end, border-inline-start, and their relatives.

The Corner Radius Has to Fit the Box

border-radius rounds the corners. The value is the radius of the arc at the corner, and it can get given separately for each of the four corners.

There is a constraint: the sum of the radii of the two corners on an edge cannot exceed that edge’s length. If it does, all the radii get shrunk by the same factor — not just the overflowing corner, all of them. This preserves the ratio between the corners.

// border.mjs — corner-radius scaling, inner radius, and shadow box math
// rule: if the radius sum exceeds the edge, all radii shrink by the same factor f
function scaleRadius(box, radii) {
  const { width, height } = box;
  const factor = Math.min(
    width  / (radii.topLeft + radii.topRight),
    width  / (radii.bottomLeft + radii.bottomRight),
    height / (radii.topLeft + radii.bottomLeft),
    height / (radii.topRight + radii.bottomRight),
    1
  );
  return Object.fromEntries(Object.entries(radii).map(([k, v]) => [k, +(v * factor).toFixed(2)]));
}

const cases = [
  { box: { width: 200, height: 80 },  radii: { topLeft: 12, topRight: 12, bottomRight: 12, bottomLeft: 12 } },
  { box: { width: 200, height: 80 },  radii: { topLeft: 60, topRight: 60, bottomRight: 60, bottomLeft: 60 } },
  { box: { width: 100, height: 40 },  radii: { topLeft: 999, topRight: 999, bottomRight: 999, bottomLeft: 999 } },
];
for (const c of cases) {
  const scaled = scaleRadius(c.box, c.radii);
  console.log(`box ${c.box.width}x${c.box.height}, requested ${c.radii.topLeft} -> applied ${scaled.topLeft}`);
}

console.log("\n--- inner radius: smaller by the border thickness ---");
for (const [outer, thickness] of [[12, 1], [12, 4], [12, 16], [8, 8]]) {
  const inner = Math.max(0, outer - thickness);
  console.log(`outer radius=${String(outer).padStart(2)} border=${String(thickness).padStart(2)} -> inner radius=${inner}${inner === 0 ? "  (square inner edge)" : ""}`);
}

console.log("\n--- box-shadow: the rectangle the shadow occupies ---");
function shadowBox({ width, height }, { x, y, blur, spread }) {
  // shadow box: grows by spread, shifts by the offset, extends by half the blur
  const bleed = blur / 2;
  return {
    left:   x - spread - bleed,
    top:    y - spread - bleed,
    right:  width + x + spread + bleed,
    bottom: height + y + spread + bleed,
  };
}
const box = { width: 200, height: 80 };
for (const shadow of [
  { x: 0, y: 2,  blur: 4,  spread: 0 },
  { x: 0, y: 8,  blur: 24, spread: -4 },
  { x: 0, y: 0,  blur: 0,  spread: 3 },
]) {
  const rect = shadowBox(box, shadow);
  console.log(`box-shadow: ${shadow.x}px ${shadow.y}px ${shadow.blur}px ${shadow.spread}px -> shadow (${rect.left}, ${rect.top}) .. (${rect.right}, ${rect.bottom})`);
}
console.log(`the box itself: (0, 0) .. (${box.width}, ${box.height})`);
box 200x80, requested 12 -> applied 12
box 200x80, requested 60 -> applied 40
box 100x40, requested 999 -> applied 20

--- inner radius: smaller by the border thickness ---
outer radius=12 border= 1 -> inner radius=11
outer radius=12 border= 4 -> inner radius=8
outer radius=12 border=16 -> inner radius=0  (square inner edge)
outer radius= 8 border= 8 -> inner radius=0  (square inner edge)

--- box-shadow: the rectangle the shadow occupies ---
box-shadow: 0px 2px 4px 0px -> shadow (-2, 0) .. (202, 84)
box-shadow: 0px 8px 24px -4px -> shadow (-8, 0) .. (208, 96)
box-shadow: 0px 0px 0px 3px -> shadow (-3, -3) .. (203, 83)
the box itself: (0, 0) .. (200, 80)

The first block gives three cases. The 12-unit radius fit the 200×80 box and got applied as is. The 60-unit radius did not fit the vertical edge — the two corners sum to 120, the edge is 80 — and all of them got reduced to 40.

The third row explains a pattern. The writing border-radius: 999px is the standard way to give a box fully rounded ends: the value always gets chosen large enough to drop to half the box’s size, and whatever the box’s dimensions are, the result is a pill shape. In the example, 999 got requested, 20 got applied — half of the 40-unit height.

Inner Radius

The second block explains why nested rounded corners sometimes look wrong.

The radius at the border’s inner edge is smaller than the outer radius by the border’s thickness. If the thickness is larger than the radius, the inner radius gets zeroed out and the inner edge becomes square — a border that is rounded outside and square inside results.

The same calculation applies to nested boxes too. If a second rounded box gets placed inside a card, sitting inward by the padding amount, the inner box’s radius has to be smaller than the outer radius by the padding; otherwise the two arcs do not stay parallel:

.card       { border-radius: 12px; padding: 8px; }
.card .area { border-radius: 4px; }   /* 12 - 8 */

Shadow Layers

box-shadow takes four lengths: horizontal offset, vertical offset, blur radius, and spread. The last two are optional.

The third block calculates the rectangle a shadow occupies. A shadow with blur radius bb extends roughly b/2b/2 beyond the box’s edge; the spread value, though, directly grows or shrinks the shadow rectangle.

The negative spread in the second row is a pattern: when a soft shadow is wanted with a wide blur but the shadow should not spill from the box’s sides, the spread gets given negative. The result is the shadow falling mostly below.

The third row shows a shadow with no blur and no offset: spread alone. This draws a sharp ring around the box and behaves like a border that takes up no space. Its difference from outline is that the shadow follows the corner radius.

Multiple shadows can get written separated by commas; the first one written gets drawn on top. Layered shadows give a softer sense of depth than a single shadow.

The inset keyword turns the shadow inward, into the box.

There is a distinction between box-shadow and filter: drop-shadow(): the first follows the border box’s rectangle (and its radius), the second follows the element’s visible shape and skips transparent regions. For an icon with transparency, the second is the right tool.

Giving Shape

clip-path restricts the box’s drawn region to a geometric shape:

.icon { clip-path: circle(50%); }
.tag { clip-path: polygon(0 0, 100% 0, calc(100% - 12px) 100%, 0 100%); }

Clipping does not affect layout: the box still takes up space at its rectangular size, only its drawing gets clipped. To change the boundary of text flowing around a floated box, shape-outside is needed; when the two properties get written together, the shape and the flow boundary coincide.

Clipping cuts off everything that falls outside the box — the focus ring included. A clipped control’s focus indicator can stay invisible; in that case, the indicator has to get brought inside the box.

/* station.css — step 23: border, corner, and shadow */
.card {
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 16px;
  box-shadow:
    0 1px 2px rgb(28 39 51 / 0.06),
    0 8px 24px -4px rgb(28 39 51 / 0.08);
}

.card .area { border-radius: 4px; }

.status-badge {
  border-radius: 999px;
  border: 1px solid currentcolor;
  padding-block: 2px;
  padding-inline: 8px;
}

.measurement-table { border-collapse: collapse; }

a:focus-visible {
  outline: 3px solid var(--brand-dark);
  outline-offset: 2px;
  border-radius: 2px;
}

The card has two shadow layers: the close, sharp one defines the edge, the far, soft one gives depth. In the second, negative spread limits the shadow spilling to the sides.

The badge’s border color is currentcolor — whatever the text color is, the border becomes that too. When a warning badge carries red text, its border also becomes red; no separate rule gets written.

The border-collapse: collapse declaration merges table cells’ adjacent borders into a single line. This mode also prevents a radius from getting applied to the table’s corners; if a rounded-corner table is wanted, the table has to get wrapped in a container and the radius has to get given there.

Summary

  • If border-style does not get written, the border does not get drawn; this is the most frequent reason a border with thickness and color written does not show up.
  • If the sum of an edge’s two corner radii exceeds the edge, all the radii get shrunk by the same factor; the writing border-radius: 999px produces a pill shape by relying on this rule.
  • The inner corner radius is smaller than the outer radius by the border’s thickness, and once the difference drops below zero the inner edge becomes square; in nested boxes, the radius gets reduced by the padding.
  • A box-shadow shadow extends by half the blur, the spread value directly grows or shrinks the shadow rectangle; negative spread limits the overflow.
  • clip-path only clips the drawing, it does not change layout or the space taken up; because the focus indicator can also get clipped, the indicator has to get brought inside the box.

Next Step

Throughout this topic, lengths always got written as pixels and rem, and the ch unit got left undefined. In how many separate forms can a length get written, and why does the difference between em and rem produce a chained calculation? The next lesson defines CSS units and shows the conversions with calculation.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close