---
title: 'Border, Shadow, and Shape'
source: 'https://academia.sh/en/courses/css-fundamentals/border-shadow-and-shape'
course: 'Visual Presentation with CSS'
language: en
updated: '2026-08-17T18:09:18+00:00'
license: 'CC BY-SA 4.0'
---

# Border, Shadow, and Shape

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

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.

```js
// 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:

```css
.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 $b$ extends roughly $b/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:

```css
.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.

```css
/* 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.
