A fancy button in Figma: 160 × 48 pixels, a 16-pixel radius, a gradient background. A 1-pixel gradient border, a small inner shadow from the top for a modern look, a centered label and a few pixels of padding.
A developer or AI agent can retrieve the CSS values through Dev Mode or MCP. Yet something still looks wrong after implementation. The button is suddenly two pixels too tall, the inner shadow is too large, and the gradient border does not look like it did in Figma.
The problem is rarely a mistake in copying the values. Figma and CSS have fundamentally different ideas about what a border is, where it sits and how it relates to the element's box.
Two Worlds: Vectors Meet the Box Model
Figma is primarily a vector-based design tool. A stroke is a separate object with a position relative to the path: inside, center or outside. By default, the stroke does not affect layout.
CSS follows the box model: a strictly defined stack of content, padding, border and margin. The border is not layered over the frame. It is part of the box and takes up layout space. That is the key difference.
Figma thinks in strokes. Browsers think in boxes. The gap between them is part of how both systems were designed.
The Problem with Inside Borders
In Figma, strokes are positioned inside by default. A 160 × 48 px frame keeps its size even with a 1 px border. The border sits along the inside edge and covers the content instead of reserving space for it. If an image reaches the edge of the frame, for example, the border covers its outermost pixel when “Clip content” is enabled.
CSS has no equivalent border position. With the commonly used box-sizing: border-box, a border reduces the inner area. With the specification's default, content-box, it is added on top and increases the element's size.
Inner Shadows Behave Differently
Figma renders an inner shadow underneath the stroke. The first pixel of the shadow is covered. CSS, on the other hand, starts the inner shadow after the border, at its inner edge. The full height of the shadow remains visible. The implementation is not pixel-perfect.
Where the Shadow Begins
Figma 2 px offset
1 px of visible shadow
CSS 2 px offset
2 px of visible shadow
Component Dimensions Drift
Figma shows a height of 48 px. With border-box, the outer frame stays at 48 px, but the content area shrinks by twice the border width. With content-box, the frame grows to 50 px. If the button has no fixed height and only padding around its label, the border is always added to its height, regardless of box-sizing. Two buttons side by side, a primary button without a border and a secondary button with a 1 px border, no longer have the same height in the browser. In Figma, they do.
Three Ways to Create an Inside Border
There are three established CSS techniques that imitate Figma's inside stroke. All three avoid layout shifts. They differ in semantics, styling options and how they can be layered.
box-shadow: inset
An inset shadow with no blur, no offset and only spread looks like an inside stroke.
.button {
box-shadow: inset 0 0 0 1px currentColor;
}
Advantages: no layout impact, and multiple layers are possible by separating them with commas. Disadvantages: it is not semantically a border, and some border properties, styling options and edge cases are not supported.
outline with a Negative Offset
An actual outline, moved inward.
.button {
outline: 1px solid currentColor;
outline-offset: -1px;
}
Real border styles are available, without affecting layout. The downside: an element can have only one outline, and outlines are intended for focus states. Using one as a border means giving up the native focus treatment.
Pseudo-Element Overlay
An absolutely positioned ::after layered over the element. This is also how Framer implements its “Inside Border”.
.button {
position: relative;
}
.button::after {
content: "";
position: absolute;
inset: 0;
border: 1px solid currentColor;
border-radius: inherit;
pointer-events: none;
}
The only method that really behaves like Figma's inside stroke: the pseudo-element actually covers the content instead of just changing its effect on layout. Images that reach the edge are covered at their outermost pixels, just as they are in Figma. Full border-style support, compatible with gradient backgrounds, and ::before plus ::after even allow multiple borders to be stacked. It costs a pseudo-element. And something has to be positioned absolutely. Where is the problem?
And Then Comes the Gradient
Figma accepts any paint as a stroke fill. CSS only accepts a single color in border-color. Gradients are not supported. Gradient borders make the gap between Figma and CSS most obvious. Ask an AI to generate one, and it almost never gets it right on the first attempt.
Three methods, the same visual result. border-image without a radius, background-clip requiring an opaque fill, and mask-composite working over images too.
The examples use a 160 × 48 px button with a 16 px radius and a 1 px border. With a real border, a 20 px line height, 13 px of vertical padding on each side and a 1 px border on each side add up to 48 px. An overlaid border takes up no layout space, so that version uses 14 px of padding on each side instead.
border-image
The method provided by the specification, with one catch: border-image completely ignores border-radius. Fine for rectangular dividers, unusable for rounded components.
.button {
border: 1px solid transparent;
border-image: linear-gradient(135deg, #d94f30, #6b4eff) 1;
}
background-clip with Two Layers
.button {
box-sizing: border-box;
min-width: 160px;
line-height: 20px;
padding: 13px 16px;
border: 1px solid transparent;
border-radius: 16px;
background:
linear-gradient(white, white) padding-box,
linear-gradient(135deg, #d94f30, #6b4eff) border-box;
}
Mask-Composite
.button {
position: relative;
box-sizing: border-box;
min-width: 160px;
line-height: 20px;
padding: 14px 16px;
border: 0;
border-radius: 16px;
background: transparent;
}
.button::before {
content: "";
position: absolute;
inset: 0;
padding: 1px;
border-radius: inherit;
pointer-events: none;
background: linear-gradient(135deg, #d94f30, #6b4eff);
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
}
In Practice
Figma's default, Inside Stroke with Strokes Excluded, does not map neatly to any CSS mode. The stroke sits over the frame. The frame keeps its size, as does its content. There is no direct equivalent on the web. The Strokes: Included option in the Auto Layout settings theoretically behaves like box-sizing: border-box. In practice, though, only when the frame has a fixed height. With Hug contents, the typical button setup, the frame grows outward instead of reducing its inner area. The option is also only available for Auto Layout frames.
Figma's behavior can only be reproduced accurately in CSS with one of the three inside-border techniques above.
The background determines which gradient technique to use. On flat, known surfaces, background-clip is the simplest choice. Over images, gradients or glass effects, mask-composite is the only clean solution.
Conclusion
The fancy button from the opening can be built. But not by carefully copying and pasting from Dev Mode. Figma does not render like CSS. Dev Mode and MCP provide valid values, but no reliable mapping between the models. They are suggestions that need translating.
A button with a gradient background and gradient border will most likely look different in code than in Figma on the first attempt, unless those translation rules are documented.
Pixel-perfect is rarely the right goal. What matters more is that the result captures the intention. Whether the border ends up being rendered with box-shadow, outline or a pseudo-element is an implementation detail.
What is missing is not better tool integration. It is awareness of the places where Figma and CSS do not mean the same thing. That is the job, not the problem.