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.
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
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>
)
}
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>
)
}
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"
}
},
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"]
}
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,
})
- If the rendered html shows
css="[object object]"
then check that the"use client"
directive is added wherever twin.macro is used.
Learn how to work with twin
- The prop styling guide - A must-read guide to level up on prop styling
- The styled component guide - A must-read guide on getting productive with styled-components
Learn more about styled-components