Unconventional React Things: Styling Edition
Marcus Buffett
Posted on August 11, 2020
I've developed an unusual approach to styling in my React code. It's sort of a functional, atomic css approach. Other methods I've used are BEM + separate css files, styled components, and tailwind. By far this is the one that lets me move fastest on my personal projects (I haven't yet subjected anybody at work to dealing with it). Without further ado.
Element Styling
const buttonStyles = s(caps, px(s6), py(s5), clickable);
const activeStyles = s(buttonStyles, bg("green"), fg(light0));
const inactiveStyles = s(buttonStyles, bg(light0), shadow);
return (
<Row style={s(fullWidth, alignCenter)}>
<div style={s(weightSemiBold, f1, fg(dark6))}>Do you have a PS4?</div>
<Spacer width={s7} />
<div style={s(roundedLeft, hasPS4 ? activeStyles : inactiveStyles)}>
yes
</div>
<div style={s(roundedRight, hasPS4 ? inactiveStyles : activeStyles)}>
no
</div>
</Row>
);
The s
function is at the heart of the approach. It just takes objects and merges them:
export const s = (...args) => _.assign({}, ...args);
To feed into s
, I've got a collection of atomic styling objects that can be pieced together, stuff like:
// Helper for constructing styles
const keyedProp = (key: string) => (x: any) => {
return {
[key]: x,
};
};
export const fullWidth = keyedProp("width")("100%");
export const clickable = keyedProp("cursor")("pointer");
export const weightRegular = keyedProp("fontWeight")(400);
export const weightSemiBold = keyedProp("fontWeight")(500);
export const weightBold = keyedProp("fontWeight")(600);
export const flexible = s(keyedProp("flexBasis")(0), keyedProp("minWidth")(0));
export const fg = keyedProp("color");
export const bg = keyedProp("backgroundColor");
export const caps = {
textTransform: "uppercase",
letterSpacing: "0.03rem",
};
export const size = (x: string | number) => {
return s(height(x), width(x));
};
There's a lot more. Sometimes they take a parameter, sometimes they don't, and all return style objects that can be fed into the style
jsx attribute.
Anyone reading that has used tailwind probably recognizes this sort of style, of having a bunch of smaller rules that combine. There's a few benefits to it being in code instead:
- Compile-time or runtime errors (depending on your setup). In tailwind if you type
mr_4
accidentally instead ofmr-4
, you won't notice unless you see that the page isn't laid out correctly. With this approach and Typescript I get an error right away. - Tailwind's approach to being able to specify the background color as any of 100 colors is to create 100 CSS classes. You can get caught with a pretty large CSS file, and you can't use dynamic colors. You get a similar usage here, but less drawbacks, instead of
bg-green
you writebg("green")
, and if you want dynamic you can dobg(someColorVar)
.
Another advantage of this approach is that it's very ergonomic, and that's partly because these style objects form a monoid under the s
function. That sounds more complicated than it actually is, and has real benefits. First of all, it means there's an identity element, like 0 in addition or 1 in multiplication. In this case it's the empty object {}
. This identity element is useful when you want nothing to happen, ex:
<div style={s(size(32), isActive ? bg("green") : {})} />
Because we have an identity element we can put {}
in there for the case where we don't want anything to happen to the previous style when isActive
is false
. The other nice feature is that since s
returns the same type as its input, we can use the output in a new call to s
:
const regularButton = s(clickable, px(8), py(4));
// We can use the above style by itself, or we can
// feed it into `s` again to create a new style
const boldButton = s(regularButton, weightSemiBold);
Just to round out the monoid thing for anyone curious what else makes it a monoid, s
is associative, but there's nothing useful about that.
Inline styles are bound to look ugly to some people. There's nothing as clean as styled components markup. But that cleanliness comes at a cost. I find having to do two passes when writing markup to be a pain, since you have to write the markup then go make the components. It can also be hard to figure out how the styling is working based on the markup, having to jump back and forth between the markup and the definitions for complex layouts.
Spacing
A while back I read a comment or article on Hacker News that that spacing should be seen as its own element instead of properties of the other elements on the page. I found myself agreeing on principal, and I've seen benefits from putting it into practice. If you've got a header, followed by 16px of space, followed by body text, the traditional approach would be to give a margin-bottom
of 16px
to the header. What I've been doing instead is this:
<div> Header </div>
<Spacer height={16}/>
<div> Body </div>
I've found this to be easier than giving margins to a bunch of my elements. It also makes the non-spacing elements more re-usable, since I don't have to modify the spacing when I copy them over to some new page or component.
This post was originally published on my blog
Posted on August 11, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.