Design tools make border placement look simple.
In Figma, you can put a stroke outside the shape, inside the shape, or centered on the edge. In CSS, we get border, but we do not get a single property that says "make this stroke center-aligned like the design file."
That gap is exactly where handoff bugs start. A card grows larger than the mockup. An image border eats into the visible area. A centered stroke from the design turns into a fully inside or fully outside border in the browser.
The good news is that CSS can reproduce all three effects. You just have to pick the right mechanism.
This guide focuses on the real production questions:
- how CSS
borderactually affects box size - how to model outside and inside borders with
box-sizing - how to fake a centered border when CSS has no direct stroke-position property
- when
outlineorbox-shadoware better as visual-only borders - how to add reliable inner borders to images and avatars
If your component also relies on rounded corners, pair this with How border-radius Really Works: Percentages, Nested Corners, Overlap, and Transform. If you need a deeper explanation of box-shadow as a drawing tool, How CSS Shadows Really Work: text-shadow, box-shadow, and drop-shadow() is the best follow-up.
1. CSS has one real border model, but several visual border tricks
The native CSS border lives on the element's box.
That means border is not just decoration. It participates in layout and combines with the box model:
- content
- padding
- border
- margin
That is why the first question is not "which border color should I use?" It is:
Should this border change layout, or should it be visual only?
Three tools matter most here:
borderfor real box borders that affect sizing rulesoutlinefor visual rings that do not take spacebox-shadowfor visual border-like effects that also do not take space
Each one solves a different problem.
2. Outside versus inside borders is mostly a box-sizing question
This is the key mental model.
When you use border on a box with the default box-sizing: content-box, the declared width and height describe only the content box. The border expands outside that measured area.
That makes it feel like an outside stroke:
.card {
box-sizing: content-box;
width: 20rem;
border: 8px solid currentColor;
}
The rendered box becomes wider than 20rem because the left and right borders are added outside the content box.
When you switch to box-sizing: border-box, the declared dimensions include the border:
.card {
box-sizing: border-box;
width: 20rem;
border: 8px solid currentColor;
}
Now the overall box stays at 20rem, and the content area shrinks inward to make room for the border.
That feels like an inside stroke.
This is why many component libraries use a global reset like this:
html {
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
For fixed-size UI components, that usually produces the border behavior designers expect most often: the component keeps its outer size.
3. If the border should affect layout, use border first
The simplest production rule is:
- use
borderwhen the stroke is part of the component's real geometry - use
outlineorbox-shadowwhen the stroke should stay visual only
That matters because border still gives you the most complete control:
- per-side widths
- per-side colors
- real border styles such as
dashed,dotted, ordouble - natural integration with
border-radius
For example:
.panel {
box-sizing: border-box;
border: 2px solid rgb(0 0 0 / 0.12);
border-radius: 1rem;
}
If you truly want an inside border on a card, input, or panel, this is usually the cleanest solution.
The same idea also applies directionally. If the design is really about one side of the box and the component must survive RTL and LTR, prefer logical border properties:
.alert {
border-inline-start: 4px solid var(--accent);
}
If your project needs the broader direction-safe model, continue with LTR, RTL, and CSS Logical Properties: How to Build Direction-Safe Layouts.
4. Use outline or box-shadow when you need a visual border without resizing the box
Sometimes the stroke should not change layout at all.
That is where outline and box-shadow become useful.
Visual outside border with outline
.chip {
outline: 2px solid currentColor;
}
This creates a visible ring outside the box without changing layout size.
Visual outside border with box-shadow
.chip {
box-shadow: 0 0 0 2px currentColor;
}
This also creates a visual outside border without changing layout size.
Both patterns are useful when:
- the component size must stay fixed
- you need an extra ring on top of an existing border
- the effect is decorative or state-based rather than structural
But they are not equal.
outline is simpler and lightweight, but it is less flexible for exact design matching.
box-shadow is better when you need:
- better visual control
- easier layering with other effects
- smoother coordination with rounded corners
If the stroke is part of a richer visual system, box-shadow is often the more practical of the two.
Neither tool is a perfect replacement for a real border.
box-shadow is great for solid visual rings, but it does not reproduce border styles like dashed, dotted, or double cleanly. outline is simpler, but its rounded-corner behavior can still be inconsistent in some browsers, especially when you need exact visual parity with a rounded component. If precise radius matching matters, test carefully and prefer border or a shadow-based ring over assuming outline will look identical everywhere.
5. A visual inside border usually means inset shadow or a negative outline offset
CSS has no native border-position: inside, but we can fake it when we only need the visual result.
Inner border with inset shadow
.card {
box-shadow: inset 0 0 0 2px currentColor;
}
That draws a hard inner stroke without changing layout dimensions.
Inner border with negative outline-offset
.card {
outline: 2px solid currentColor;
outline-offset: -2px;
}
This can also create an inner-looking border.
In practice, box-shadow is usually the more design-friendly choice because it layers better with rounded UI and is easier to combine with other shadow treatments.
The main caveat is that box-shadow is still a shadow tool. If you accidentally add blur, you no longer have a crisp border:
.card {
box-shadow: inset 0 0 0 2px currentColor; /* crisp */
}
not:
.card {
box-shadow: inset 0 0 4px 2px currentColor; /* now it is blurred */
}
6. CSS does not have a native centered stroke, so you have to split the width
This is the part most designers notice immediately in handoff.
A centered stroke means:
- half of the stroke sits inside the edge
- half sits outside the edge
CSS border cannot do that on its own. So the practical solution is to combine two layers of equal thickness.
If the design asks for a 10px centered border, split it into two 5px pieces.
Option 1: border plus outer box-shadow
.card {
box-sizing: border-box;
border: 5px solid currentColor;
box-shadow: 0 0 0 5px currentColor;
}
This is often the most straightforward centered-border pattern because:
- the real border gives you the inside half
- the zero-blur shadow gives you the outside half
Option 2: inset shadow plus outline
.card {
box-shadow: inset 0 0 0 5px currentColor;
outline: 5px solid currentColor;
}
This is visually valid too, but it is more clearly a visual composition than a real border.
Option 3: border plus outline
.card {
box-sizing: border-box;
border: 5px solid currentColor;
outline: 5px solid currentColor;
}
This can work, but I would usually prefer the border plus box-shadow version when you need tighter visual control.
The real decision is not which combination is mathematically possible. It is which one ages better in your component system.
For most rounded UI, border plus box-shadow is the safer default.
It is also the combination I would trust first when the design system cares about consistent rounded geometry. Once outline enters the stack, the chances of browser-specific visual mismatch go up.
7. Images and avatars need a different pattern
This is where many otherwise correct border techniques feel awkward in production.
You can absolutely put a normal border on an <img>. But if the goal is a soft inner ring, an overlay stroke, or a centered decorative border, treating the image itself as the drawing surface often becomes brittle.
The important reason is that <img> is a replaced element. Real borders still work on it, but some border-like simulation techniques, especially inset shadow patterns, are much more reliable on a wrapper or pseudo-element than on the image element itself.
The more reliable pattern is to wrap the image and draw the visual border on the wrapper or a pseudo-element.
<div class="avatar">
<img src="./avatar.jpg" alt="Team member profile" />
</div>
.avatar {
position: relative;
inline-size: 6rem;
aspect-ratio: 1;
}
.avatar img {
display: block;
inline-size: 100%;
block-size: 100%;
border-radius: 50%;
object-fit: cover;
}
.avatar::after {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
box-shadow: inset 0 0 0 2px rgb(0 0 0 / 0.12);
pointer-events: none;
}
This gives you a reliable inner border overlay without shrinking the image area or needing extra decorative markup inside the media itself.
It is especially useful for avatars because very light photos can disappear into light backgrounds. A subtle inner ring keeps the image readable without making it feel boxed in.
If the component also clips the image, make sure the wrapper and the overlay share the same radius. Otherwise the stroke and the crop can drift apart at the edges.
8. Choose the technique by intent, not by habit
A lot of CSS border code becomes messy because the same tool gets reused for every stroke-like effect.
A cleaner decision guide looks like this:
Use border when:
- the stroke is part of the component's real geometry
- layout should account for it
- you need per-side border control
- you need real border styles
Use box-shadow when:
- the stroke should be visual only
- you need a fake outer border without resizing
- you need an inner visual border
- you are building a centered stroke with a second layer
Use outline when:
- you need a simple extra ring
- layout must stay unchanged
- the effect is utility-like rather than deeply art-directed
This is similar to the broader spacing lesson in When to Use margin, padding, and gap in CSS Layouts: choose the property that matches the job, not just the one that can be forced into the right screenshot.
9. A few edge cases are worth remembering
These are the border bugs that tend to repeat.
Rounded components need every layer to share the same radius
If you fake a centered border with border plus box-shadow, or an inner border with a pseudo-element, make sure every layer inherits the same border-radius.
Otherwise, the stroke edges stop lining up and the component feels slightly wrong even when the numbers technically match.
Visual-only borders can overlap neighbors
outline and zero-blur box-shadow do not take layout space.
That is useful, but it also means a thick visual border can overlap nearby content if components are packed too tightly.
Global border-box changes the default mental model
If your project resets everything to box-sizing: border-box, most ordinary borders will behave like inside borders by default.
That is usually a feature, not a bug, but it is worth remembering when you compare browser output to design-tool measurements.
10. The takeaway
Matching Figma-style stroke positions in CSS is less about a hidden property and more about using the box model deliberately.
If you keep one rule from this page, make it this one:
Inside, outside, and centered borders in CSS are really three different sizing and layering decisions.
Use border plus box-sizing when the stroke should be part of the box.
Use outline or box-shadow when the stroke should stay visual only.
Split the stroke into two layers when the design calls for a centered border.
And when images or avatars are involved, draw the decorative border on a wrapper or pseudo-element instead of forcing the media element to do all the work itself.