Ein Fancy Button in Figma: 160 × 48 Pixel, 16 Pixel Radius, Verlauf als Hintergrund. 1 Pixel Border mit Verlauf, ein kleiner inner-shadow von oben für einen modernen Look, zentriertes Label und paar Pixel Padding.
Die CSS-Werte dazu kann der Entwickler oder AI-Agent sauber via Dev Mode/MCP abrufen. Doch nach der Implementation stimmt trotzdem etwas nicht. Der Button ist auf einmal zwei Pixel zu hoch, der Inner Shadow zu groß und die Gradient-Border sieht auch nicht so aus wie in Figma.
Der Grund liegt selten in fehlerhafter Übertragung, denn Figma und CSS haben grundsätzlich unterschiedliche Vorstellungen davon, was die Border ist, wo sie sitzt und wie sie sich zur Box des Elements verhält.
Zwei Welten: Vektor trifft Box-Model
Figma ist primär ein vektorbasiertes Design-Tool. Für Figma ist ein Stroke ein eigenständiges Objekt mit einer wählbaren Position relativ zum Pfad: inside, center, outside. Layout-Auswirkungen hat der Stroke standardmäßig keine.
CSS folgt dem Box-Model: einem streng definierten Stack aus Content, Padding, Border und Margin. Die border ist nicht über den Frame gelegt, sondern Teil davon und belegt Layout-Platz. Das ist hier der wichtigste Unterschied.
Figma denkt in Strokes. Browser denken in Boxen. Die Lücke dazwischen ist Teil des Designs beider Systeme.
Das Problem mit den Inner Borders
In Figma sind Strokes standardmäßig inside positioniert. Ein Frame mit 160 × 48 px behält seine Größe auch mit einer 1 px breiten Border. Sie liegt innen am Rand und überlagert den Inhalt, statt Platz dafür zu reservieren. Reicht zum Beispiel ein Bild bis an den Rand des Frames, verdeckt die Border bei aktiviertem „Clip content“ den äußersten Pixel des Bildes.
CSS kennt diese Border-Position nicht. Mit dem heute üblichen box-sizing: border-box reduziert eine border die innere Fläche. Mit dem Spec-Default content-box wird sie additiv und vergrößert das Element.
Inner Shadows verhalten sich unterschiedlich
Einen Inner-Shadow rendert Figma unter den Stroke. Der erste Pixel des Shadows wird verdeckt. CSS hingegen rendert den Inner-Shadow erst hinter der Border, ab der inneren Kante. Die volle Shadow-Höhe bleibt sichtbar. Pixelperfect ist das in der Umsetzung nicht.
Wo der Schatten beginnt
Figma 2 px Offset
1 px Schatten sichtbar
CSS 2 px Offset
2 px Schatten sichtbar
Komponenten-Maße driften
Figma zeigt 48 px Höhe an. Mit border-box bleibt der Frame außen bei 48 px, der Content-Bereich schrumpft jedoch um die doppelte Border-Breite. Mit content-box wächst der Frame auf 50 px. Hat der Button gar keine fixe Höhe, sondern nur Padding um das Label, ist die Border immer additiv, egal welches box-sizing gesetzt ist. Zwei Buttons nebeneinander, Primary ohne Border, Secondary mit 1 px Border, sind im Browser nicht mehr gleich hoch. In Figma schon.
Drei Wege zur Inside Border
Es gibt drei etablierte CSS-Techniken, die Figmas Inner-Stroke-Verhalten nachmachen. Alle drei vermeiden Layout-Verschiebung; sie unterscheiden sich in Semantik, Stylability und Stackbarkeit.
box-shadow: inset
Ein Inset-Shadow ohne Blur, ohne Offset, nur Spread sieht aus wie ein Inner-Stroke.
.button {
box-shadow: inset 0 0 0 1px currentColor;
}
Vorteile: kein Layout-Impact, mehrere Layer durch Komma-Separierung möglich. Nachteile: es ist semantisch keine Border, und einige Border-Eigenschaften, Stylings und Edge Cases entfallen.
outline mit negativem Offset
Eine echte Outline, nach innen geschoben.
.button {
outline: 1px solid currentColor;
outline-offset: -1px;
}
Echter Border-Style verfügbar, kein Layout-Impact. Nachteil: nur eine Outline pro Element, und Outline ist eigentlich für Focus States gedacht. Wer sie als Border missbraucht, gibt das native Focus-Verhalten auf.
Pseudo-Element-Overlay
Ein absolut positioniertes ::after als Layer über dem Element. So macht es auch Framer für seine "Inside Border".
.button {
position: relative;
}
.button::after {
content: "";
position: absolute;
inset: 0;
border: 1px solid currentColor;
border-radius: inherit;
pointer-events: none;
}
Die einzige Methode, die wirklich wie Figmas Inside Stroke funktioniert: Das Pseudo-Element überlagert tatsächlich den Inhalt, statt nur am Layout-Verhalten zu drehen. Bilder, die bis zum Rand gehen, werden in den obersten Pixeln genauso verdeckt wie in Figma. Voller Border-Style-Support, kombinierbar mit Gradient-Backgrounds, und über ::before plus ::after lassen sich sogar mehrere Borders stapeln. Kostet ein Pseudo-Element. Und etwas muss absolut positioniert werden. Wo ist das Problem?
Und dann kommt der Gradient
Figma akzeptiert jeden Paint als Stroke-Fill. CSS akzeptiert in border-color nur eine einzelne Farbe. Gradients gehen nicht. Bei Gradient Borders wird die Lücke zwischen Figma und CSS am sichtbarsten. Wer das von einer KI generieren lässt, bekommt es fast nie auf Anhieb richtig.
Drei Methoden, dasselbe optische Ergebnis. border-image ohne Radius, background-clip braucht einen opaken Untergrund, mask-composite funktioniert auch über Bildern.
Die Beispiele verwenden einen Button mit 160 × 48 px, 16 px Radius und 1 px Border. Bei einer echten Border ergeben 20 px Zeilenhöhe, zweimal 13 px vertikales Padding und zweimal 1 px Border die 48 px Höhe. Eine darüberliegende Border beansprucht keinen Layout-Platz, dafür sind es zweimal 14 px Padding.
border-image
Der spec-konforme Weg, mit einem Haken: border-image ignoriert border-radius komplett. Für rechteckige Trennlinien okay, für gerundete Komponenten unbrauchbar.
.button {
border: 1px solid transparent;
border-image: linear-gradient(135deg, #d94f30, #6b4eff) 1;
}
background-clip mit doppeltem Layer
.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 der Praxis
Figmas Default (Inside Stroke, Strokes Excluded) entspricht keinem CSS-Modus sauber. Der Stroke liegt über dem Frame, der Frame behält seine Größe, der Inhalt darin auch. Im Web gibt es das so nicht. Es gibt zwar die Option Strokes: Included in den Auto Layout Settings, die theoretisch wie box-sizing: border-box funktioniert. In der Praxis aber nur dann, wenn der Frame eine fixe Höhe hat. Bei Hug-Content (also dem typischen Button) wächst der Frame nach außen, statt den Inhalt nach innen zu schrumpfen. Außerdem gibt es die Option nur in Auto Layout Frames.
Sauber abbilden lässt sich Figmas Verhalten in CSS nur über eine der drei Inside-Border-Methoden weiter oben.
Hintergrund-Kontext bestimmt die Gradient-Methode. Auf flachen, bekannten Flächen ist background-clip die einfachste Wahl. Auf Bildern, Verläufen oder Glas-Effekten ist mask-composite die einzige saubere Lösung.
Fazit
Der Fancy Button vom Anfang lässt sich bauen. Aber nicht durch sauberes Copy-Paste aus dem Dev Mode. Figma rendert nicht wie CSS. Der Dev Mode und MCP generieren zwar gültige Werte, aber kein verlässliches Modell-Mapping. Es sind Vorschläge, die man übersetzen muss.
Ein Gradient Button mit Gradient Border landet höchstwahrscheinlich erstmal visuell anders als in Figma im Code, solange die Übersetzungsregeln nicht dokumentiert sind.
Pixelperfect ist dabei selten der richtige Anspruch. Wichtiger ist, dass das Ergebnis die Intention trifft. Ob die Border am Ende über box-shadow, outline oder ein Pseudo-Element rendert, ist Implementation Detail.
Was fehlt, ist keine bessere Tool-Integration. Sondern Awareness an den Stellen, wo Figma und CSS nicht dasselbe meinen. Das ist der Job, nicht das Problem.