Skip to content

Latest commit

 

History

History

next-stitches-typescript

Twin + Next.js + Stitches + TypeScript

TwinTwinNext.jsNext.jsStitchesStitchesTypeScript

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.

Table of contents

Getting started

Installation

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

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.

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

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: 'stitches',
  },
}

b) Or in package.json:

// package.json
"babelMacros": {
  "twin": {
    "preset": "stitches"
  }
},

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,
    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 the stitches config

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

Add the server stylesheet

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>
    )
  }
}

Complete the TypeScript setup

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"],
}

Usage

Styled components

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 />

Prop styling

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) }} />
)

Customization

Next steps

For more usage docs, visit the Stitches docs