Similar Components with Different Styles in Chakra UI

Similar Components with Different Styles in Chakra UI


When building components with Chakra UI v3, you might need to create similar components with slight variations in styles, such as different background colors. How can you manage these variations efficiently?

Sample of the similar components

Let’s consider a ServerStatus component that represents the running state of a server. A simple version of this component might look like the following:

import { Box } from "@chakra-ui/react";

export const ServerRunningStatus = () => {
  return (
    <Box
      w="200px"
      p={2}
      textAlign="center"
      borderRadius="md"
      bg="green.600"
      color="white"
    >
      Running
    Box>
  );
};
Enter fullscreen mode

Exit fullscreen mode

However, a server doesn’t only have a running state—it could also be stopped or in an unknown state. Since the display content is mostly the same, we can use a single component with props to control its variations.

import { Box } from "@chakra-ui/react";

type Status = "running" | "stopped" | "unknown";

interface Props {
  status: Status;
}

export const ServerStatus = ({ status }: Props) => {
  const bgColor = {
    running: "green.600",
    stopped: "red.600",
    unknown: "gray.600",
  }[status];

  return (
    <Box
      w="200px"
      p={2}
      textAlign="center"
      borderRadius="md"
      textTransform="capitalize"
      bg={bgColor}
      color="white"
    >
      {status}
    Box>
  );
};
Enter fullscreen mode

Exit fullscreen mode

To differentiate the states visually, we define a bgColor property. This approach works fine in many cases, and when new states are added to Status, a type error will occur if they are not handled properly. However, as the number of styles that need differentiation increases, managing them manually can get harder to manage.



Managing Multiple Styles

Now, let’s also manage text color along with the background color by simply adding another property:

import { Box } from "@chakra-ui/react";

type Status = "running" | "stopped" | "unknown";

interface Props {
  status: Status;
}

export const ServerStatus = ({ status }: Props) => {
  const styles = {
    running: { bg: "green.600", color: "white" },
    stopped: { bg: "red.600", color: "white" },
    unknown: { bg: "gray.200", color: "gray.600" },
  }[status];

  return (
    <Box
      w="200px"
      p={2}
      textAlign="center"
      borderRadius="md"
      textTransform="capitalize"
      bg={styles.bg}
      color="white"
    >
      {status}
    Box>
  );
};
Enter fullscreen mode

Exit fullscreen mode

While this works, there are a few issues. The biggest drawback is that styles lack type safety. If a value is incorrect, TypeScript won’t catch it, and auto-completion won’t be available.



Using Record for Type Safety

We can improve this by using a Record type:

import { Box, type BoxProps } from "@chakra-ui/react";

type Status = "running" | "stopped" | "unknown";

interface Props {
  status: Status;
}

const styles: Record<Status, Required<Pick<BoxProps, "bg" | "color">>> = {
  running: { bg: "green.600", color: "white" },
  stopped: { bg: "red.600", color: "white" },
  unknown: { bg: "gray.200", color: "gray.600" },
};

export const ServerStatus = ({ status }: Props) => {
  return (
    <Box
      w="200px"
      p={2}
      textAlign="center"
      borderRadius="md"
      textTransform="capitalize"
      {...styles[status]}
    >
      {status}
    Box>
  );
};
Enter fullscreen mode

Exit fullscreen mode

By using Record, we ensure that every Status has a corresponding style definition. Using Required> is optional, but it guarantees that both bg and color exist for every Status. Additionally, since we leverage BoxProps, we benefit from TypeScript’s auto-completion.

If different statuses require different numbers of style properties, we can structure the mapping flexibly:

const styles: Record<Status, BoxProps> = {
  running: { bg: "green.600", color: "white" },
  stopped: { bg: "red.600", color: "white" },
  unknown: { bg: "gray.200", borderColor: "gray.800" },
};
Enter fullscreen mode

Exit fullscreen mode

This approach might be sufficient in many cases. However, what if we need to define more complex styles? Writing a large number of styles directly in the component can reduce readability.



Separating Styles with Chakra UI’s Recipe

Chakra UI provides the Recipe feature, which allows us to separate styles from the component logic. Let’s see an example:

import { Box, defineRecipe, useRecipe } from "@chakra-ui/react";

type Status = "running" | "stopped" | "unknown";

interface Props {
  status: Status;
}

const serverStatusRecipe = defineRecipe({
  base: {
    textAlign: "center",
    w: "200px",
    p: 2,
    borderRadius: "md",
    textTransform: "capitalize",
  },
  variants: {
    status: {
      running: { bg: "green.200", color: "white" },
      stopped: { bg: "red.600", color: "white" },
      unknown: { bg: "gray.200", color: "gray.600" },
    },
  },
});

export const ServerStatus = ({ status }: Props) => {
  const recipe = useRecipe({ recipe: serverStatusRecipe });
  const styles = recipe({ status });

  return <Box css={styles}>{status}Box>;
};
Enter fullscreen mode

Exit fullscreen mode

By using defineRecipe, we can write styles with type safety and auto-completion. The combination of base and variants allows us to naturally define common styles and their variations. This separation improves readability and maintainability.

Moreover, serverStatusRecipe can be placed in a separate file or even within Chakra’s theme configuration, making it highly reusable across the application.



Conclusion

Depending on the complexity of the styles and the need for reusability, you can choose the best approach for your use case. Whether using simple prop-based styling, a Record mapping, or Chakra UI’s Recipe, each method offers different benefits in terms of maintainability and scalability.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *