Practical React Patterns : Dynamic Styling

I came across this pattern working with my team at Mozilla, so I can’t take full credit. However, I found this method of working with components too elegant not to share. Long story short, this is a great way to manage a component that takes a large number of styling props. This pattern will help you map prop parameters to your SCSS files in a clean, scalable way.

Before getting into the weeds, the ideal setup for this pattern is TypeScript (we will be using enums) and React CSS Modules ( using enums to pinpoint values on the styles object ). Note: not everyone likes enums. You can also do this pattern by subbing out the enums for union types.

If you are impatient like me you can scroll down to the code, it is simple to understand, if you are interested in the what and why, stick around.

The Problem

I was tasked with creating a React component library, of course, we need to have buttons in the library so I started there. I quickly realized just a simple button that needed to cover all aspects of what the button “could” be was not so simple. We only have a “primary” and a “secondary” button style but both primary and secondary styles encapsulate a “solid”, “outline” and “clear” type. Not to mention each has :focus, :disabled, :hover styles. Oh yeah, and we have a light and dark mode. Oh and also, the buttons will have a “small”, “medium” and “large” option. That’s a lot.

In addition to stuffing all of this functionality into one component, I also wanted to make the implementation of the button, from the DX side, extremely easy. A component library is only as good as how easy it is to use.

The implementation

After writing a lot of bad code, I went back to the drawing board. One thing I felt was right about my bad code was I had some pretty nice enums ( or union types ) that outlined all the button names. I also had some pretty clean SCSS going on. It hit me that there was almost a perfect one-to-one pattern with my enums and my SCSS. With a little tweaking, I could make it a perfect one and use the JSX as just a middle-man for my enum and SCSS to communicate.

I have not been a huge fan of how React handles CSS, but having the styles imported as an object did open up doors for more possibilities. In this case, I had the styles I wanted and the enums established, but how could I connect these to worlds? Good ole bracket notation!

The Code

First, we have the button categories and sizes declared in enums. A major part of this pattern is the enums are formatted in snake_case so that we can use them to map values as SCSS values.

export enum ButtonCategoriesE {
  PRIMARY_SOLID = 'primary_solid',
  PRIMARY_OUTLINE = 'primary_outline',
  PRIMARY_CLEAR = 'primary_clear',
  SECONDARY_SOLID = 'secondary_solid',
  SECONDARY_OUTLINE = 'secondary_outline',
  SECONDARY_CLEAR = 'secondary_clear',
}
export enum ButtonSizesE {
  SMALL = 'small',
  MEDIUM = 'medium',
  LARGE = 'large',
}

Here is an example of the primary button solid and the size style breakdown in the SCSS. Look to the ButtonCategoriesE enum to get an idea of what all the other CSS classes would look like.

/******************************
        PRIMARY BUTTONS
******************************/

/*-------------------------------
  SOLID BUTTON 
-------------------------------*/
.button_primary_solid {
  @include rect_button_base();
  background-color: $color-interaction-primary;
  border: solid 2px $color-interaction-primary;
  color: $color-text-reverse;

  &:hover:enabled {
    background-color: $color-interaction-primary-hover;
    border: solid 2px $color-interaction-primary-hover;
  }

  &_active,
  &:focus {
    border: solid 2px $color-interaction-primary-active;
    background-color: $color-interaction-primary-active;
  }

  &:disabled,
  &[disabled] {
    background-color: $color-interaction-primary-disabled;
    border: solid 2px $color-interaction-primary-disabled;
    color: $color-text-disabled;
  }
}

/*-------------------------------
  SIZES
-------------------------------*/

.small {
  padding: 6px 12px;
}

.medium {
  padding: 8px 16px;
}

.large {
  padding: 12px 18px;
}

The Button component will have, among others, the category prop and the size prop. I like to set defaults to the most likely scenario, in this case, that would be a “solid/medium” button.

const Button = ({
  category = ButtonCategoriesE.PRIMARY_SOLID,
  size = ButtonSizesE.MEDIUM,
  ......


  /**
   * Configure CSS Class
   */
  const className = `
    ${styles['button_' + category]} 
    ${styles[size]} 
    ${active && styles['button_' + category + '_active']}
  `

  return (
    <button
      className={className}
      id={id}
      aria-label={label}
      type={type}
      disabled={disabled}
      onClick={onClick}
    >
      {content}
    </button>
  )
  
  }

Using the Button component is very powerful now with just two styling props. This is an example of an “outlined/large” button.

<Button
  label="cancel"
  size={ButtonSizesE.LARGE}
  category={ButtonCategoriesE.PRIMARY_OUTLINE}
  icon="arrow-left"
/>

Hopefully, you found this pattern helpful. Leave a comment if you thought this was great or, on the other hand, have some suggestions to make it better!

Stay Learning.