"How do you organise CSS in a large app" has one honest answer in 2026: decide who owns each style. Component-scoped styles own their component; utilities own nothing and compose; tokens own the design decisions. Tailwind v4 and Nuxt UI made this the default stack for Nuxt teams, and the interview question is whether you can explain the trade-offs rather than repeat the marketing.
<style scoped>, a hashed attribute; :deep(), :slotted(), :global()) own one component; CSS Modules (<style module>, $style) do the same with class hashing; utility-first (Tailwind) puts styling in markup and relies on a fixed vocabulary; global layered CSS (@layer) owns resets, tokens and typography. Most codebases combine tokens + utilities + a few scoped rules.tailwind.config.js; @import "tailwindcss" then @theme { --color-brand: oklch(…); --font-sans: … } defines tokens as CSS variables that generate utilities (bg-brand, font-sans); @source registers extra content paths (a layer's components, a UI library), @utility defines custom utilities, @variant/@custom-variant custom variants (dark as a class variant), @plugin loads legacy plugins. The Oxide engine (Rust) scans files and generates only used classes; the Vite plugin (@tailwindcss/vite) replaces PostCSS in most setups. Tailwind 4.3 is current.--color-blue-600), semantic tokens (--ui-primary, --ui-bg, --ui-text), component tokens. In Nuxt UI, semantic colours are chosen in app.config.ts (ui.colors.primary = 'emerald') and exposed as --ui-* variables; components are styled with Tailwind Variants (tv()) slots and accept a ui prop and class to override. That is the theming model most Nuxt teams inherit.color-scheme + light-dark() for pure CSS, or a .dark class on <html> toggled by @nuxtjs/color-mode (which Nuxt UI uses) with @custom-variant dark (&:where(.dark, .dark *)); store the preference in a cookie so the server renders the right theme without a flash; multi-brand theming swaps token values per tenant, not components.tv()/cva), class passthrough merges with tailwind-merge to resolve conflicts, ui slot overrides for internals, @apply only inside component CSS for repeated combinations (not to rebuild a class system).color-mix(). Know why, not just that.@import chains at runtime; content-visibility and contain for long pages; purge is automatic with Tailwind's scanner, but dynamic class names built by string concatenation are invisible to it.eslint-plugin-tailwindcss or the Tailwind IntelliSense extension for class order and unknown classes, tokens documented in one place (a tokens.css or the design-system docs).Tailwind v4 CSS-first setup for a Nuxt app with a base layer:
@import "tailwindcss";
@import "@nuxt/ui";
/* Components that live outside this layer's app/ must be registered for scanning */
@source "../../../../app";
@source "../../../../layers/**/app";
@theme {
--font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
--color-brand-50: oklch(97% 0.02 260);
--color-brand-500: oklch(62% 0.19 260);
--color-brand-900: oklch(30% 0.10 260);
--radius-card: 0.75rem;
--breakpoint-3xl: 120rem;
}
@custom-variant dark (&:where(.dark, .dark *));
@utility scrollbar-thin {
scrollbar-width: thin;
}
@layer components {
.prose-tight :where(p) { margin-block: 0.5em; }
}
export default defineAppConfig({
ui: {
colors: { primary: 'brand', neutral: 'zinc' }, // semantic -> primitive; Nuxt UI exposes --ui-primary
button: { slots: { base: 'font-medium' }, defaultVariants: { size: 'md' } },
},
})
A component that composes variants and stays overridable:
<script setup lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
const tag = tv({
base: 'inline-flex items-center gap-1 rounded-card px-2 py-0.5 text-xs font-medium',
variants: {
tone: {
neutral: 'bg-neutral-100 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-100',
brand: 'bg-brand-50 text-brand-900 dark:bg-brand-900 dark:text-brand-50',
},
size: { sm: 'text-[0.6875rem]', md: '' },
},
defaultVariants: { tone: 'neutral', size: 'md' },
})
type Props = VariantProps<typeof tag> & { class?: string }
const { tone, size, class: className } = defineProps<Props>()
</script>
<template>
<span :class="tag({ tone, size, class: className })"><slot /></span>
</template>
Scoped styles for what utilities express badly, with tokens:
<style scoped>
.timeline {
--line: color-mix(in oklch, var(--ui-primary) 40%, transparent);
display: grid;
grid-template-columns: 1.5rem 1fr;
:deep(.entry + .entry)::before { content: ''; border-inline-start: 2px solid var(--line); }
}
</style>
:class="`text-${tone}-600`" produces classes the scanner never sees, so they are not generated and the style silently fails in production. Map variants to full class strings (a tv() variants object) or safelist explicitly.
<style scoped> cannot reach inside a child component past its root without :deep(), and Nuxt UI components expect overrides through class and the ui prop, not descendant selectors. Fighting a library's internals with :deep() is a maintenance trap; use the override API it provides.
Tailwind's current minor (4.3 as of September 2026) and the exact directive names (@theme, @source, @utility, @custom-variant, @plugin); also the Nuxt UI theming keys in app.config.
@theme with oklch() and generate a component using bg-brand-500; then swap the palette for a second tenant by changing only the tokens.TeamTag with tv() and try overriding padding from a parent through class; confirm tailwind-merge resolves the conflict."By ownership. Design decisions live in tokens, custom properties defined once in Tailwind v4's @theme and mapped to semantic names, so a theme or a tenant changes by swapping values. Component styling is utility-first with variants encoded in a tv() object so every allowed combination is a full class string the scanner can see, with class passthrough merged by tailwind-merge and a ui slot API for internals, which is the Nuxt UI model. The few things utilities express badly, complex pseudo-element layouts or third-party markup, get scoped SFC styles using the same tokens. Global CSS is limited to resets and typography inside cascade layers. And I lint it: Stylelint for hand-written CSS, the Tailwind plugin for unknown classes and order."
"It became CSS-first. Configuration moved from tailwind.config.js into the stylesheet: @theme defines tokens as native custom properties that generate utilities, @source points the scanner at extra directories such as a layer's components, @utility and @custom-variant extend the vocabulary, and the Rust-based engine generates only the classes it finds. Practically that means tokens are real CSS variables I can read in scoped styles and color-mix(), dark mode is a custom variant on a class, a Vite plugin replaces the PostCSS setup, and the old JavaScript config, @apply-heavy component classes and plugin ecosystems are legacy paths I can explain but do not reach for."
Nothing yet. Notes filed here show up on this page and the section overview.
Cascade, layout and modern CSS
Specificity and cascade layers, custom properties and logical properties, flexbox versus grid and subgrid, container queries, :has(), nesting and @scope, modern colour, view transitions, anchor positioning, popover and dialog, and the Baseline vocabulary for support.
Responsive and adaptive design
Mobile-first with fluid type and spacing, container queries over media queries for components, viewport units and safe areas, touch targets and pointer queries, user preference queries for motion, contrast and colour scheme, images that do not shift layout, and print.