Building a web application and producing a smooth user experience in the always changing world of web development requires more than just good looks – you also need to make sure that your application's design is efficient and consistent throughout.

If you have a design system in place, you can easily build UI components that are consistent and can be reused across numerous projects while still looking nice.

A design system is a set of reusable UI components and design tokens. These tokens are like building blocks that include things like buttons, colors, and fonts. Its goal is to enable developers and designers to create engaging product experiences by providing consistent user experience across all products.

Designers often use tools like Figma to create these systems. Here's an example of a design system made in Figma:


After designers make these parts, developers can use different tools to actually use them and build the website. There are various frameworks that frontend engineers can choose from to do this. Some popular ones are MaterialUI and ChakraUI. These libraries can make things easier, but they might not cover all the special cases you want.

If you or your design team have specific designs in mind that don't match what existing libraries offer, you'll need to dive into writing a substantial amount of code. However, this process can quickly become tiresome and burdensome, especially when the code you're creating lacks consistency or when you find yourself repeatedly constructing the same UI elements.

This is where a design system becomes incredibly valuable. Armed with the knowledge of how to implement a design system, you gain the ability to craft a tailored design that seamlessly integrates with the existing design system, offering the level of flexibility you desire.

Furthermore, this approach offers the advantage of only having to construct only the components you truly need. Unlike using libraries, which often come with numerous pre-built components you might never use, this method keeps your codebase focused and efficient.

Creating your own design might sound appealing, but it's not as straightforward as it might appear.

Nonetheless, with the assistance of styling tools such as Tailwind CSS (which is ideal due to its high level of customization), a library known as cva (which is short for class-variance-authority), and along with TypeScript, the process of establishing your custom design system within Next.js becomes notably achievable.

Now that you have a clear understanding of what a design system is and its significance, continue reading to discover how these tools can be effectively combined to achieve our objectives.

Project Setup: Installation Of The CVA Library

For the purpose of this article, it is assumed that you have already set up your Next.js project and incorporated TypeScript and Tailwind CSS.

If not, integrating these two elements into a new Next.js project is a straightforward process.

Following this initial setup, you can effortlessly add the CVA library to your project using the following command:

npm i class-variance-authority

With the inclusion of CVA, a user-friendly interface is at your disposal, simplifying the definition of variants.

These variants enable the conditional application of class sets, while also offering the means to express default variations. If this sounds a bit complex at the moment, don't worry – we'll delve into practical examples as we move forward.

Practical Example: Implementing a Custom Design System

Imagine a situation where you're constructing a CRUD application, equipped with buttons to carry out various actions such as create, read, update, and delete.

While these buttons might share common attributes like font-size and border-radius, they might differ slightly – for instance, in their colors. Perhaps you'd prefer a red button for the delete action, a blue one for create, and a black one for update.

Traditionally, you might consider creating separate components for each button and assigning them distinct colors. For instance, to make the black button, you'd include the Tailwind CSS utility class bg-black to set the background color:

export default function Button() {
  return (
      className="bg-black rounded-3xl py-2 text-white w-80 font-sm"
      // other button attributes
      //button text

However, instead of creating separate components for each button, you can streamline the process by designing a single button component that can adapt to the various button variants you need. This concept is the heart of a design system.

Let's explore how we can transform this same button component we previously created to suit our needs.

Our first step is to import the cva function and the VariantProps from the class-variance-authority library. As we proceed, the significance of these imports will become apparent:

import { cva, VariantProps } from 'class-variance-authority'

Subsequently, we will define a variable named buttonStyles. This variable will house the invocation of the cva function.

Within this function, we'll first provide the default styles for the buttons, followed by the variants object. This object will contain the variations for the different types of buttons we desire. This segment is pivotal when implementing the design system.

In your code, include the following code snippet:

const buttonStyles = cva("rounded-3xl py-2 text-white w-80 font-sm", {
  variants: {
    intent: {
      primary: "bg-blue-700",
      secondary: "bg-black",
      danger: "bg-red-600",
    defaultVariants: {
      intent: "primary",

Within the embedded variants object, there exists another object named intent. This is where you assign names to the distinct button variations you desire – for instance, primary, secondary, and danger, corresponding to the create, update, and delete buttons respectively.

The style defined for the intent specified when rendering the button will be applied to that button, wherever it appears.

Additionally, the variants object features another property called defaultVariants. The intent within the defaultVariant object will be applied when no explicit intent is provided. In this instance, we have set the default intent to primary.

Next, we'll define an interface named ButtonProps which extends the VariantProps type provided by the class-variance-authority library:

interface ButtonProps extends VariantProps<typeof buttonStyles> {
  text: string;

This interface is used to define the props that the Button component will accept. The VariantProps type from class-variance-authority adds the intent property, which will be used to determine the variant of the button. The interface also includes an additional property called text, which is of type string, it is the actual text that will appear on the button.

Having done this, we can go ahead and implement the Button component with respect to this defined interface:

export default function Button({ intent, text, ...props }: ButtonProps) {
  return (
    <button className={buttonStyles({ intent })} {...props}>

In the Button component function, we used object destructuring to receive the intent property from the ButtonProps passed to the component.

Additionally, we used the spread operator (...props) to spread any other props that might be passed to the component. This allows us to easily pass down any other attributes or event handlers to the <button> element.

This also makes our props accurately typed, with their types precisely aligned as you hover over each one. For instance, the intent prop strictly adheres to the intents we defined within the buttonStyles variable, ensuring a robust type correspondence:


Moving on, the crucial part is the className attribute on the <button> element. We're using the buttonStyles function to pass in an object with the intent property, which corresponds to the desired button variant (primary, secondary, or danger).

This dynamically generates the appropriate styles based on the selected intent, applying the associated styles to the button.

At last, we've streamlined the process of creating buttons with different styles, allowing us to utilize a single Button component while dynamically altering its appearance through the intent prop, showcasing the effective implementation of our personalized design system.

With this in place, you're now equipped to render the Button component according to your needs and preferences. As a visual demonstration, let's observe the outcomes after rendering each of the variants:

  <Button intent="primary" text="Create" /> <br /> <br />
  <Button intent="secondary" text="Update" /> <br /> <br />
  <Button intent="danger" text="Delete" /> <br /> <br />

Visual result:


This visual representation underscores the versatility and adaptability of our streamlined Button component, showcasing the distinct styling achieved through the intent-based approach within our own crafted design system.

Wrapping Up

Throughout this journey, you've gained insights into the potential of a design system to produce a versatile button component that dynamically changes colors in response to the provided intent.

It's important to note that this adaptability isn't confined solely to colors – you can similarly experiment with font sizes, text colors, and various other CSS properties.

Empowered by the insights you've gathered, you're now positioned to leverage this newfound knowledge to not only enhance your understanding of design systems but to also implement creative solutions that align with your unique project requirements. Happy Coding!