Navigation

Styled components in a virtual DOM

At Lookback, we’ve re­cently tried out us­ing CycleJS for a web client code base. CycleJS is all about cyclic re­ac­tive func­tional streams and promises nice sep­a­ra­tion of con­cerns by sep­a­rat­ing out side ef­fects and han­dling of ex­ter­nal APIs into some­thing called Drivers. It uses a vir­tual DOM, like React, to trans­late app state to user in­ter­face. Add a dash of TypeScript, and you’ve got a re­ally nice and tight web fron­tend setup.

My in­ter­est for both fron­tend ar­chi­tec­ture and de­sign sys­tems made me see an op­por­tu­nity to cre­ate styled com­po­nents for for re-use in the vir­tual DOM. The idea is to con­struct small, re-us­able com­po­nents to use in­stead of mark­ing up con­tent with the reg­u­lar ap­proach of us­ing CSS classes. The key in our ap­proach here is­n’t in­line CSS em­bed­ded on the com­po­nent, but about ap­ply­ing func­tional CSS classes.

I once held a pretty strong opin­ion that one should sep­a­rate markup and styling of a web page. That works out pretty good for web con­tent with doc­u­ment style con­tent — just like all the early web pages were. When build­ing com­plex in­for­ma­tion ar­chi­tec­tures in ever chang­ing web apps, where the cas­cad­ing part of CSS just gets in your way, I’ve turned to in­ves­ti­gate this func­tional CSS class ap­proach in­stead. There’s writ­ing on this phi­los­o­phy else­where:

Be sure to check out the Tachyons CSS li­brary (I’ve based Lookback’s in­ter­nal func­tional CSS li­brary off Tachyons’ struc­ture).

A primer on func­tional CSS

Let’s say we’ve got these CSS rules:

// headings.scss
h1 {
margin-bottom: $spacing-base;
}
// modals.scss
.modal {
h1 {
margin-bottom: $spacing-base / 3;
}
form {
text-align: center;
}
}

And here we’ve got the markup for modal con­tent (here the sce­nario of cre­at­ing a new pro­ject in­side of a generic web app):

<div class="modal">
<h1>New Project</h1>
<form>
<input name="project-name" placeholder="Project name">
<input type="submit" value="Create">
</form>
</div>

This will make all H1 head­ings in all modals have tighter bot­tom mar­gin, and make all forms in all modals have cen­tered con­tent. This is prob­a­bly fine to start off with. But what if I’d like to have more mar­gin on a H1 head­ing in one cer­tain modal?

I’d ei­ther:

  1. Introduce an­other name­space on that modal, per­haps .modal-some-name and ap­ply more mar­gin on all h1 el­e­ments in that name­space.
  2. Introduce a new class name on those h1 el­e­ments that need more mar­gin, and ap­ply it from CSS.

Both of these so­lu­tions tight­ens the cou­pling be­tween the markup and CSS, and for­ever cre­ates a de­pen­dency from the for­mer on the lat­ter. Meaning, dur­ing it­er­at­ing on the prod­uct, I as a fron­tend de­vel­oper will be forced to go back and forth be­tween the markup and CSS when re­quire­ments change, fea­tures are added, and view hiearchies are refac­tored. In my ex­pe­ri­ence, there’s very few changes in the markup that also don’t re­quire ad­just­ments in the CSS — even for ex­tremely well en­gi­neered fron­tends.

Enter func­tional CSS.

Using func­tional CSS is to de­part from the clas­si­cal thought of No styling in the markup”. We’re not em­bed­ding in­line styles per se, but we’re ap­ply­ing styling in­for­ma­tion that does not make any se­man­tic sense from a markup per­spec­tive.

This is the above ex­am­ple refac­tored to use func­tional CSS classes:

// headings.scss
h1 {
margin-bottom: $spacing-base;
}
// spacing.scss
.mb0 {
margin-bottom: 0;
}
.mb1 {
margin-bottom: $spacing-small;
}
// ...
// text-align.scss
.tc {
text-align: center;
}
.tl {
text-align: left;
}
.tr {
text-align: right;
}
<div class="modal">
<h1 class="mb0">New Project</h1>
<form class="tl">
...
</form>
</div>

Note how the classes form a sort of do­main spe­cific lan­guage in de­scrib­ing which style rule or rules that are ap­plied. With func­tional CSS, I’m free to com­bine styles on el­e­ments by com­po­si­tion, as well as de­vi­ate from com­mon styling wher­ever I need to. I can use a .f1 class to de­note the largest font size, and bind it to a Sass vari­able:

// typography.scss
.f1 {
font-size: $font-size-1;
}

So if I need to change font sizes all over the the web app, it’s still easy since we haven’t em­bed­ded any in­line CSS — just en­gi­neer­ing every­thing with classes and vari­ables.

Components with func­tional CSS in a vir­tual DOM

Back to the com­po­nents part. During the de­vel­op­ment of said web client, I iden­ti­fied com­mon pat­terns from the de­sign mock­ups. It could be a cer­tain style for a form la­bel or head­ing. They were pre­sent fre­quently enough that war­ranted some kind of Don’t Repeat Yourself strat­egy, but still not im­por­tant enough for cus­tom rules in the stylesheet.

Two as­pects in this setup are quite cru­cial:

  1. Thanks to a very im­por­tant part of func­tional CSS — com­po­si­tion — we can put to­gether sev­eral styles into one, with­out hav­ing the need to cre­ate a new CSS com­po­nent for it.
  2. The pro­gram­matic na­ture of vir­tual DOMs. Markup in the vir­tual DOMs of React and CycleJS are re­ally just func­tion calls, with the sig­na­ture (selector, props, children).

Let’s talk more about each one real quick:

Composition

Thanks to min­i­mal func­tional CSS classes, we can do things like this:

<label class=".f7.tracked.c-muted.mb1.b">Foo</label>

This kind of cryp­tic class name string trans­lates to:

  • f7- Font size of level 7 (smallest)
  • tracked- Smallest let­ter spac­ing
  • c-muted- Muted colour
  • mb1- Bottom mar­gin of level 1
  • b- Bold font weight

All these prop­er­ties to­gether make up a re-us­able style stack.

Markup as func­tions

The vir­tual DOM in CycleJS is called Snabbdom. In CycleJS, it looks like this:

import { div, h1, p, strong } from '@cycle/dom';
/*
This becomes:
<div>
<h1 id="myId" class="my-heading some-other-class">Hello world</h1>
<p><strong>This is bolder text</strong> followed by a regular text node.</p>
</div>
*/
const vdom = div([
h1('#myId.my-heading.some-other-class', 'Hello world'),
p([
strong('This is bolder text'),
'followed by a regular text node.'
]);
]);

The div, h1, p, and strong func­tions are helpers from the DOM lib of CycleJS. They fol­low the sig­na­ture (selector?, props?, children?). The selector pa­ra­me­ter is a CSS style se­lec­tor string, which is used to ap­ply IDs and class names.

This way of build­ing user in­ter­faces was te­dious at first for a sea­soned HTML coder, but af­ter a while, treat­ing DOM el­e­ments like func­tions be­came fluid. Not be­ing held back by some stu­pid con­straints of the tem­plat­ing li­brary you’re us­ing, you can use all your Javascript skills to con­struct com­po­nents.

Reusable com­po­nents

I thought to my­self, If styling with func­tional CSS is only about ap­ply­ing small, atomic classes, and classes in Snabbdom is just a se­lec­tor string, I could store the classes a strings some­where and just im­port them and use them in the VDOM”.

Take One

This be­came my first it­er­a­tion:

// styles.ts
// Keep shared styles in this dict.
export const Styles = {
SmallFormLabel: '.f7.ttu.comp-blue-f.mb1',
TopHeading: '.lh-title.mb4',
};
// SomeComponent.ts
import { h1, label, form, input } from '@cycle/dom';
import { Styles } from './styles';
export default function SomeComponent(props) {
const vdom = form([
h1(Styles.TopHeading, 'My Form'),
label(Styles.SmallFormLabel, { for: 'name' }, 'Label'),
input({ type: 'text', id: 'name', placeholder: 'Name' }),
]);
return vdom;
}

This makes it pos­si­ble to con­trol the ex­act ap­pear­ance of a SmallFormLabel from within the styles.ts file. Change there — change every­where!

Take Two

This was quite fine, but still did­n’t feel com­po­nent-y enough. What about ex­ten­si­bil­ity? If I wanted to ap­ply more styles to a TopHeading, I’d have to do an ES6 style string lit­eral ${Styles.TopHeading}.more-classes and ap­ply as se­lec­tor. Not that el­e­gant, and a lot to type.

Since VDOM el­e­ments are func­tions, we can im­ple­ment a back­ing func­tion which en­hances a VDOM el­e­ment with a given se­lec­tor, and re­turns the el­e­ment ready to be used.

The sig­na­ture would look like:

function enhanceWithStyle(domTag: DomTag, classes: Selector): DomTag;

where we’ve got the types:

type Selector = string;
// This is the signature for a Snabbdom helper, like h1(), p(), etc.
type DomTag = (sel?: Selector | any, ...args: any[]) => VNode;

Let’s en­hance!

// styles.ts
import { label, h1 } from '@cycle/dom';
const Styles = {
SmallFormLabel: '.f7.ttu.comp-blue-f.mb1',
TopHeading: '.lh-title.mb4',
};
export const SmallFormLabel = enhanceWithStyle(label, Styles.SmallFormLabel);
export const TopHeading = enhanceWithStyle(h1, Styles.TopHeading);
// SomeComponent.ts
import { form, input } from '@cycle/dom';
import { SmallFormLabel, TopHeading } from './styles';
export default function SomeComponent(props) {
const vdom = form([
TopHeading('.some-other-class', 'My Form'),
SmallFormLabel({ for: 'name' }, 'Label'),
input({ type: 'text', id: 'name', placeholder: 'Name' }),
]);
return vdom;
}

Voíla! We can use our cus­tom com­po­nents just like any other, since it uses the sig­na­ture (selector?, props?, children?). Composable and re-us­able.

Implementation

The im­ple­men­ta­tion for the en­hance func­tion is:

// styles.ts
import { VNode } from '@cycle/dom';
// specific type for selectors
export type Selector = string;
// This is the signature for a Snabbdom helper, which we need to also
// export if we use it ...
export type DomTag = (sel?: Selector | any, ...args: any[]) => VNode;
const isSelector = (str?: string): str is Selector =>
typeof str === 'string' &&
str.length > 1 && // A selector with only a dot doesn't make sense. Require > 1 chars
str[0] === '.'; // Starts with a dot, like '.className'
export const concatSelectors = (...ss: Selector[]): Selector =>
ss.filter(isSelector).join('');
/**
* Enhance an existing Snabbdom helper with a set of style classes,
* in order to DRY things up.
*
* Example:
*
* import { enhanceWithStyle } from './libs/styles';
*
* // Enhance the label component from Snabbdom:
* const SmallLabel = enhanceWithStyle(label, '.some-class.another');
*
* // Use in the DOM:
* SmallLabel('.more-classes', 'My Label');
*/
export const enhanceWithStyle = (domTag: DomTag, classes: Selector): DomTag => (
sel: any,
...args
) => {
const tagArgsToPass = isSelector(sel)
? [
// Apply our classes, and append any custom selector passed, if it's a string.
concatSelectors(classes, sel),
...args,
]
: [
classes,
sel, // sel isn't a selector here, treat it as an any argument to the Hyperscript helper
...args,
];
return domTag(...tagArgsToPass);
};

Conclusion

What I like with this ap­proach is the sim­plic­ity: many peo­ple un­der­stand the con­cept of com­po­si­tion. I’m sure this kind of en­hance­ment func­tions ex­ist for React and other vir­tual DOMs, but this re­ally is some­thing you can hack to­gether on your own, since it’s just” func­tions!

What we’ve achieved is:

  • Re-usability of styles in the shape of small VDOM helpers.
  • Isolation of CSS styling into the .css file — not the app logic.
  • Composition of style rules with­out hav­ing to deal with cas­cade headaches.

Thanks for read­ing!