Skip to content

Files

Latest commit

 

History

History

next-styled-components-typescript

Twin + Next.js + Styled Components + TypeScript

TwinTwinNext.jsNext.jsStyled componentsStyled componentsTypeScript

Download this example using degit

npx degit https://github.com/ben-rogerson/twin.examples/next-styled-components-typescript folder-name

From within the new folder, run npm install, then npm run dev to start the dev server.

Table of contents

Getting started

Installation

Install Next.js

Choose "Yes" for the src/ directory option when prompted.

npx create-next-app@latest --typescript

Install the dependencies

npm install styled-components
npm install -D twin.macro tailwindcss babel-plugin-macros babel-loader @babel/plugin-syntax-typescript babel-plugin-styled-components
Install with Yarn

Choose "Yes" for the src/ directory option when prompted.

yarn create next-app --typescript

Install the dependencies

yarn add styled-components
yarn add twin.macro tailwindcss babel-plugin-macros babel-loader @babel/plugin-syntax-typescript babel-plugin-styled-components --dev

Add the global styles

Twin uses the same preflight base styles as Tailwind to smooth over cross-browser inconsistencies.

The GlobalStyles import adds these base styles along with some @keyframes for the animation classes and some global css that makes the ring classes and box-shadows work.

Due to an issue in styled-components, global styles get added in the wrong order when using styled-components. This gives the tailwind base styles an incorrect specificity.
Until the issue is fixed, the workaround is to export the styles from another file.

You can import GlobalStyles within a new file placed in src/styles/GlobalStyles.tsx:

// src/styles/GlobalStyles.tsx
'use client'
import React from 'react'
import { createGlobalStyle } from 'styled-components'
import tw, { theme, GlobalStyles as BaseStyles } from 'twin.macro'

const CustomStyles = createGlobalStyle({
  body: {
    WebkitTapHighlightColor: theme`colors.purple.500`,
    ...tw`antialiased`,
  },
})

const GlobalStyles = () => (
  <>
    <BaseStyles />
    <CustomStyles />
  </>
)

export default GlobalStyles

Then import the GlobalStyles file in src/app/layout:

// src/app/layout.tsx
import type { Metadata } from 'next'
import GlobalStyles from '@/styles/GlobalStyles'
import StyledComponentsRegistry from '@/lib/registry'

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

Add the server stylesheet

To avoid the ugly Flash Of Unstyled Content (FOUC), add the following in lib/registry.js:

// lib/registry.js
// From https://nextjs.org/docs/app/building-your-application/styling/css-in-js#styled-components
'use client'
import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'

export default function StyledComponentsRegistry({
  children,
}: {
  children: React.ReactNode
}) {
  // Only create stylesheet once with lazy initial state
  // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
  const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())

  useServerInsertedHTML(() => {
    const styles = styledComponentsStyleSheet.getStyleElement()
    styledComponentsStyleSheet.instance.clearTag()
    return <>{styles}</>
  })

  if (typeof window !== 'undefined') return <>{children}</>

  return (
    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
      {children}
    </StyleSheetManager>
  )
}

Add the twin config

Twin’s config can be added in a couple of different files.

a) Either in babel-plugin-macros.config.js:

// babel-plugin-macros.config.js
module.exports = {
  twin: {
    preset: 'styled-components',
  },
}

b) Or in package.json:

// package.json
"babelMacros": {
  "twin": {
    "preset": "styled-components"
  }
},

Add TypeScript types

Create a types/twin.d.ts file with the declarations from the example:

mkdir types && cd $_ && curl -o twin.d.ts -L https://github.com/ben-rogerson/twin.examples/raw/master/next-styled-components-typescript/types/twin.d.ts

Then add the types folder to the include array in your typescript config:

// tsconfig.json
{
  // ...
  "include": ["src", "types"]
}

Add the next babel config

Create a new file either in the root or in a config subfolder:

// withTwin.mjs
import babelPluginTypescript from '@babel/plugin-syntax-typescript'
import babelPluginMacros from 'babel-plugin-macros'
import * as path from 'path'
import * as url from 'url'
// import babelPluginTwin from 'babel-plugin-twin'

const __dirname = url.fileURLToPath(new URL('.', import.meta.url))

// The folders containing files importing twin.macro
const includedDirs = [path.resolve(__dirname, 'src')]

/** @returns {import('next').NextConfig} */
export default function withTwin(
  /** @type {import('next').NextConfig} */
  nextConfig,
) {
  return {
    ...nextConfig,
    compiler: {
      ...nextConfig.compiler,
      styledComponents: true,
    },
    webpack(
      /** @type {import('webpack').Configuration} */
      config,
      options,
    ) {
      config.module = config.module || {}
      config.module.rules = config.module.rules || []

      config.module.rules.push({
        test: /\.(tsx|ts)$/,
        include: includedDirs,
        use: [
          {
            loader: 'babel-loader',
            options: {
              sourceMaps: options.dev,
              plugins: [
                // babelPluginTwin, // Optional
                babelPluginMacros,
                [babelPluginTypescript, { isTSX: true }],
              ],
            },
          },
        ],
      })

      if (typeof nextConfig.webpack === 'function')
        return nextConfig.webpack(config, options)

      return config
    },
  }
}

Then in your next.config.mjs, import and wrap the main export with withTwin(...):

// next.config.mjs
import withTwin from './withTwin.mjs'

/**
 * @type {import('next').NextConfig}
 */
export default withTwin({
  reactStrictMode: true,
})

Note

  • If the rendered html shows css="[object object]" then check that the "use client" directive is added wherever twin.macro is used.

Customization

Next steps

Learn how to work with twin

Learn more about styled-components