Use Krysalicss with Next.js

Add Krysalicss to a Next.js App Router project. Next handles SCSS natively once the sass package is installed; the framework's package exports cooperate with both Webpack and Turbopack so no extra bundler config is needed.

Install

Next.js v15+ compiles SCSS through its built-in Sass loader as long as the sass package is on disk:

Terminal
pnpm add sass
pnpm add @adnap/krysalicss

No next.config.js changes required.

Wire the SCSS entry

Create one SCSS entry under app/styles/ and @use the framework's batteries-included entry:

app/styles/app.scss
// app/styles/app.scss
@use '@adnap/krysalicss';

Import the SCSS file from the App Router's root layout (app/layout.tsx). Next emits a single global stylesheet for everything imported here.

app/layout.tsx
// app/layout.tsx
import './styles/app.scss';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>{children}</body>
    </html>
  );
}

The @adnap/krysalicss specifier resolves via the package's sass export condition to src/krysalicss.scss — no path alias, no node_modules/ traversal needed.

Override a variable

Configure a per-component import with with () ahead of the framework entry. Sass picks up the override at the first @use:

app/styles/app.scss
// app/styles/app.scss
@use '@adnap/krysalicss/module/card' with (
  $padding: 2rem,
  $border-radius: 12px,
);
@use '@adnap/krysalicss';

The full override surface is documented in Override component variables.

Strip unused modules

Pass $feature-list to the framework entry. Next bundles only the emitted slices into the global stylesheet:

app/styles/app.scss
// app/styles/app.scss
@use '@adnap/krysalicss' with (
  $feature-list: (
    base-reset,
    base-global,
    typography-base,
    typography-title,
    typography-link,
    layout-container,
    layout-grid,
    element-button,
    element-badge,
    module-card,
    module-navbar,
  ),
);

See Tree-shake to specific modules for the flag inventory and the per-module import alternative.

Light + dark theme switching

The dark theme listens for prefers-color-scheme automatically. For a manual toggle, set the class on <html> from a server-evaluated inline script so the first paint is correct (no flash). Next's App Router runs the layout server-side, so dangerouslySetInnerHTML is appropriate here:

app/layout.tsx
// app/layout.tsx
import './styles/app.scss';

const themeBootstrap = `
  (function () {
    try {
      var saved = localStorage.getItem('theme');
      if (saved === 'dark') document.documentElement.classList.add('theme-dark');
    } catch (_) {}
  })();
`;

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script dangerouslySetInnerHTML={{ __html: themeBootstrap }} />
      </head>
      <body>{children}</body>
    </html>
  );
}

The toggle itself is a tiny Client Component:

app/components/ThemeToggle.tsx
// app/components/ThemeToggle.tsx
'use client';

export function ThemeToggle() {
  const toggle = () => {
    const root = document.documentElement;
    root.classList.toggle('theme-dark');
    localStorage.setItem(
      'theme',
      root.classList.contains('theme-dark') ? 'dark' : 'light',
    );
  };
  return <button className="button" onClick={toggle}>Toggle theme</button>;
}

The suppressHydrationWarning attribute on <html> tells React not to warn about the bootstrap script changing the class before hydration.

See also