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.
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
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;
}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 */
}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:
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;
}Choosing the right primitive
Use a border when:
- 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
Use a shadow ring when:
- 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.