HomeSegun Adebayo

Building a Typography component with Recipes in Panda CSS

  • #panda
Segun Adebayo

Segun Adebayo

4 min read
Building a Typography component with Recipes in Panda CSS

Panda CSS offers a rich set of features to build your design systems. In this article, I'll show you how to design a `Typography` component using text styles and recipes in Panda CSS. If you use design tools like Figma or Sketch, text styles should sound pretty familiar.

If you want to watch the video version of this tutorial, you can find it on my YouTube channel.

Building a Typography component with Recipes in Panda CSS

Defining text styles

Text styles are a way to avoid repetition when declaring text-related css properties. Think of it like a "css template" for your text that youc an reuse across your app for better consistency.

In the `panda.config.ts` file, let's define text styles for `headline1` and `headline2`

// panda.config.ts

import { defineConfig } from '@pandacss/dev';

export default defineConfig({
  // ...
  theme: {
    extend: {
      textStyles: {
        headline1: {
          description:
            'In the type scale, headlines span from a range of 1 through 6. Headlines are the largest text on the screen, reserved for short, important text or numerals.',
          value: {
            fontWeight: '300',
            // yes! you can change the font size based on the writing mode
            fontSize: { _ltr: '5.5625rem', _rtl: '5.8125rem' },
            lineHeight: '7rem',
            letterSpacing: '-0.015em',
          },
        },
        headline2: {
          description: 'Oversized headlines in website.',
          value: {
            fontWeight: '300',
            fontSize: '3.625rem',
            lineHeight: '4.5rem',
            letterSpacing: '-0.005em',
          },
        },
      },
    },
  },
});

Creating a typography recipe

Recipes are a way to define a set of style variations for a component. In this case, we'll define a `typography` recipe that can be used to apply different text styles to a component.

The `cva` function is a generated function by panda

We'll define a `typography` recipe that has a `level` variant which can be either `h1` or `h2`.

// recipes/typography.ts

import { cva } from 'styled-system/css';

export const typographyRecipe = cva({
  base: {
    color: 'gray.800',
  },
  variants: {
    level: {
      h1: {
        textStyle: 'headline1',
      },
      h2: {
        textStyle: 'headline2',
      },
    },
  },
});

To infer the variant properties of the recipe, we can use the `RecipeVariantProps` type.

import { cva, type RecipeVariantProps } from 'styled-system/css';

export const typographyRecipe = cva({
  //...
});

export type TypographyVariantProps = RecipeVariantProps<typeof typography>;

Building the Typography component

Now that we have the `typography` recipe, we can use it to build out a `Typography` component in React.

// components/typography.tsx

import { cx } from 'styled-system/css';
import { typographyRecipe, type TypographyVariantProps } from '../recipes/typography';

type TypographyHTMLProps = React.HTMLAttributes<HTMLElement>;

type TypographyElement = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span';

export type TypographyProps = TypographyVariantProps &
  TypographyHTMLProps & { as?: TypographyElement };

export function Typography(props: TypographyProps) {
  const [variantProps, localProps] = typographyRecipe.splitVariantProps(props);
  const { as: Component = 'p', className, ...restProps } = localProps;
  return <Component className={cx(typographyRecipe(variantProps), className)} {...restProps} />;
}

Let's unpack what's happening here

const [variantProps, localProps] = typographyRecipe.splitVariantProps(props);

This line uses the `splitVariantProps` function on the recipe to separate the typography recipe from other react props passed to the component.

const { as: Component = 'p', className, ...restProps } = localProps;

This line is used to allows the component to accept an `as` prop which can be used to change the underlying HTML element. It is set to render a `p` tag by default.

return <Component className={cx(typographyRecipe(variantProps), className)} {...restProps} />;

This line applies the typography recipe className based on the variant properties passed to the `Typography` component. It also concatenates any additional `className` passed.

Using the Typography component

Let's use the `Typography` component in our app.

// src/App.tsx

import { Typography } from './components/typography';

function App() {
  return (
    <div dir="ltr">
      <Typography level="h1">This text should have headline1 text style</Typography>
      <Typography level="h2">This text should have headline2 text style</Typography>
    </div>
  );
}

export default App;

Adding bold, italic and underline variants

Let's extend the `typography` recipe to include `bold`, `italic` and `underline` variants for quick text formatting.

We'll model them as boolean variants which means they can either be `true` or `false`.

// recipes/typography.ts

export const typographyRecipe = cva({
  base: {
    color: 'gray.800',
  },
  variants: {
    level: {
      h1: { textStyle: 'headline1' },
      h2: { textStyle: 'headline2' },
    },
    bold: {
      true: {
        fontWeight: 'bold',
      },
    },
    italic: {
      true: {
        fontStyle: 'italic',
      },
    },
    underline: {
      true: {
        textDecoration: 'underline',
      },
    },
  },
});

// ...

Now we can use the `bold`, `italic` and `underline` variants in the `Typography` component.

// src/App.tsx

import { Typography } from './components/typography';

function App() {
  return (
    <div dir="ltr">
      <Typography level="h1" bold>
        This text should have headline1 text style and bold
      </Typography>
      <Typography level="h2" italic>
        This text should have headline2 text style and italic
      </Typography>
      <Typography level="h2" underline>
        This text should have headline2 text style and underline
      </Typography>
    </div>
  );
}

Swapping the underlying HTML element

By default, the typography component renders a paragraph tag. Using the `as` prop, the underlying HTML element of the `Typography` component can be changed.

// src/App.tsx

import { Typography } from './components/typography';

function App() {
  return (
    <div dir="ltr">
      <Typography level="h1" as="h1" bold>
        This text should have headline1 text style and be rendered as h1
      </Typography>
      <Typography level="h2" as="h2" italic>
        This text should have headline2 text style and be rendered as h2
      </Typography>
    </div>
  );
}

Conclusion

In this article, we've learned how to use text styles and recipes to build a `Typography` component in Panda CSS. We also learned how to use boolean variants to add bold, italic and underline text formatting to the `Typography` component.

Learn more about recipes in Panda CSS by visiting the official documentation.


Stay up to date

Get emails from me about web development, tech, and early access to new projects.


Segun Adebayo

Written by Segun Adebayo (Sage)

Sage is a Github Star 🌟 and Design Engineer 👨🏽‍💻. He is passionate about helping people build an accessible web faster. Sage is the author of Chakra UI, a React UI library for building accessible experiences.

Segun Adebayo

Passionate UI engineer looking to bridge the gap between design and code

All rights reserved © Segun Adebayo 2024