If you have worked on a design system, you have probably had this conversation:

The border is shifting the layout.It looks different from Figma.Is this supposed to sit inside or outside the element?

Borders seem simple until the same edge has to work across design files, component states, layout rules, and accessibility modes.

For many visual treatments, a zero-blur box shadow is a useful alternative. I call these shadow rings. They are not a universal replacement for borders, but they solve a specific class of design-to-code problems well.

The problem: Figma and CSS describe edges differently

Figma gives designers three stroke positions: inside, centre, and outside.

Figma stroke panel showing position options: Center, with weight controls and effects section

CSS borders participate in the box model rather than offering the same three position controls. Under the default content-box model, a 2px border adds 4px to the rendered outer width and height of an element with a declared width.

/* With content-box, the rendered outer width becomes 204px */
	.button {
	  width: 200px;
	  border: 2px solid #0066cc;
	}

With box-sizing: border-box, that border is included inside the declared dimensions. The problem is not that borders always shift layout. It is that adding or changing one can alter geometry when the box model, intrinsic sizing, or surrounding layout has not accounted for it.

  • Layout can shift when a border appears only on hover, selection, or focus
  • Designers and developers can interpret the intended stroke position differently
  • Intrinsic and fixed-size components can respond differently to the same border change
  • The visual edge can become coupled to layout behaviour that was not part of the design intent
Hover, focus, or tap an item to feel how borders alter layout while shadow rings stay paint-only.
Navigation Tabs
Hover, focus, or tap
Choose an item to compare the two ring implementations.
Selectable Cards
Click to select
The selected card squeezes its content box inward.
Border
border: 2px solid #38BDF8;
Consumes layout space on each side.
Shadow Ring
box-shadow: 0 0 0 2px #38BDF8;
Paint-only, no layout impact.
Click the inputs to see how traditional borders cause layout shift, while shadow rings stay perfectly aligned

The alternative: shadow rings

A box shadow with zero blur can create a solid visual edge without changing the element's box geometry.

/* A 2px visual ring that does not change layout */
	.button {
	  box-shadow: 0 0 0 2px #0066cc;
	}

The syntax is box-shadow: offsetX offsetY blur spread color. When the offsets and blur are zero, the spread value becomes the ring width.

The inset keyword moves the ring inside the element:

/* Outside ring (like Figma's "outside" stroke) */
	.card {
	  box-shadow: 0 0 0 2px #e0e0e0;
	}
	/* Inside ring (like Figma's "inside" stroke) */
	.card {
	  box-shadow: inset 0 0 0 2px #e0e0e0;
	}
Outside Ring
box-shadow: 0 0 0 2px rgb(228 228 231);
Paint extends outside the box; layout stays the same.
Inside Ring
box-shadow: inset 0 0 0 2px rgb(228 228 231);
Paint sits inside the box; layout stays the same.
Centered Ring
box-shadow: inset 0 0 0 1px rgb(228 228 231), 0 0 0 1px rgb(228 228 231);
Half inside / half outside (simulated via 1px inset + 1px outset).
Traditional Border
border: 2px solid rgb(228 228 231);
Adds to the box model; with content-box it grows layout (border-box avoids growth but shrinks the content box).
Different shadow ring techniques: outside ring, inside ring, centered ring, and how traditional borders compare

Why this matters for design systems

1. Composite visual tokens

Traditional border treatments often combine several decisions:

// The old way: multiple tokens that must stay in sync
	tokens: {
	  border: {
	    width: { sm: '1px', md: '2px', lg: '3px' },
	    color: { default: '#e0e0e0', focus: '#0066cc', error: '#d32f2f' },
	    style: { default: 'solid' },
	    radius: { sm: '4px', md: '8px', lg: '16px' }
	  }
	}

Shadow rings let a system publish a composite token for the complete visual treatment:

// The new way: complete ring definitions
	tokens: {
	  ring: {
	    default: '0 0 0 1px #e0e0e0',
	    focus: '0 0 0 2px #0066cc',
	    error: 'inset 0 0 0 2px #d32f2f',
	    selected: '0 0 0 2px #0066cc'
	  }
	}

This can make component usage simpler, but it does not remove the underlying decisions. Width, colour, inset or outset position, and contrast still need semantic governance. Border radius remains independent because the shadow follows the element's radius rather than defining it.

2. Composable with elevation

Since box-shadow accepts multiple values, a component can combine a ring and elevation in one declaration:

.card {
	  box-shadow:
	    0 0 0 1px rgba(0, 0, 0, 0.1), /* subtle border ring */
	    0 4px 12px rgba(0, 0, 0, 0.15); /* elevation shadow */
	}
	.card:hover {
	  box-shadow:
	    0 0 0 1px rgba(0, 0, 0, 0.1), /* border stays consistent */
	    0 8px 24px rgba(0, 0, 0, 0.2); /* elevation increases */
	}
Border ring · 0 0 0 1px rgba(255, 255, 255, 0.12)
Elevation · 0 14px 40px rgba(0, 0, 0, 0.6)
Card component showing combined border ring + elevation shadow, with the layers labeled

This is useful, but it also means the ring and elevation share one property. A component system needs a clear way to compose those layers without one state accidentally replacing the other.

3. Geometry that stays fixed

For hover and selected states, a shadow ring can change the visual edge while the component keeps the same geometry:

Focus increases border-width → the element grows → content jumps.
.input {
  border: 1px solid #e0e0e0;
  transition: border-width 150ms ease;
}
.input:focus {
  border-width: 2px;       /* grows the element */
  border-color: #0066cc;
}

4. Focus needs a different default

For focus, start with the property designed for the job. Outline supports outline-offset, does not affect layout, and survives more user-agent accessibility modes.

.button:focus-visible {
	  outline: 2px solid #0066cc;
	  outline-offset: 2px;
	}

A layered box shadow can still create a polished two-colour indicator, but it needs contrast testing against both the component and its surrounding surface. It also needs a forced-colours fallback because browsers can suppress box shadows in forced-colours mode.

.button:focus-visible {
	  outline: 2px solid transparent;
	  box-shadow:
	    0 0 0 2px #ffffff,
	    0 0 0 4px #0066cc;
	}
Light
Tab to focus
Dark
Tab to focus
Button with offset focus ring on both light and dark backgrounds

Choosing the right primitive

- The edge is structural rather than decorative - It must participate in forced-colours rendering - You need dashed, dotted, gradient, or border-image styling - You need collapsed table geometry

- The edge is a visual layer that should not affect layout - The component can safely compose multiple shadows - You have checked whether an overflow-hidden ancestor clips an outer ring

Use outline for focus unless you have tested an alternative across contrast and forced-colours modes.

The takeaway

The useful shift is not "borders bad, shadows good." It is choosing the primitive whose behaviour matches the job.

Borders describe structure. Outlines describe focus. Shadow rings are excellent visual layers when geometry must stay fixed. A design system gets simpler when those responsibilities are explicit.