Why CSS Never Matches Figma: Browser vs Canvas Pipelines

Browsers share one CSS model; Figma uses WASM and GPU shaders. Where design-to-code breaks: radius, strokes, blur, gap, and what to verify before you build.

Ashish 7 min read

Open any Figma file. Look at those perfectly smooth corners, the glass blur with real depth, the stroke that sits cleanly inside the element without touching the spacing.

Now build it in CSS.

It’s close. But something is always slightly off. The corners feel harder. The blur is flat. The border is eating into your padding.

You’ve probably blamed yourself: wrong tool version, wrong browser, not enough CSS tricks. The real reason is simpler: Figma and your browser are two different rendering engines running different math. Some things Figma draws don’t exist in CSS yet. Some things look the same but work differently under the hood.

This post breaks down how each engine works, where they diverge, and what that means for every design-to-code handoff.

Diagram of the Figma canvas pipeline: WASM, GPU shaders, and vector rasterization versus the browser CSS layout and paint model.

How Chrome turns CSS into pixels

Standard Browser Rendering Pipeline (Chrome) The 6-Step Sequence for Every Frame (DOM / CSSOM based) INPUT HTML CSS 1. PARSE " Build DOM " Build CSSOM " Tree Union 2. STYLE " Map Rules " Specificity " Inheritance 3. LAYOUT (REFLOW) Expensive: " Geometric math " Size / Position " Box model calc 4. PAINT " Record List " Layer Order " (No Pixels) 5. COMPOSITE Cheap Path: " Send to GPU " Layer Stacking GPU FINAL IMAGE CORE LIMITATION: THE W3C STANDARDS BOX 1. The Math is Fixed Chrome implements W3C math exactly. A border-radius or blur calculation is immutable and hard-coded in C++. 2. No Custom Shaders Standard HTML/CSS cannot invent new pixel math. You must wait for the W3C to propose and browsers to implement new features. 3. Interoperability The rigitidy is a feature: it ensures a website looks and behaves the same on every device. Figma bypasses this for performance.
Diagram of the standard browser (Chrome) rendering pipeline: HTML and CSS through parse, style, layout, paint, composite, and GPU output, plus W3C constraints.

When you write CSS and open it in Chrome, six things happen in sequence for every frame and every element.

1. Parse

Chrome reads your HTML and builds the DOM (a tree of every element). In parallel it reads CSS and builds the CSSOM (a tree of every style rule). Those two trees are combined into the render tree.

2. Style

Chrome walks every element and decides which CSS rules apply. It resolves specificity, inheritance, and the cascade. Every node ends up with computed styles; each property has an exact value.

3. Layout

Chrome figures out where each element sits and how much space it needs. This step is expensive. Change one width and Chrome may recalculate layout for a large subtree. That’s what people mean by reflow, and why it hurts performance.

4. Paint

Chrome records drawing instructions, not pixels yet: things like “fill this rectangle,” “draw this text run.” Order follows the CSS painting spec.

5. Composite

The page is split into layers and sent to the GPU. The GPU composites layers into the bitmap you see. Properties like transform and opacity can often skip heavy work earlier in the pipeline. That’s why they’re cheap to animate.

The crucial idea: every CSS property is implemented with math (fixed by web standards) (W3C / CSS WG). You write border-radius: 40px, and you get a circular arc, because the spec says so. You write backdrop-filter: blur(10px), and you get a Gaussian blur on the spec’s terms. You can’t extend that math from your stylesheet; browsers implement one interoperable definition.


How Figma draws outside the DOM

Figma's High-Performance Rendering Pipeline BROWSER ENVIRONMENT (Chrome / Safari / Firefox) User Interaction Click, Drag, Zoom C++ / Rust (WebAssembly) " Internal Document Model " Scene Graph Updates " Vector Boolean Operations NO DOM / NO CSS WebGL / WebGPU Raw GPU Instructions Custom Shaders (GLSL/WGSL) GPU Rasterization Pixels on Screen Why this wins: " 60FPS Performance " Massive Layer Counts " Browser Independence " Custom Visual Effects
Diagram of Figma's high-performance rendering pipeline: user interaction, C++ and Rust in WebAssembly without DOM or CSS, WebGL or WebGPU with custom shaders, and GPU rasterization.

Figma made a different bet when it launched in 2015: the canvas is not HTML and CSS.

C++ / Rust core

Figma maintains its own document model and scene graph: a tree of layers, effects, and constraints. When you nudge a frame or tweak corner smoothing, that graph updates in a compiled native/WASM layer, not in ad-hoc DOM updates from your UI JavaScript.

WebAssembly

The renderer runs in the browser at near-native speed. That’s how huge files with thousands of layers stay usable. The heavy work isn’t “the browser laying out divs,” it’s Figma’s code inside WASM.

WebGL / WebGPU

Figma walks its scene graph and issues raw GPU draw calls. Effects are often custom shaders (GLSL / WGSL): the math for corner smoothing, glass, blend modes, etc. is whatever Figma ships, not “whatever all browsers agreed to implement.”

Screen

The GPU composites and displays the canvas. Chrome is not rendering Figma’s pixels. It’s hosting a surface where Figma’s engine draws.

Bottom line: Figma can ship a new visual primitive by writing shader + scene-graph logic and releasing. No multi-year standards track. No waiting for Safari and Firefox to match. That freedom is exactly why mocks can outpace CSS.


Where design-to-code breaks: property by property

Once you see both pipelines, mismatches stop feeling mysterious.

cornerSmoothing (Figma) vs border-radius

Figma can use a superellipse-style curve, where curvature ramps smoothly into the corner instead of a hard hand-off. CSS border-radius uses circular arcs; you can get a subtle “kink” at the join. (Apple’s app-icon shape (is the famous real-world parallel).) There is W3C interest (corner-shape, etc.), but nothing you can rely on in production everywhere yet.

Workaround: SVG squircle paths, clip-path, or tools like figma-squircle, and accept some authoring cost.

Stroke alignment vs border

In Figma, inside / center / outside stroke keeps geometry predictable relative to the frame. With box-sizing: border-box, a CSS border lives inside the box you sized. It isn’t a free-floating “outside stroke” the same way. A 2px inside stroke in Figma is not identical to border: 2px solid.

Workaround: box-shadow / inset rings, extra wrappers, or explicit dimensions that include the border math you need.

Glass vs backdrop-filter

Figma glass is often a physically inspired stack (refraction, highlights, depth cues) implemented in shaders. CSS backdrop-filter: blur() is a Gaussian-style blur (and friends) on underlying pixels: powerful, but not the same light model.

Workaround: layered backdrop-filter, shadows, gradients, and aim for convincing, not pixel-perfect.

Multiple fills

Figma stacks many fills on one layer, each with opacity and blend mode. In CSS you mostly compose background layers, with background-color at the bottom of that stack, so the model is flatter.

Workaround: extra DOM (::before, ::after, stacked children) to fake additional “fills.”

Negative gap (Auto Layout) vs CSS gap

Figma Auto Layout can use negative gap for deliberate overlap (avatars, stacks). CSS gap cannot be negative.

Workaround: negative margins on children. That works, but you’re outside the same mental model as Auto Layout; test breakpoints carefully.

Plus Lighter vs mix-blend-mode

Figma’s Plus Lighter is not a 1:1 alias of a single CSS mix-blend-mode. The compositing math is Figma’s.

Workaround: approximate with something like mix-blend-mode: screen and tune opacity by eye.


Why the gap is structural (specs vs product shaders)

CSS is a shared contract. New visual power means proposals, implementer consensus, shipping in multiple engines, and years of iteration. That friction is why your stylesheet behaves predictably on phones, kiosks, and four different browsers.

Figma is a product renderer. New visuals ship when the team writes the shader and merges the release.

Neither model is “wrong.” They’re optimized for different goals: interoperability vs. design-tool expressiveness. Pretending they’re equivalent on every frame is where handoff pain comes from.


Handoff checklist: sanity-check the file before CSS

The gap isn’t something you “fix” with more clever CSS alone. It’s architectural. It is manageable.

Before you write markup, skim the file for:

  • Corner smoothing above 0%? Plan for squircle/clip-path workarounds, not vanilla border-radius alone.
  • Outside / center stroke? Don’t assume border is a drop-in.
  • Glass / heavy blur stacks? Expect backdrop-filter to be a rough match, not a clone.
  • Multiple fills on one layer? Plan extra elements.
  • Negative Auto Layout gap? Plan a margin-based layout, not pure gap.

Have the conversation early: not “impossible,” but “native on the web / workaround, or intentional approximation.” That clarity saves hours on every project.

Figma features with no production CSS twin (today)

cornerSmoothing · stroke alignment models beyond border · full glassFill fidelity · unconstrained multipleFills on one node · negativeGap in flex/grid · some advanced layerBlur looks · plusLighterBlend

Some of this is moving (corner-shape, Houdini in Chrome in places). The gap is real now and will shrink slowly, and it’s worth tracking if you live in design systems.


Engineer’s breakdown of two pipelines, so your next build starts with the right expectations, not the wrong guilt.