Skip to main content
ASoc
Comparison

Sass vs. Tailwind: 133 Lines and One Arbitrary Selector

Sass's four features, checked one at a time against this codebase's real @theme block, group-hover usage, and the single [&_selector] Tailwind still reaches for.

The ASoc Team7 min read

Sass adds variables, nesting, mixins and file splitting on top of CSS you still write by hand; Tailwind replaces hand-written CSS with utility classes compiled from a token set, so most of what Sass is for stops being a question. This storefront ships zero .scss files and one 133-line stylesheet. Here is where each of Sass's four features actually goes, checked against real code, and the one place the utility model still reaches for something Sass would have given for free.

The comparison at a glance

SassTailwind CSS v4 (this repo)
Variables$variable, scoped by file@theme tokens, real CSS custom properties
NestingNative, any depthNone — composed via group/peer variants, or an arbitrary [&_selector] escape hatch
Mixins@mixin / @include@apply (used twice in this codebase) or a component function
File splitting@use partials, one concept per fileNot needed at 133 lines; one globals.css
Build stepA compiler (sass, dart-sass, a bundler loader)A PostCSS plugin already required for Tailwind itself
OutputWhatever you wrote, verbatimOnly the classes actually referenced in markup
Runtime costNone — compiles to plain CSSNone — compiles to plain CSS

The last row is why this isn't the Chakra UI question. Styling React components covers the axis that actually matters for a CSS-in-JS library — whether it needs a Client Component boundary. Sass and Tailwind both compile away before the browser sees anything; the real difference is what each one asks a human to author.

Variables: $brand vs @theme

A Sass variable is a compile-time constant scoped to whichever files @use it:

// hypothetical: styles/_tokens.scss
$color-primary: #465fff;
$color-primary-600: #3641f5;

// styles/button.scss
@use "tokens" as *;
.btn-primary { background: $color-primary; }

This repo's equivalent is @theme in src/app/globals.css, and it does one more thing than a Sass variable can: it emits a real CSS custom property and the utility that reads it, from the same declaration.

/* src/app/globals.css */
@theme {
  --color-primary: #465fff;
  --color-primary-600: #3641f5;
  /* … 41 tokens total, audited in Tailwind design tokens */
}

bg-primary and text-primary-600 exist because that block exists — no separate class definition, and no @use import at any call site. The token audit (41 declared, against Tailwind's 419 shipped defaults) is its own post: Tailwind design tokens.

Nesting: eleven group-hover call sites, and one real gap

Sass nesting writes the parent–child relationship where you'd read it:

// hypothetical
.card {
  &:hover .icon { opacity: 1; }
}

Tailwind has no nesting primitive, because a utility class styles the element it's on, not a descendant. The compositional answer is group/group-hover, used in 11 component files in this codebase — the parent gets group, the child reaches up with group-hover:opacity-100. It reads correctly in the markup: the relationship lives at the two elements it actually connects, not in a third stylesheet.

// TemplateCard.tsx — the hover preview overlay
<div className="group/media relative">
  <div className="invisible opacity-0 transition group-hover/media:visible group-hover/media:opacity-100">

That covers "style this element when its ancestor is hovered." It does not cover "style whatever the CMS put inside this box," and this codebase has exactly one place that needs it: legal prose rendered from MDX.

// src/components/templates/LegalTemplate.tsx
<div className="mt-8 space-y-6 text-base leading-7 text-text-color dark:text-white/80
     [&_h2]:mt-10 [&_h2]:text-xl [&_h2]:font-semibold [&_h2]:text-title-color
     [&_li]:ml-5 [&_li]:list-disc [&_table]:w-full [&_table]:text-sm">

/terms, /privacy and /refund-policy render Markdown-shaped content whose <h2>, <li> and <table> tags this component never touches directly — there's nothing to put a class on. [&_h2]:mt-10 is Tailwind's arbitrary-variant escape hatch, and it is doing exactly what a Sass .legal-prose h2 { margin-top: 2.5rem; } nested rule would do. One selector, in one file, is the entire nesting gap this repo has ever hit. Every other parent–child relationship in 91 components is expressible as group/peer, because every other one is between elements this codebase authored, not markup it received.

Mixins: @apply, used exactly twice

A Sass mixin captures a reusable declaration block:

// hypothetical
@mixin base-body { background: white; font: inherit; color: #344054; }
body { @include base-body; }

Tailwind's @apply is the direct equivalent, and this codebase's entire usage is the base body style:

/* src/app/globals.css */
@layer base {
  body {
    @apply bg-white font-sans text-base font-normal text-gray-700;
  }
  .dark body {
    @apply bg-gray-900 text-gray-300;
  }
}

Two rules, both about the one element every page shares. Nothing else in the stylesheet reaches for @apply, because a mixin's usual job — a reusable declaration block applied at several call sites — is instead a reusable component: instead of @include button-primary on three different class names, there's one <Button variant="primary"> atom. The unit of reuse moved from a CSS block to a component prop, which sidesteps needing the CSS-level tool at all.

File splitting: 133 lines don't need @use

Sass earns @use and partials at scale — a design system with hundreds of rules genuinely needs _colors.scss, _typography.scss, _mixins.scss split apart and composed. This repo's entire hand-written stylesheet is 133 lines: a @theme token block, one responsive .container rule, a dark-mode @custom-variant, one @keyframes, and a two-selector utility layer for hiding scrollbars. There's nothing to split. If the token block grew to the size that made one file unwieldy, @theme supports the same @import composition Sass partials do — the ceiling exists, this codebase is nowhere near it.

The compiled output

Both approaches produce plain CSS with no runtime cost — the meaningful difference is what ships, not how it executes. A production build of this site (npm run build) emits one CSS file for the entire 111-product catalog, all eight core routes, and every component that renders on any of them:

.next/static/chunks/2cdc99m3g_kds.css   75,791 bytes raw / 13,662 bytes gzipped

That's the whole site's styling, not a per-page slice — Tailwind's compiler walks every className string that's actually referenced and emits only those utilities. A Sass build has no equivalent step: it compiles whatever selectors you wrote, whether or not any markup still uses them, which is why Sass codebases accumulate a separate dead-CSS-detection tool (PurgeCSS, historically bolted onto Tailwind v2 for the same reason v3+ don't need it).

Troubleshooting

SymptomCauseFix
A new bg-primary utility doesn't exist after adding a tokenTailwind v4 + Turbopack doesn't hot-reload @theme editsRestart npm run dev — nothing errors, the class just never generates
@apply on a class that doesn't exist yetThe referenced utility isn't in the token set or core TailwindCheck @theme first; @apply can't invent a utility, only reference one
An [&_h2] rule doesn't fireThe descendant selector's specificity lost to a more specific class already on the elementArbitrary variants compile to a plain CSS selector — normal cascade rules still apply, unlike a Sass @extend
Reaching for @mixin-style duplication across five componentsThe instinct is right, the tool is wrong hereExtract a component, not a class — see Styling React components for the Server Component constraint that makes this the correct default
A Sass migration guide says "just replace $var with a CSS custom property"True for the variable, not for nesting or mixinsOnly Tailwind's @theme gives you the class and the property from one declaration — a bare :root custom property doesn't

Frequently asked questions

Can Sass and Tailwind be used together? Yes — Tailwind's own docs don't forbid it, and some teams run Sass for globals/animations while Tailwind handles component classes. This repo doesn't, because there was never a rule that needed a preprocessor: one @theme block replaced the variables, group/peer replaced the nesting, and @apply covers the two spots that wanted a mixin.

Does Tailwind CSS need a build step like Sass does? Yes, but it's one you already have. Tailwind compiles through PostCSS (@tailwindcss/postcss in this repo's postcss.config.mjs), which Next.js runs as part of next build regardless. Adding Sass would mean a second, separate compiler in the pipeline; Tailwind's is the one already there.

What does Tailwind lose without nesting? Almost nothing for markup you author yourself — group/peer covers ancestor-triggered styling, and co-locating the class on the element it affects is arguably clearer than a nested rule three levels deep in a .scss file. The real gap is markup you don't author: this codebase's one [&_selector] case is legal prose from Markdown, where there's no JSX to attach a class to.

Is Sass still worth learning if a project uses Tailwind? For maintaining an existing Sass codebase, yes. For starting a new one, the honest comparison is: Sass gives you better hand-written CSS, and Tailwind gives you a reason to write less of it by hand in the first place. Whether that trade is worth it depends on how much of the styling is one-off versus how much is a design system with a token set — the second case is what @theme was built for.

Templates in this post

ASoc Remit, a payments-platform landing page, ships the same token-driven @theme setup as this site — a brand palette and a marketing layout with zero .scss anywhere in its build. ASoc Script, an AI copywriting landing page, and ASoc Seeker, an AI keyword-research landing page, both lean on group-based hover interaction for their feature sections rather than nested selectors.

Browse the full sets: Next.js landing page templates and Tailwind landing page templates.

Keep reading

Comparison8 min read

shadcn vs. Tailwind Is a Category Error (One Runs on the Other)

92 components, 13 runtime dependencies, zero UI libraries — what hand-rolling actually cost this codebase, and the seven components shadcn would have handed over.

Read more
Comparison11 min read

Shopify vs a Next.js Storefront: What You Actually Inherit

Shopify's fee buys a checkout, tax handling and an ops backend — not hosting. The four jobs you take on by leaving, the headless hybrid, and when owning the frontend pays.

Read more