Inline Styles in React: 55 Uses, Audited From a Tailwind Codebase
55 inline styles across 21 files in a Tailwind-only codebase — audited by hand, each one tied to a runtime value or a non-CSS renderer.
Inline styles in React take a plain JavaScript object — style={{ color: "red" }}, camelCased properties, no string CSS — and every guide agrees they're the wrong default: no pseudo-selectors, no media queries, worse caching than a class. This codebase is a working example of that advice held and broken on purpose: 92 components built almost entirely on Tailwind utility classes still contain 55 inline style={{...}} uses across 21 files — audited directly from the source tree, each one there for a reason a static class genuinely can't cover.
The count, and what it means in a Tailwind-only codebase
$ grep -rn "style={{" src/ --include="*.tsx" | wc -l
55
$ grep -rln "style={{" src/ --include="*.tsx" | wc -l
21
55 occurrences across 21 files, in a codebase where the styling convention (documented in this project's own architecture notes) is Tailwind utility classes for everything else. That's not an inconsistency — it's the signal worth reading: inline style shows up only where a value can't be known until runtime, or where the rendering environment doesn't understand CSS classes at all. Three real examples cover both cases.
Case 1: a value no fixed class can express
TemplateGallery.tsx slides its carousel track by however many slides the visitor has scrolled past — a number that doesn't exist until the component renders:
// src/components/molecules/TemplateGallery.tsx
<div
className="flex transition-transform duration-500 ease-out motion-reduce:transition-none"
ref={trackRef}
style={{ transform: `translateX(-${index * 100}%)` }}
/>
className still carries everything static — the transition, the easing, the reduced-motion fallback. Only the actual translate percentage, which depends on index (current slide) at render time, goes in style. A Tailwind class is a fixed string decided at build time; index * 100 isn't known until a user clicks. There's no version of this that a static utility class could express, with or without Tailwind's arbitrary-value syntax (translate-x-[n%] still can't take a JS expression).
Case 2: a CSS feature Tailwind's utility set doesn't cover
Carousel.tsx fades the edges of its marquee with a CSS mask — a real property, but not one Tailwind ships a utility for:
// src/components/organisms/Carousel.tsx
<div
className="group relative flex w-full overflow-hidden py-8"
style={{
maskImage:
"linear-gradient(to right, transparent, black 8%, black 92%, transparent)",
}}
>
This isn't a dynamic value — the gradient stops are fixed. It's inline because mask-image has no Tailwind utility class at all (unlike background-image, which does). When a utility doesn't exist for a static property, inline style is the same fallback the language always had, not a workaround Tailwind introduced.
Case 3: the renderer doesn't run CSS at all
app/opengraph-image.tsx generates this site's social preview image, and its entire layout is inline styles — not a stylistic choice, a hard constraint of what's rendering it:
// src/app/opengraph-image.tsx
return new ImageResponse(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
position: "relative",
alignItems: "center",
background: "linear-gradient(135deg, #465fff 0%, #2a31d8 55%, #161950 100%)",
color: "#ffffff",
fontFamily: "Outfit",
overflow: "hidden",
}}
>
next/og's ImageResponse renders JSX through Satori, which converts a flexbox-and-a-CSS-subset layout into an image at request/build time — it never loads a stylesheet, never resolves a className, and doesn't know Tailwind exists. Every visual property here has to be a style object because there is no other mechanism available. This is the clearest case in the codebase: it isn't "inline style was easier," it's "inline style is the only API Satori accepts."
The pattern across all 21 files
Pulling the cases together, most of the 21 files fall into the same two buckets: components computing a genuinely runtime value (TemplateGallery's slide offset, PreviewModal's device-frame width — frameWidth * scale, both unknowable until render) and files rendered outside the normal DOM+stylesheet pipeline (opengraph-image.tsx, blog/[slug]/opengraph-image.tsx, global-error.tsx — the last one deliberately styled without depending on the app's own CSS bundle, since a global error boundary can't assume anything else loaded correctly).
A third reason recurs often enough to be a convention, not an exception — and it's the honest counterexample to "inline style only when a class can't do it," since text-transparent exists as a Tailwind utility:
// src/components/molecules/AvatarGroup.tsx
<img
className={imgClassName}
style={{ color: "transparent" }}
// ...
/>
$ grep -rn 'color: "transparent"' src/ --include="*.tsx" | wc -l
12
Twelve occurrences, not one — the same line shows up on every lazy-loaded <img> whose visible className is a prop the caller supplies (AvatarGroup, FeatureTabs' four feature screenshots, and others). Nothing guarantees a caller-supplied imgClassName includes text-transparent, so the property that must never be skipped — no visible alt-text glyph if an image fails to load — is pinned inline instead, immune to whatever className a given call site passes. That's a real third category: not "no class exists," but "this specific property must not depend on a prop that could change," applied consistently everywhere the same risk exists.
A fourth, smaller pattern lives in techStack.tsx, where each of the seven framework-edition icons gets its own brand-color gradient and matching drop-shadow, sourced straight from that item's data:
// src/data/techStack.tsx
<span
className="flex aspect-square w-12 items-center justify-center rounded-full"
style={{
backgroundImage: "linear-gradient(180deg, #E64C18 0%, #FF7A4D 100%)",
filter: "drop-shadow(0px 16px 42px rgba(230, 76, 24, 0.24))",
}}
>
Seven icons, seven unique two-stop gradients plus a matching shadow color — every one different, all fixed once at authoring time. Tailwind's arbitrary-value syntax could technically hold a literal gradient string, but at that length it stops reading like a utility class at all; keeping the value as a plain object next to the icon it belongs to is the more maintainable version of the same static-value case as Carousel's mask gradient, just repeated per data-array item instead of once.
Troubleshooting: telling a justified inline style from a lazy one
| Symptom | Cause | Fix |
|---|---|---|
| An inline style repeats the same fixed values on every render | The value never changes — it was written inline out of convenience, not necessity | Move it to a Tailwind class or a CSS custom property; reserve style for values that actually vary |
A dynamic value is hard-coded into a className template string | Trying to force a runtime value into Tailwind's arbitrary-value syntax (className={`w-[${x}px]`}) | Tailwind can't see a runtime-interpolated class at build time and won't generate the CSS for it — use style instead |
| Styles look right in the browser but the OG image comes out unstyled | Assuming next/og's ImageResponse resolves className/Tailwind the way the browser does | It doesn't — Satori only understands inline style objects and a CSS subset; rewrite the layout in inline styles |
| A "one-off" inline style shows up in five different components | No shared token for a value that recurs | Promote it to a Tailwind @theme token or a shared constant instead of repeating the inline object |
Frequently asked questions
Is using inline styles in React always bad practice? No — it's bad practice as a default, which is why guides warn against it. It's the right tool specifically for runtime-computed values (an index-based transform) and for rendering contexts that don't process CSS classes at all (Satori-based image generation). Both cases are real in this codebase, and neither is a shortcut around Tailwind.
Why not just use Tailwind's arbitrary-value classes (translate-x-[10%]) instead of inline style?
Arbitrary-value classes still need a value Tailwind can see at build time or extract statically from your source. A JS expression like translateX(-${index * 100}%) is a runtime value with unbounded possible outputs — Tailwind has nothing to generate a class for until the number exists, which is after the page has already built.
Does next/og's ImageResponse support Tailwind CSS at all?
No. Satori (the engine behind ImageResponse) parses a constrained set of inline CSS properties on style objects — no stylesheet loading, no class resolution, no Tailwind. Every OG-image route in this codebase is written in inline styles for exactly that reason.
How many inline styles is "too many" in a Tailwind codebase? There's no fixed number, but the ratio matters: 55 occurrences across 21 of roughly 92 components (23%) — and every one traceable to a runtime value or a non-CSS renderer — reads very differently than the same count spread across components that just skipped writing a utility class.
Templates in this post
ASoc Nova (a crypto-trading platform landing page), ASoc Pip (a forex-trading marketing site) and ASoc Press (a news & magazine template) are all built on the same Tailwind-first, inline-style-only-when-necessary convention audited above.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the OG-image pipeline this post's third case comes from, see Next.js Open Graph Image Generation; for the broader utility-vs-hand-written-CSS tradeoff, see Tailwind vs. CSS.
