Skip to main content
Light Dark System
Get ready for more awesome! Web Awesome, the next iteration of Shoelace, is on Kickstarter. Read Our Story

Integrating with NextJS

This page explains how to integrate Shoelace with a NextJS app.

There are 2 guides available:

Usage with App Router (NextJS 14)

  • Node: v20.11.1
  • NextJS: 14.2.4
  • Shoelace: 2.15.1

Working with ESM

If you haven’t already, create your NextJS app. You can find the documentation for that here: https://nextjs.org/docs/getting-started/installation

After you’ve created your app, the first step to using Shoelace is modifying your package.json to have "type": "module" in it since Shoelace ships ES Modules.

// package.json
{
  "type": "module"
}

Installing packages

To get started using Shoelace with NextJS, the following packages must be installed.

npm install @shoelace-style/shoelace copy-webpack-plugin

Shoelace for obvious reasons, and the copy-webpack-plugin will be used later for adding our icons to our public/ folder.

Modifying your Next Config

We’ll start with modifying our next.config.js to copy Shoelace’s assets and to properly work with ESM.

Here’s what your next.config.js should look like:

NextJS 14 Webpack Config

In order to add Shoelace’s assets to the final build output, we need to modify next.config.js to look like this.

// next.config.js

import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import CopyPlugin from 'copy-webpack-plugin';

const __dirname = dirname(fileURLToPath(import.meta.url));

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: { esmExternals: 'loose' },
  webpack: (config, options) => {
    config.plugins.push(
      new CopyPlugin({
        patterns: [
          {
            from: resolve(__dirname, 'node_modules/@shoelace-style/shoelace/dist/assets/'),
            to: resolve(__dirname, 'public/shoelace-assets/assets/')
          }
        ]
      })
    );
    return config;
  }
};

export default nextConfig;

Importing the Shoelace’s CSS (default theme)

Once we’ve got our webpack config / next config setup, lets modify our app/layout.tsx to include Shoelace’s default theme.

// app/layout.tsx
import './globals.css';
import '@shoelace-style/shoelace/dist/themes/light.css';
// We can also import the dark theme here as well.
// import "@shoelace-style/shoelace/dist/themes/dark.css";

Writing a “setup” component

Now, we need to create a ShoelaceSetup component that will be a client component in charge of setting the basePath for our assets / icons.

To do so, create a file called app/shoelace-setup.tsx

'use client';
// ^ Make sure to have 'use client'; because `setBasePath()` requires access to `document`.

import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js"

export default function ShoelaceSetup({
  children,
}: {
  children: React.ReactNode
}) {
  setBasePath(`/shoelace-assets/`);
  return <>{children}</>
}

Then we’ll add this setup component into app/layout.tsx

Our layout.tsx Should now look something like this:

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
+ import "@shoelace-style/shoelace/dist/themes/light.css";

+ import ShoelaceSetup from "./shoelace-setup";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

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

Writing components that use Shoelace

Now that we have the setup in place, we can write an app/page.tsx to use Shoelace components in combination with the dynamic() component loader from NextJS.

Here’s what that would look like, do note the "use client"; at the top of the component is required.

// app/page.tsx
'use client';

import React from "react";
import dynamic from "next/dynamic";

const SlButton = dynamic(
  // Notice how we use the full path to the component. If you only do `import("@shoelace-style/shoelace/dist/react")` you will load the entire component library and not get tree shaking.
  () => import("@shoelace-style/shoelace/dist/react/button/index.js"),
  {
    loading: () => <p>Loading...</p>,
    ssr: false,
  },
);

const SlIcon = dynamic(
  () => import("@shoelace-style/shoelace/dist/react/icon/index.js"),
  {
    loading: () => <p>Loading...</p>,
    ssr: false,
  },
);

export default function Home() {
  return (
    <main>
      <SlButton>Test</SlButton>
      <SlIcon name="gear" />
    </main>
  );
}

Now you should be up and running with NextJS + Shoelace!

If you’re stuck, there’s an example repo here you can checkout.

Usage with Pages Router (NextJS 12)

  • Node: 16.13.1
  • NextJS: 12.1.6
  • Shoelace: 2.0.0-beta.74

To get started using Shoelace with NextJS, the following packages must be installed.

yarn add @shoelace-style/shoelace copy-webpack-plugin next-compose-plugins next-transpile-modules

Enabling ESM

Because Shoelace utilizes ESM, we need to modify our package.json to support ESM packages. Simply add the following to your root of package.json:

"type": "module"

There’s one more step to enable ESM in NextJS, but we’ll tackle that in our Next configuration modification.

Importing the Default Theme

The next step is to import Shoelace’s default theme (stylesheet) in your _app.js file:

import '@shoelace-style/shoelace/dist/themes/light.css';

Defining Custom Elements

After importing the theme, you’ll need to import the JavaScript files for Shoelace. However, this is a bit tricky to do in NextJS thanks to the SSR environment not having any of the required browser APIs to define endpoints.

We’ll want to create a component that uses React’s useLayoutEffect to add in the custom components before the first render:

function CustomEls({ URL }) {
  // useRef to avoid re-renders
  const customEls = useRef(false);

  useLayoutEffect(() => {
    if (customEls.current) {
      return;
    }

    import('@shoelace-style/shoelace/dist/utilities/base-path').then(({ setBasePath }) => {
      setBasePath(`${URL}/static/static`);

      // This imports all components
      import('@shoelace-style/shoelace/dist/react');
      // If you're wanting to selectively import components, replace this line with your own definitions

      // import("@shoelace-style/shoelace/dist/components/button/button");
      customEls.current = true;
    });
  }, [URL, customEls]);

  return null;
}

You may be wondering where the URL property is coming from. We’ll address that in the next few sections.

Using Our New Component In Code

While we need to use useLayoutEffect for the initial render, NextJS will throw a warning at us for trying to use useLayoutEffect in SSR, which is disallowed. To fix this problem, we’ll conditionally render the CustomEls component to only render in the browser

function MyApp({ Component, pageProps, URL }) {
  const isBrowser = typeof window !== 'undefined';
  return (
    <>
      {isBrowser && <CustomEls URL={URL} />}
      <Component {...pageProps} />
    </>
  );
}

Additional Resources