Fix: Vanilla Extract Not Working — Styles Not Applied, TypeScript Errors, or Build Failing
Quick Answer
How to fix Vanilla Extract issues — .css.ts file setup, style and recipe APIs, sprinkles for utility classes, theme tokens, dynamic styles, and integration with Next.js, Vite, and Remix.
The Problem
Styles defined in a .css.ts file don’t appear on the page:
// styles.css.ts
import { style } from '@vanilla-extract/css';
export const button = style({
backgroundColor: 'blue',
color: 'white',
padding: '12px 24px',
});// Button.tsx
import { button } from './styles.css';
function Button() {
return <button className={button}>Click me</button>;
}
// Button renders but has no stylesOr the import fails:
Module not found: Can't resolve './styles.css'Or TypeScript throws errors on style definitions:
Type '{ backgroundColor: string; }' is not assignable to type 'StyleRule'Why This Happens
Vanilla Extract processes .css.ts files at build time and outputs standard CSS. The style functions are replaced by class name strings at runtime:
.css.tsfiles are mandatory — only files with the.css.tsextension are processed by the Vanilla Extract compiler. Writing styles in a regular.tsfile doesn’t work because the build plugin doesn’t process it.- The bundler plugin must be installed — Vanilla Extract requires a framework-specific plugin (Vite, webpack, esbuild, Next.js). Without it,
.css.tsimports fail because the bundler doesn’t know how to handle them. - Styles are evaluated at build time — everything in a
.css.tsfile runs during the build, not in the browser. You can’t reference runtime values (React state, props) insidestyle(). Dynamic styling requiresstyleVariants, recipes, orassignInlineVars. - Import paths use
.cssnot.css.ts— when importing in your component, useimport { button } from './styles.css'(without.ts). The build plugin handles the resolution.
Fix 1: Set Up Vanilla Extract
npm install @vanilla-extract/css
# Framework plugins — install ONE
npm install -D @vanilla-extract/vite-plugin # Vite
npm install -D @vanilla-extract/next-plugin # Next.js
npm install -D @vanilla-extract/webpack-plugin # Webpack
npm install -D @vanilla-extract/esbuild-plugin # esbuildVite:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
export default defineConfig({
plugins: [react(), vanillaExtractPlugin()],
});Next.js:
// next.config.mjs
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin';
const withVanillaExtract = createVanillaExtractPlugin();
export default withVanillaExtract({
// Next.js config
});// styles/button.css.ts — define styles
import { style, globalStyle } from '@vanilla-extract/css';
export const container = style({
maxWidth: '1200px',
margin: '0 auto',
padding: '0 16px',
});
export const button = style({
backgroundColor: '#3b82f6',
color: 'white',
padding: '12px 24px',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
fontSize: '16px',
fontWeight: 600,
transition: 'background-color 0.2s',
':hover': {
backgroundColor: '#2563eb',
},
':active': {
backgroundColor: '#1d4ed8',
},
':disabled': {
opacity: 0.5,
cursor: 'not-allowed',
},
});
// Responsive styles — use @media
export const card = style({
padding: '16px',
'@media': {
'(min-width: 768px)': {
padding: '24px',
},
'(min-width: 1024px)': {
padding: '32px',
},
},
});
// Global styles (use sparingly)
globalStyle('html, body', {
margin: 0,
padding: 0,
fontFamily: 'Inter, sans-serif',
});
globalStyle(`${container} > *`, {
marginBottom: '16px',
});// Button.tsx — import with .css extension (not .css.ts)
import { button } from './styles/button.css';
function Button({ children }: { children: React.ReactNode }) {
return <button className={button}>{children}</button>;
}Fix 2: Variants with styleVariants and Recipes
// styles/button.css.ts
import { style, styleVariants } from '@vanilla-extract/css';
// Base style
const buttonBase = style({
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '8px',
fontWeight: 600,
border: 'none',
cursor: 'pointer',
transition: 'all 0.2s',
});
// styleVariants — map of variant name to additional styles
export const buttonVariant = styleVariants({
primary: [buttonBase, { backgroundColor: '#3b82f6', color: 'white' }],
secondary: [buttonBase, { backgroundColor: '#e5e7eb', color: '#374151' }],
danger: [buttonBase, { backgroundColor: '#ef4444', color: 'white' }],
ghost: [buttonBase, { backgroundColor: 'transparent', color: '#3b82f6' }],
});
export const buttonSize = styleVariants({
sm: { padding: '6px 12px', fontSize: '14px' },
md: { padding: '10px 20px', fontSize: '16px' },
lg: { padding: '14px 28px', fontSize: '18px' },
});// Button.tsx
import { buttonVariant, buttonSize } from './styles/button.css';
import clsx from 'clsx';
type ButtonProps = {
variant?: keyof typeof buttonVariant;
size?: keyof typeof buttonSize;
children: React.ReactNode;
};
function Button({ variant = 'primary', size = 'md', children }: ButtonProps) {
return (
<button className={clsx(buttonVariant[variant], buttonSize[size])}>
{children}
</button>
);
}Recipes — multi-variant component API (like CVA):
npm install @vanilla-extract/recipes// styles/button.css.ts
import { recipe, type RecipeVariants } from '@vanilla-extract/recipes';
export const button = recipe({
base: {
display: 'inline-flex',
alignItems: 'center',
borderRadius: '8px',
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.2s',
},
variants: {
variant: {
primary: { backgroundColor: '#3b82f6', color: 'white' },
secondary: { backgroundColor: '#e5e7eb', color: '#374151' },
ghost: { backgroundColor: 'transparent', color: '#3b82f6' },
},
size: {
sm: { padding: '6px 12px', fontSize: '14px' },
md: { padding: '10px 20px', fontSize: '16px' },
lg: { padding: '14px 28px', fontSize: '18px' },
},
rounded: {
true: { borderRadius: '9999px' },
},
},
compoundVariants: [
{
variants: { variant: 'primary', size: 'lg' },
style: { boxShadow: '0 4px 6px rgba(59, 130, 246, 0.3)' },
},
],
defaultVariants: {
variant: 'primary',
size: 'md',
},
});
export type ButtonVariants = RecipeVariants<typeof button>;// Button.tsx
import { button, type ButtonVariants } from './styles/button.css';
type ButtonProps = ButtonVariants & {
children: React.ReactNode;
};
function Button({ variant, size, rounded, children }: ButtonProps) {
return (
<button className={button({ variant, size, rounded })}>
{children}
</button>
);
}
// Usage
<Button variant="primary" size="lg" rounded>
Submit
</Button>Fix 3: Theme Tokens with createTheme
// styles/theme.css.ts
import { createTheme, createThemeContract } from '@vanilla-extract/css';
// Define the shape of your theme (contract)
export const vars = createThemeContract({
color: {
brand: null,
text: null,
textMuted: null,
bg: null,
bgSurface: null,
border: null,
},
space: {
sm: null,
md: null,
lg: null,
xl: null,
},
font: {
body: null,
heading: null,
},
radius: {
sm: null,
md: null,
lg: null,
},
});
// Light theme — fills in the contract values
export const lightTheme = createTheme(vars, {
color: {
brand: '#3b82f6',
text: '#111827',
textMuted: '#6b7280',
bg: '#ffffff',
bgSurface: '#f9fafb',
border: '#e5e7eb',
},
space: { sm: '8px', md: '16px', lg: '24px', xl: '48px' },
font: { body: 'Inter, sans-serif', heading: 'Cal Sans, sans-serif' },
radius: { sm: '4px', md: '8px', lg: '16px' },
});
// Dark theme — same contract, different values
export const darkTheme = createTheme(vars, {
color: {
brand: '#60a5fa',
text: '#f9fafb',
textMuted: '#9ca3af',
bg: '#111827',
bgSurface: '#1f2937',
border: '#374151',
},
space: { sm: '8px', md: '16px', lg: '24px', xl: '48px' },
font: { body: 'Inter, sans-serif', heading: 'Cal Sans, sans-serif' },
radius: { sm: '4px', md: '8px', lg: '16px' },
});// styles/components.css.ts — use theme tokens
import { style } from '@vanilla-extract/css';
import { vars } from './theme.css';
export const card = style({
backgroundColor: vars.color.bgSurface,
border: `1px solid ${vars.color.border}`,
borderRadius: vars.radius.md,
padding: vars.space.lg,
color: vars.color.text,
});
export const heading = style({
fontFamily: vars.font.heading,
color: vars.color.text,
marginBottom: vars.space.md,
});// App.tsx — apply theme class to root
import { lightTheme, darkTheme } from './styles/theme.css';
function App() {
const [isDark, setIsDark] = useState(false);
return (
<div className={isDark ? darkTheme : lightTheme}>
{/* All children use the theme tokens */}
</div>
);
}Fix 4: Sprinkles — Utility-First Styling
npm install @vanilla-extract/sprinkles// styles/sprinkles.css.ts
import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles';
const responsiveProperties = defineProperties({
conditions: {
mobile: {},
tablet: { '@media': '(min-width: 768px)' },
desktop: { '@media': '(min-width: 1024px)' },
},
defaultCondition: 'mobile',
properties: {
display: ['none', 'flex', 'block', 'grid', 'inline-flex'],
flexDirection: ['row', 'column'],
alignItems: ['stretch', 'center', 'flex-start', 'flex-end'],
justifyContent: ['flex-start', 'center', 'flex-end', 'space-between'],
gap: { 0: '0', 1: '4px', 2: '8px', 3: '12px', 4: '16px', 6: '24px', 8: '32px' },
padding: { 0: '0', 1: '4px', 2: '8px', 3: '12px', 4: '16px', 6: '24px', 8: '32px' },
margin: { 0: '0', auto: 'auto', 1: '4px', 2: '8px', 4: '16px' },
width: { full: '100%', auto: 'auto', '1/2': '50%', '1/3': '33.333%' },
fontSize: { sm: '14px', md: '16px', lg: '18px', xl: '20px', '2xl': '24px' },
fontWeight: { normal: '400', medium: '500', semibold: '600', bold: '700' },
borderRadius: { none: '0', sm: '4px', md: '8px', lg: '16px', full: '9999px' },
},
shorthands: {
p: ['padding'],
m: ['margin'],
w: ['width'],
},
});
const colorProperties = defineProperties({
conditions: {
light: {},
dark: { '@media': '(prefers-color-scheme: dark)' },
},
defaultCondition: 'light',
properties: {
color: {
text: '#111827',
muted: '#6b7280',
brand: '#3b82f6',
white: '#ffffff',
danger: '#ef4444',
},
backgroundColor: {
white: '#ffffff',
surface: '#f9fafb',
brand: '#3b82f6',
danger: '#ef4444',
transparent: 'transparent',
},
},
});
export const sprinkles = createSprinkles(responsiveProperties, colorProperties);
export type Sprinkles = Parameters<typeof sprinkles>[0];// Usage — Tailwind-like utility classes, but type-safe
import { sprinkles } from './styles/sprinkles.css';
function Header() {
return (
<header
className={sprinkles({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
p: 4,
backgroundColor: 'white',
})}
>
<h1 className={sprinkles({ fontSize: { mobile: 'lg', desktop: '2xl' }, fontWeight: 'bold' })}>
Logo
</h1>
</header>
);
}Fix 5: Dynamic Styles with CSS Variables
// styles/dynamic.css.ts
import { style, createVar, fallbackVar } from '@vanilla-extract/css';
// Define a CSS custom property
export const accentColor = createVar();
export const progressWidth = createVar();
export const progressBar = style({
height: '8px',
borderRadius: '4px',
backgroundColor: '#e5e7eb',
overflow: 'hidden',
'::after': {
content: '""',
display: 'block',
height: '100%',
width: progressWidth, // Dynamic value
backgroundColor: fallbackVar(accentColor, '#3b82f6'), // With fallback
transition: 'width 0.3s ease',
},
});// ProgressBar.tsx
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { progressBar, accentColor, progressWidth } from './styles/dynamic.css';
function ProgressBar({ percent, color }: { percent: number; color?: string }) {
return (
<div
className={progressBar}
style={assignInlineVars({
[progressWidth]: `${percent}%`,
[accentColor]: color ?? '#3b82f6',
})}
/>
);
}
// Usage — truly dynamic values at runtime
<ProgressBar percent={75} color="#10b981" />Still Not Working?
Cannot find module './styles.css' — check that you have the correct bundler plugin installed and configured. Vanilla Extract needs @vanilla-extract/vite-plugin for Vite, @vanilla-extract/next-plugin for Next.js, etc. Also verify the import path uses .css not .css.ts.
Styles exist but aren’t visible — Vanilla Extract generates class names but the CSS must be injected into the page. In Vite dev mode, this happens automatically via <style> tags. In production, CSS is extracted to files. Check that your bundler config doesn’t exclude .css files from processing.
TypeError: style is not a function at build time — your .css.ts file is being imported as a regular module instead of being processed by Vanilla Extract. This usually means the plugin isn’t running. Check plugin order in your config — some frameworks need the VE plugin before other plugins.
Styles bleed between components — Vanilla Extract scopes all style() calls by default (unique class names). If styles are leaking, you probably have globalStyle() calls that are too broad. Scope global styles by combining them with a parent selector: globalStyle(\${container} p`, { … })`.
For related CSS-in-JS issues, see Fix: styled-components Not Working and Fix: Tailwind CSS Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Mantine Not Working — Styles Not Loading, Theme Not Applying, or Components Broken After Upgrade
How to fix Mantine UI issues — MantineProvider setup, PostCSS configuration, theme customization, dark mode, form validation with useForm, and Next.js App Router integration.
Fix: Panda CSS Not Working — Styles Not Applying, Tokens Not Resolving, or Build Errors
How to fix Panda CSS issues — PostCSS setup, panda.config.ts token system, recipe and pattern definitions, conditional styles, responsive design, and integration with Next.js and Vite.
Fix: UnoCSS Not Working — Classes Not Generating, Presets Missing, or Attributify Mode Broken
How to fix UnoCSS issues — Vite plugin setup, preset configuration, attributify mode, icons preset, shortcuts, custom rules, and integration with Next.js, Nuxt, and Astro.
Fix: CSS Container Query Not Working — @container and container-type Issues
How to fix CSS container queries not working — setting container-type correctly, understanding containment scope, fixing @container syntax, and handling browser support and specificity issues.