Download this example using degit
npx degit https://github.com/ben-rogerson/twin.examples/next-stitches-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 --typescript
Install the dependencies
npm install @stitches/react
npm install -D twin.macro tailwindcss babel-plugin-macros
Install with Yarn
Choose "Yes" for the src/
directory option when prompted.
yarn create next-app --typescript
Install the dependencies
yarn add @stitches/react
yarn add twin.macro tailwindcss babel-plugin-macros --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.
You can add Twin’s globalStyles
import in src/styles/globalStyles.tsx
:
// src/styles/globalStyles.tsx
import tw, { theme, globalStyles } from 'twin.macro'
import { globalCss } from '../stitches.config'
const customStyles = {
body: {
WebkitTapHighlightColor: theme`colors.purple.500`,
...tw`antialiased`,
},
}
const styles = () => {
globalCss(customStyles)()
globalCss(globalStyles as Record<any, any>)()
}
export default styles
Then import the global styles in src/pages/_app.tsx
:
// src/pages/_app.tsx
import { AppProps } from 'next/app'
import globalStyles from '../styles/globalStyles'
const App = ({ Component, pageProps }: AppProps) => {
globalStyles()
return <Component {...pageProps} />
}
export default App
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: 'stitches',
},
}
b) Or in package.json
:
// package.json
"babelMacros": {
"twin": {
"preset": "stitches"
}
},
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,
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,
})
Add this stitches configuration in stitches.config.ts
:
// stitches.config.ts
import { createStitches, CSS as StitchesCSS } from '@stitches/react'
export type { CSS } from '@stitches/react/types/css-util'
export const stitches = createStitches({
prefix: '',
theme: {},
utils: {},
})
export const { css, styled, globalCss, theme, keyframes, getCssText } = stitches
To avoid the ugly Flash Of Unstyled Content (FOUC), add a server stylesheet in src/pages/_document.tsx
that gets read by Next.js:
// src/pages/_document.tsx
import NextDocument, { Html, Head, Main, NextScript } from 'next/document'
import { getCssText } from '../stitches.config'
export default class Document extends NextDocument {
static async getInitialProps(ctx: any) {
try {
const initialProps = await NextDocument.getInitialProps(ctx)
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{/* Stitches CSS for SSR */}
<style
id="stitches"
dangerouslySetInnerHTML={{ __html: getCssText() }}
/>
</>
),
}
} finally {
}
}
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
Because twin routes the styled
and css
, you’ll need complete the typescript setup.
Create a types/twin.d.ts
file in your project root and add these declarations:
import 'twin.macro'
import { css as cssImport } from '@stitches/react'
import type { CSS as StitchesCSS } from '@stitches/react'
import type StyledComponent from '@stitches/react/types/styled-component'
import type Util from '@stitches/react/types/util'
import type CSSUtil from '@stitches/react/types/css-util'
import {
stitches as config,
css as cssImport,
styled as stitchesStyled,
} from '../stitches.config'
// Support a css prop when used with twins styled.div({}) syntax
type CSSProp = StitchesCSS<typeof config>
type Media = typeof config.media
type Theme = typeof config.theme
type ThemeMap = typeof config.themeMap
type Utils = typeof config.utils
type Styled<Type> = {
<
Composers extends (
| string
| React.ComponentType<unknown>
| Util.Function
| { [name: string]: unknown }
)[],
CSS = CSSUtil.CSS<Media, Theme, ThemeMap, Utils>,
>(
...composers: {
[K in keyof Composers]: string extends Composers[K] // Strings, React Components, and Functions can be skipped over
? Composers[K]
: Composers[K] extends
| string
| React.ComponentType<unknown>
| Util.Function
? Composers[K]
: RemoveIndex<CSS> & {
/** The **variants** property lets you set a subclass of styles based on a key-value pair.
*
* [Read Documentation](https://stitches.dev/docs/variants)
*/
variants?: {
[_Name in string]: {
[_Pair in number | string]: CSS
}
}
/** The **compoundVariants** property lets you to set a subclass of styles based on a combination of active variants.
*
* [Read Documentation](https://stitches.dev/docs/variants#compound-variants)
*/
compoundVariants?: (('variants' extends keyof Composers[K]
? {
[Name in keyof Composers[K]['variants']]?:
| Util.Widen<keyof Composers[K]['variants'][Name]>
| Util.String
}
: Util.WideObject) & {
css: CSS
})[]
/** The **defaultVariants** property allows you to predefine the active key-value pairs of variants.
*
* [Read Documentation](https://stitches.dev/docs/variants#default-variants)
*/
defaultVariants?: 'variants' extends keyof Composers[K]
? {
[Name in keyof Composers[K]['variants']]?:
| Util.Widen<keyof Composers[K]['variants'][Name]>
| Util.String
}
: Util.WideObject
} & CSS & {
[K2 in keyof Composers[K]]: K2 extends
| 'compoundVariants'
| 'defaultVariants'
| 'variants'
? unknown
: K2 extends keyof CSS
? CSS[K2]
: unknown
}
}
): StyledComponent.StyledComponent<
Type,
StyledComponent.StyledComponentProps<Composers>,
Media,
CSSUtil.CSS<Media, Theme, ThemeMap, Utils>
>
}
declare module 'react' {
// The css prop
interface HTMLAttributes<T> extends DOMAttributes<T> {
css?: CSSProp
tw?: string
}
// The inline svg css prop
interface SVGProps<T> extends SVGProps<SVGSVGElement> {
css?: CSSProp
tw?: string
}
}
// Support twins styled.div({}) syntax
type StyledTags = {
[Tag in keyof JSX.IntrinsicElements]: Styled<Tag>
}
declare module 'twin.macro' {
// The styled and css imports
const styled: StyledTags & typeof stitchesStyled
const css: typeof cssImport
}
Then add the following in your typescript config:
// tsconfig.json
{
// Tell typescript about the types folder
"types": ["types"],
// Recommended settings
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
}
Use the tw import to create and style new components:
import tw from 'twin.macro'
const Input = tw.input`border hover:border-black`
;<Input />
Switch to the styled import to add conditional styling:
import tw, { styled } from 'twin.macro'
const StyledInput = styled.input({
// Spread the base styles
...tw`bg-white max-w-[200px]`,
// Add conditional styling in the variants object
// https://stitches.dev/docs/variants
variants: {
hasBorder: { true: tw`border-purple-500` },
},
})
;<StyledInput hasBorder />
Style jsx elements using the tw prop:
import 'twin.macro'
const Input = () => <input tw="border hover:border-black" />
Nest Twin’s tw
import within a css prop to add conditional styles:
import tw from 'twin.macro'
const Input = ({ hasHover }) => (
<input
css={{
// Spread the base styles
...tw`border`,
// Add conditionals afterwards
...(hasHover && tw`hover:border-black`),
}}
/>
)
Or mix sass styles with the css import:
import tw, { css } from 'twin.macro'
const hoverStyles = {
'&:hover': {
'border-color': 'black',
...tw`text-black`,
},
}
const Input = ({ hasHover }) => (
<input css={{ ...tw`border`, ...(hasHover && hoverStyles) }} />
)
For more usage docs, visit the Stitches docs