Delegating Styles
Video Summary
Let's suppose we're building a low-level component, like a Button, DatePicker, or Slider.
For low-level components like this, we often want to make sure that the consumer can apply custom styles. Low-level components tend to get used a lot, in lots of different circumstances, as part of different designs, and so odds are we'll need to tweak them to fit in.
In this example, we're going to build a VolumeSlider
component that consumes a low-level Slider
component, and we want to update it so that we can supply custom styles for the slider's handle.
There are multiple ways to do this. Let's look at two options.
Option 1: Granular props
function Slider({ label, handleSize, handleColor, handleActiveColor, ...delegated}) { const id = React.useId();
return ( <div className={styles.wrapper}> <label htmlFor={id} className={styles.label}> {label} </label> <input {...delegated} type="range" id={id} className={styles.slider} style={{ '--handle-size': handleSize, '--handle-color': handleColor, '--track-size': trackSize, }} /> </div> );}
In this version, we expose 3 cosmetic-related props: handleSize
, handleColor
, and handleActiveColor
. The consumer can pass these props like so:
<Slider handleSize={12} handleColor="green" handleActiveColor="lightgreen"/>
This is a perfectly valid option, but it's not the only one. Let's consider an alternative:
Option 2: supplying a custom className
function Slider({ label, className = '', ...delegated}) { const id = React.useId();
const sliderClassName = `${styles.slider} ${className}`;
return ( <div className={styles.wrapper}> <label htmlFor={id} className={styles.label}> {label} </label> <input {...delegated} type="range" id={id} className={sliderClassName} /> </div> );}
In this option, we create a merged sliderClassName
by combining the built-in styles with any user-provided ones.
To apply the custom styles, the consumer could do this:
<Slider className={styles.volumeSlider} />
My question for you: Which option do you prefer, and why? What are the pros/cons of each approach?
Here's the sandbox from the video, for you to tinker with:
Code Playground
Alright, let's talk about the tradeoffs!
The first thing I should say is that there is no consensus in the React community. People have strong opinions about this.
The big difference between these approaches is how much power is granted to the consumer.
With the className option, developers can apply any CSS they want to the <input>
tag. For example, with a bit of clever CSS, we could force it to be a vertical slider:
Code Playground
By exposing the className
prop, we grant the consumer a lot of control. With individual props, we limit that control.
Is this a good thing or a bad thing? Well, that's where opinions diverge. Let's look at both sides, and I'll share my personal opinion afterwards.
The arguments for specific props
When building low-level components, we're likely implementing them according to a design system.
As we learned earlier, a design system is a document that provides guidelines and rules for using each component. It specifies which changes are allowed, and which changes are not allowed. The components we implement should only allow the customizations specified in the design system.
In other words, we want to make sure that developers “color within the lines”. By exposing a className
prop, a rogue developer could radically change how this component is styled, bending it into whatever shape they like, in violation of the design system.
A professional-looking app is one that is consistent, where each Slider instance is part of a visual set. With the className
prop, the app will eventually lose that consistency.
Components are meant to encapsulate markup, logic, and styles. If developers can apply any styles they want, then we don't truly have encapsulation. What's the point of having a design system if the developer can do whatever they want with the aesthetic?
Design systems are living, breathing documents, and we can always update them as requirements change. But it's chaos to allow developers to apply any CSS they'd like to these components.
The arguments for a “className” prop
In the real world, the "specific props" approach tends to become completely unwieldy.
In our example above, we added 3 props, but this is only the tip of the iceberg. For example, what if we want to allow the consumer to specify a hover color? Or, what if we want to customize the handle size, but only for a specific media query?
CSS is a huge, sprawling language. We could wind up with 50+ props, which would be a nightmare to maintain, and no fun at all to consume. Each new prop is another Jenga™ brick being added to the top of the tower.
Also, designers have a knack for creating exceptions and one-offs. It won't be long until they come up with a legitimate use case that doesn't work with our specific props. And then what?
Here's what tends to happen: developers will “reach around” React altogether, and apply whatever CSS they need:
/* HACK: Apply rotation to Slider component */.some-wrapper form input[type="range"] { margin: 50px !important; transform: rotate(90deg) !important;}
Remember, we can't literally stop developers from applying CSS to a particular element! Whether or not we have a className
prop, a determined developer can still apply whatever styles they want.
Or, maybe even worse, they'll decide to create their own component, SliderAlt
, that is 95% the same, but different in this one regard. Some codebases have multiple near-identical components because the prop interface wasn't flexible enough for them.
It's just not realistic to come up with a handful of style-related props that will work for every possible use case. The real world is too messy for that.
My take
Alright, I've done my best to summarize both sets of arguments! But what do I think?
I've worked on teams that have tried to restrict the ability to apply custom styles, and it just doesn't work. The real world really is too messy, and CSS really is too big and sprawling. So I like to add a className
prop.
It's true that a rogue developer can apply whatever CSS they want, bending the component into an unrecognizeable shape… but why would they do that?
In my experience, it's designers who sometimes want to bend the rules of the system, in order to create the best possible user experience. Usually, it's a small tweak, something that still fits within the spirit of the design system.
What about consistency? Personally, I think polish and UX is more important than consistency. I don't want a worse user experience because we got boxed in by the design system! Obviously, we don't want it to be so inconsistent that the project looks like a disorganized mess, but I trust our design friends not to push things that far.
A lot of this does come down to trust. No matter what restrictions we try and put into place, a determined developer will be able to work around them. But we should trust our teammates!
But yeah, that's just my opinion, and like I say, there isn't a consensus on this topic! I'd encourage you to experiment with different patterns and see what works best.