Highly Dynamic Styled-Components

Tags: webpage, development, react, CSS

CSS-in-JS; the Future Present of Style

I work on websites all the time, literally it’s my job and my hobby. I’m focused on and constantly trying to improve my craft and I’m always looking for new tools that help to make my experience quicker and more consistent. CSS-in-JS is one of the most successful tools I’ve added to my toolbox; collocating your styles with the components they modify is a great method to organize large projects. In addition to the improved organization and the added programmability that comes with JavaScript, you’re creating a bundle of transpiled, vendor-prefixed CSS with cache-busting fingerprinting.

I’ve been using CSS-in-JS for a lot of my projects because of these benefits, and it’s generally been a success. I even think that CSS-in-JS outpaces CSS preprocessors in terms of developer experience, even if I’m in favor of CSS preprocessing in 2022. These successes don’t come without their troubles, and one that I’ve recently learned to overcome is highly dynamic components. Let me fill you in on why they’re a problem in the first place.

Some CSS-in-JS

I’m a big fan of styled-components. It encapsulates all of the benefits of the CSS-in-JS paradigm and provides a seriously extensible and useful interface for making complicated yet coordinated styles. The way it works is by using JS format strings, like this example pulled from their homepage:

const Button = styled.a`
  display: inline-block;
  border-radius: 3px;
  padding: 0.5rem 0;
  margin: 0.5rem 1rem;
  width: 11rem;
  background: transparent;
  color: white;
  border: 2px solid white;

  ${props => props.primary && css`
    background: white;
    color: black;
  `}
`

You can already see how customizable this is: here you get a class for your link button which on top of that keys off of the props passed to that styled component. So a component like this might be used in a React component as follows:

function App() {
  return(
  <>
    <Button href="/reg">This is a regular button</Button>
    <Button href="/primary" primary>This is a primary button</Button>
  </>);
}

One thing that happens in this situation is that two classes are generated.

Every time you modify the CSS from the styled component you’re going to generate a new class that is applied to your element. If you’re making a lot of prop-based changes that means you’re making a lot of classes, or at least you’re trying to. You see, if you do try to make a highly dynamic class, like if you want to animate an element in CSS via properties - say if your component is data-driven or dynamic - then you’re potentially making hundreds of classes! If you’re running SSG, or trying to statically generate these classes it might just entirely break.

An example of a Highly Dynamic Component

Say you want to animate an element based on a single numerical property: xpos. It’s easy to think of a translation effect, and styled components can do it!

interface StyledAnimProps {
  xpos: number;
}

const StyledAnim = styled.div.attrs(({xpos}: StyledAnimProps) => ({
  xpos: props.xpos
}))`
  transform: translate(${(props) => props.xpos}, 0%);
`

By the way, I’m using typescript with my React and Styled Components so that interface isn’t necessary and that .attrs invocation isn’t needed, but it does afford you the ability to avoid any nasty typescript transpilation errors. If you have a better paradigm for typescript typed styled components, let me know.

That aside, if you make a component like this you’re in for a world of hurt. Hooking that component up like so

function App() {
  const [move, setMove] = useState(0);
  useEffect(() => {
    const interval = setTimeout(() => {
       if (move > 2*Math.PI) {
         setMove(.1Math.PI);
       } else {
         setMove(move + .1*Math.PI);
       }
    }, 1000);
    return () => clearTimeout(interval);
  });
  return(
    <StyledAnim xpos={Math.sin(move)} />
  )
}

If you put this somewhere, it’ll cycle through (about) 20 distinct “classes” as the item moves back and forth. That’s not great, and if you’re getting more continuous data (like floats, or data spanning 3 or 4 magnitudes), then it only gets worse.

Inline styles with Styled Components

That headline might sound like a crime, but it’s actually necessary to avoid the catastrophe of exploding the CSS generated from CSS-in-JS. The fix for our problem is to inline your styles, but the normal trick of adding style to the element itself doesn’t work by default in styled components.

    // This won't work!
    <StyledAnim style={{transform: `translate(${someVal}, 0%)`}} />

But you can do it.

By digging through the GitHub of the styled-components project and looking for others with the same problem I was able to find this thead. You’re able to apply a “style” prop to the higher-order Component via the .attrs method. For our component you’d get something like this:

interface StyledAnimProps {
  xpos: number;
}

const StyledAnim = styled.div.attrs(({xpos}: StyledAnimProps) => ({
  style: props => ({
    transform: `translate(${props.xpos}%, 0%)`
  })
}))`

This use of the .attrs method essentially bootstraps the styles into the inline styles method we usually see in React components. With this change when you open up the browser debug menu you’ll be able to see the inline styles flashing as they oscillate.

Unfortunately, this method for creating highly dynamic or often-changed components isn’t listed in the main documentation, so I’m not sure if this is the way to do it, but it avoids the problem I ran into quite neatly.

Stay Dynamic

This workaround for highly-dynamic components helped me to clean up my own code and helped with the implementation of my own interactive components. Hopefully this inspires you to use more dynamic components with CSS-in-JS libraries, or if you don’t use CSS-in-JS to give it a try!