shadcn/ui vs. Emotion: Zero-Runtime CSS vs. CSS-in-JS
Zero styling runtime vs. a render-time serializer, measured against 92 hand-built components, a 24/92 client-boundary ratio, and a CSP line Emotion wouldn't need.
shadcn/ui ships zero styling runtime — it copies Tailwind-classed .tsx files into your repo, so the CSS is static at build time. Emotion is the opposite bet: components hold their styles as JS, and a runtime library serializes them into <style> tags in the browser on every render. shadcn (and any Tailwind codebase) has no Emotion-shaped cost to measure, because there is no serialization step to run.
This storefront made the zero-runtime bet: 92 components, 13 runtime dependencies, and not one of them a styling library. Here is what that actually means next to what Emotion buys.
The two things being compared
| shadcn/ui (on Tailwind) | Emotion | |
|---|---|---|
| What it is | Components copied into your repo, styled with Tailwind utility classes | A CSS-in-JS library: css/styled write styles as JavaScript |
| When styles become CSS | Build time — Tailwind's compiler generates static classes | Render time — a serializer runs in the browser (or SSR) and injects <style> |
| Ships as | Your own source, tree-shaken like any other code | A runtime dependency in every bundle that imports it |
| Theming | CSS custom properties in a @theme block | A JS theme object passed through a ThemeProvider |
| Who uses it directly | Anyone adopting Tailwind + the shadcn CLI | MUI and Chakra UI both use Emotion under the hood as their styling engine |
| Server Components | Presentational components render fine; interactive ones carry "use client" | Needs a client boundary — the serializer runs at render time, which the RSC render doesn't do the same way |
Emotion isn't usually chosen on its own for a green-field app today — it's what you get for free (and pay for) by picking MUI or Chakra, both of which use it as their styling engine. That's also why the real comparison here isn't "install shadcn or install Emotion" — it's "own your styles as static Tailwind classes, or accept a runtime serializer as the price of a batteries-included component library."
What "zero-runtime" measures, concretely
Everything in src/components here is Tailwind-classed source, hand-written or copied, never a CSS-in-JS template literal. The census, straight from the tree:
| Layer | Components | Client Components |
|---|---|---|
| Atoms | 7 | 0 |
| Molecules | 41 | 19 |
| Organisms | 33 | 5 |
| Templates | 11 | 0 |
| Total | 92 | 24 |
And the styling surface backing all of it: 38 colour tokens in a 133-line globals.css, compiled once by Tailwind's @theme block, plus utility classes in the markup. No @emotion/react, @emotion/styled, class-variance-authority, or tailwind-merge anywhere in the dependency list — the complete runtime dependencies block is 13 entries, none of them a styling engine:
@mdx-js/loader, @mdx-js/react, @next/mdx, @supabase/ssr,
@supabase/supabase-js, @vercel/analytics, lucide-react, next,
react, react-dom, rehype-slug, remark-gfm, server-only
Here's the actual atom this site's Button renders from — real code, not written for this post:
// src/components/atoms/Button.tsx
const base =
"inline-flex items-center justify-center gap-2 rounded-lg px-6 py-3 text-base font-medium shadow-xs duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2";
const variants: Record<ButtonVariant, string> = {
primary: "bg-primary text-white hover:bg-primary-600",
outline:
"border border-stroke-tertiary bg-white text-text-color hover:bg-gray-50 hover:text-gray-800",
dark: "bg-gray-900 text-white hover:bg-gray-800",
};
const classes = `${base} ${variants[variant]} ${className}`.trim();
That's the entire styling mechanism: three template-literal string concatenations, resolved before the component ever renders. An Emotion equivalent — styled.a with a variant prop branching inside a template literal, or css calls composed at render time — does the same visual job, but the class names and the underlying CSS rules don't exist until the serializer runs. This atom needs no serializer because Tailwind already turned every one of those class names into CSS during the build.
The bundle floor this buys — and doesn't
react-dom itself is this app's unavoidable floor: 221 KiB raw / 70 KiB gzipped, measured against this site's own production build (full breakdown in Astro vs. Next.js). Every styling engine adds to that floor before a single button renders. MUI's core plus Emotion routinely lands 80–120 KiB gzipped once a real app's usage is tree-shaken — that number comes from auditing this same codebase's dependency choices in React UI libraries. Tailwind's utility classes add nothing to that JS floor at all: the cost moved entirely into the CSS file, which browsers parse without executing.
The number that surprises people going the other direction: this site's Content-Security-Policy already carries style-src 'self' 'unsafe-inline' — required because Tailwind itself emits inline <style> in a couple of spots and Next.js injects an inline bootstrap script (see next.config.ts's own comment on the tradeoff). That means if this codebase adopted Emotion tomorrow, its runtime-injected <style> tags would need no CSP change at all — the directive commonly assumed to be Emotion's specific tax is already open here, for an unrelated reason. The real cost of Emotion was never the CSP line; it's the render-time serialization and the client-boundary requirement.
Where each one actually wins
- Zero-runtime (shadcn/Tailwind) wins when most of the app is presentational and Server-Component-friendly. Here,
24of92components need"use client", and zero atoms do — every primitive renders on the server, something Emotion's serializer can't do the same way, since the styling computation has to happen somewhere a<style>tag can be injected. - Emotion wins when you're not choosing it directly but adopting MUI or Chakra for their component coverage, and the styling engine comes bundled with that decision. Nobody picks Emotion first and a component library second in practice.
- Neither wins if the real question is "do I need a component library at all" — see shadcn vs. Tailwind for that fork, one level up from this one.
Hydration and Core Web Vitals
Zero-runtime CSS has one more effect that rarely gets named directly: it removes a whole category of hydration mismatch. Emotion's SSR path has to inject the exact same serialized styles the client will compute, or React's hydration warns about a mismatch; get the extraction step wrong (a missing CacheProvider, a version skew between server and client Emotion) and the failure mode is either a flash of unstyled content or a hydration error in the console. A static Tailwind class has no such step — the class name in the server-rendered HTML and the class name Tailwind's compiler generated are the same string, generated once, at build time, before either server or client exist.
That's not a performance argument so much as a failure-surface argument: shadcn/Tailwind has one less synchronization point that has to agree between two runtimes. This site's own Lighthouse numbers — desktop performance 100 on all eight measured pages, mobile 89–99 — are partly a product of having no client-side styling computation to account for at all; the full Lighthouse methodology is here for the accessibility half of that same audit.
Mistakes and troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| "Do I install shadcn or Emotion?" | Category error — they solve different layers | shadcn needs Tailwind; Emotion is what MUI/Chakra use underneath their own components |
| Styled-components/Emotion styles flash unstyled on first paint | The serializer runs client-side and SSR extraction wasn't wired up | Configure the framework's SSR style-extraction step, or move to build-time CSS |
| A Server Component using an Emotion-styled import throws | styled() calls create client-only context (theme, cache) that RSC can't serialize | Push the styled component behind a "use client" boundary |
| Tailwind classes inside a component copied from shadcn don't apply | Tailwind isn't scanning that file's path | Check the content/glob sources in your Tailwind config |
| Bundle size grew after adding a component library | The styling engine (Emotion) and its runtime ship with every import that uses it | Audit with a bundle analyzer; tree-shake unused parts of the library |
| CSP blocks Emotion's injected styles | style-src has no 'unsafe-inline' and no nonce wired to the serializer | Add a nonce-based CSP source matched to the library's SSR output, or accept 'unsafe-inline' as this site does |
Frequently asked questions
Is shadcn/ui styled with Emotion? No. shadcn/ui components are Tailwind-classed by default — plain utility classes compiled at build time, not CSS-in-JS.
Does using Emotion mean I can't use Server Components? Client-composed styled components need a client boundary, because the serializer computing the CSS runs where React renders them. Presentational markup with no styled-component wrapper can still be a Server Component.
Is Emotion slower than Tailwind? It does strictly more work — serializing style objects to CSS at render time instead of compiling once at build time — but the cost is usually invisible unless you're auditing bundle size or hydration cost specifically, which is why it rarely shows up until you count it, as this post does.
Do I need Emotion to use MUI or Chakra? Both ship with Emotion as their default styling engine. You can swap it (MUI supports a styled-components adapter), but removing a styling engine entirely means dropping the component library too.
Templates in this post
ASoc Amplify is a social-media-management SaaS marketing site built around a unified-workspace hero, a core-toolkit services grid, and a results-stats band. ASoc Atelier is a designer's portfolio and studio site with a four-case-study work grid, a five-service menu, and a booking funnel. ASoc Axiom is a dark-theme AI-consultancy landing page with a four-service grid, project-based pricing, and a four-question FAQ.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
