DEV Community

Scott Wagner
Scott Wagner

Posted on

TIL: DRYing Out Styles With styled-components

Welcome to the first post in my TIL series. My intention with this series is to share tidbits of neat things that I've learned, experimented with, or rediscovered.

This first post is going to be about extracting and reusing some common styling in React components using styled-components (a CSS-in-JS library for React), and how Typescript saved the day.

So here's the background: I'm working in a React Typescript project that uses styled-components. In this project, there was a component that had a dropdown element to it and when the dropdown expanded a chevron svg would rotate as a bit of visual indication that the dropdown was now open. I needed to add a new component that had a dropdown element with a similar svg animation.

Note: The CodeSandbox sample project isn't the actual project, but a stripped down example for this post.

How I Started

Here's the first component. The interesting bit is the svg styling in the button, particularly the transition and transform properties. They combine to create the rotation animation that responds to the styled component prop change.

// OriginalComponent.tsx
/* other components and stuff */

const DropdownTriggerButton = styled.button<DropdownTriggerButtonProps>`
  /* some button styles */
  svg {
    height: 1em;
    width: auto;
    /* LOOK HERE 👇 */
    transition: transform ease-in-out 300ms;
    ${(props) =>
      props.isOpen
        ? css`
            transform: rotate(0deg);
          `
        : css`
            transform: rotate(180deg);
          `}
  }
`;
Enter fullscreen mode Exit fullscreen mode

What Next?

I needed to add a new component, so I did that.

// NewComponent.tsx
/* other components and stuff */

const Expander = styled.div<ExpanderProps>`
  svg {
    height: 1.5em;
    width: auto;
    cursor: pointer;
    /* LOOK HERE 👇 */
    transition: transform ease-in-out 200ms;
    ${(props) =>
      props.expanded
        ? css`
            transform: rotate(0deg);
          `
        : css`
            transform: rotate(180deg);
          `}
  }
`;
Enter fullscreen mode Exit fullscreen mode

The Neat Part

Through the use of the css utility in styled-components, I was able to extract the common svg animation to a reusable variable for inclusion in both components. This is the DRYing (Don't Repeat Yourself) bit.

/* expandIconAnimation.tsx */
import { css } from "styled-components";

type expandIconAnimationProps = {
  expanded: boolean;
};

export const expandIconAnimation = css<expandIconAnimationProps>`
  svg {
    transition: transform ease-in-out 300ms;
    ${(props) =>
      props.expanded
        ? css`
            transform: rotate(0deg);
          `
        : css`
            transform: rotate(180deg);
          `}
  }
`;
Enter fullscreen mode Exit fullscreen mode

This is neat for a couple reasons:

  1. There's now an animation that's easy to include in new components that need it. We don't need to reinvent the wheel every time.
  2. This promotes visual consistency across components. If the two components had the same icon animation for the same semantic reason (implemented separately), and there were differences in the timing function, or the animation duration, or even the transform, it wouldn't feel as cohesive. This can be a bad thing. If you did want to allow some style overrides for specific circumstances, you could change the expandIconAnimation variable to a function that accepted arguments for values that need to be be able to be overridden.

Updating Our Original Components

Our components after being updated to use the expandIconAnimation "partial":

const DropdownTriggerButton = styled.button<DropdownTriggerButtonProps>`
  /* some button styles */

  ${expandIconAnimation}
  svg {
    height: 1em;
    width: auto;
  }
`;
Enter fullscreen mode Exit fullscreen mode
const Expander = styled.div<ExpanderProps>`
  ${expandIconAnimation}
  svg {
    height: 1.5em;
    width: auto;
    cursor: pointer;
  }
`;
Enter fullscreen mode Exit fullscreen mode

We've successfully reduced the duplication and ensured a consistent rotation animation for the expand/collapse functionality. However, you may have noticed that the DropdownTriggerButton used isOpen as the prop to control the transforms, whereas the Expander used expanded, and the expandIconAnimation expects expanded. This means that the usage in DropdownTriggerButton won't work.

Luckily, Typescript catches that for us. When using expandIconAnimation in DropdownTriggerButton we get a build error that the property expanded is missing in the props type for DropdownTriggerButton, but is required by expandIconAnimation. Fortunately, it's a pretty simple fix in this scenario. We can just change the prop on DropdownTriggerButton from isOpen to expanded. The types are satisfied, and the animation works as expected. Thanks Typescript.

Takeaways

  1. Making common styles can be a good way to reduce code duplication and promote visual consistency.
  2. Typescript is a great tool to aid in the prevention of bugs.

Top comments (0)