Skip to content

Dynamic CSS Components Without JavaScript

Publish date: Author: Heydon

What is a component? In electronics, it's a discrete device used to affect the behavior of the electricity in a circuit, or electrical system. Electronic components are modular, and are often used in combination to achieve specific values. For example, a number of resistor components of different values might be used in series to produce a desired resistance.

Electronic components take an input, augment it internally, and return a different output. In this respect, the only real conceptual difference between electronic components and web interface components is that the former passes the signal along, and the latter passes it down.

Props

Whether you are using React, Vue, custom elements, or some other component-organized system, inputs tend to be passed to the component via props (properties). The property names/identifiers belong to the component; the values of the properties belong to a containing (ancestor) component and its current state.

A UI component outputs (using a return statement in JavaScript) some UI, and the shape this UI takes is, at least partially, beholden to the values of the props originally passed. Like resitors augment the current, UI components augment the interface.

Rendering

On the web, the UI a component outputs consists of markup/HTML. The structure of that HTML will differ depending on how the current values of its props are interpolated. For example, if a hypothetical users prop consisted of an array of 726 users in its current state, the following render function would output an unordered list of 726 users. Array of 891; list of 891.

const render = () => {
return `<ul>
${prop.users.map(user => `
<li>${user.name}</li>
`
).join('')}

</ul>`
}

What's neat is that I have a single component, as one module definition, that can be instantiated under differing circumstances and produce different outcomes. For example, I might use the same component just to display logged in users. All that has to change is the input: the value of the users prop.

CSS components

When it comes to CSS, it would probably be beneficial to work in a similar way: leverage a master styling module (née a stylesheet), and provide props just to tweak certain values.

This is the philosophy of the layout primitives that manifest Every Layout's layouts. But the interpolation of prop values is achieved using template literals inside custom element definitions (read: JavaScript). They are only partly “JavaScript independent” because server-side rendering applies the initial styles. Subsequent changes to styling props, on the client, mean re-rendering those styles by manipulating the DOM. This is from the Stack layout:

document.head.innerHTML += `
<style id="${this.i}">
[data-i="${this.i}"]${this.recursive ? '' : ' >'} * + * {
margin-block-start: ${this.space};
}

${this.splitAfter ? `
[data-i="${this.i}"]:only-child {
block-size: 100%;
}

[data-i="${this.i}"] > :nth-child(${this.splitAfter}) {
margin-block-end: auto;
}`

: ''}
</style>
`.replace(/\s\s+/g, ' ').trim();

(There are reasons why we don't use Shadow DOM.)

Since only JavaScript can affect the prop/attribute that sets the re-render in motion in the first place, one could argue this setup already hangs together as well as can be expected. But what if we could do something similar with just CSS and HTML attribution? It would certainly be less intensive.

Custom properties

Custom properties, unlike static Sass variables, work with the cascade. They can be set globally, but also locally and contextually. Typically, we do this all in a stylesheet file away from our component's HTML. Take the following example, which uses custom properties to manage “separator” symbols applied between adjacent/inline list items.

/* start template */
.separated > * {
list-style: none;
display: inline;
}

.separated > * + *::before {
content: var(--symbol, '→')'\0020';
}
/* end template */

/* start instance */
.separated.hand {
--symbol: '☞'
}
/* end instance */

The first two of the three declarations is a kind of template (as commented). It takes care of all the gnarly CSS, as well as applying a default value (). When defining my special hand-based separator component, all I have to do is supply a property representing my chosen symbol. This is analogous to templating HTML using props and JavaScript string interpolation.

Except for one thing: .separated.hand is not an instance of a component, so much as an identifier for a possible/proposed instance of a component. It is not CSS I will necessarily need, and would represent bloat where the class="separated hand" component is retired.

Instead, why don't I just apply the symbol part in the HTML, as needed?

<ul class="separated" style="--symbol: '☞'">
<li>First separated item</li>
<li>Second separated item</li>
<li>etc.</li>
</ul>

This works without any JavaScript (on the client or server). It's just CSS.

But it's also highly efficient for client runs/renders where it is a part of HTML templating (think JSX). Whereas a typical CSS-in-JS solution uses JavaScript interpolation to template a stylesheet and append it to the DOM, all that has to change here is the symbol itself. The CSS “module” will already be loaded and, since it doesn't need to be manipulated directly, can also be cached.

return `
<ul class="separated" style="--symbol: {symbol}">
<li>First separated item</li>
<li>Second separated item</li>
<li>Setc.</li>
</ul>
`
;

Moreover, you can affect the props directly in your browser's dev tools, by editing the style property's HTML. Because we felt this would be a handy feature for exploring and prototyping with the Every Layout layouts, we built this feature into the custom elements. We had to specify getters and setters, and observe prop changes with the attributeChangedCallback. None of that is necessary when the props are just CSS, as in the method proposed here.

CSS is reactive by nature, in a way JavaScript can only aspire to be.

Multiple properties

Of course, you can use multiple properties if you need. Maybe I'd like to display my list as a CSS Grid and control the grid-template-columns and grid-gap properties…

<ul class="grid" style="--columns: 20ch 1fr 50px; --gap: 2rem">
<li>First separated item</li>
<li>Second separated item</li>
<li>etc.</li>
</ul>

Again, the basic CSS grid CSS file (including display: grid, align-items: center and whatever else is constant) can be cached and reused, without having to forego the props-as-inputs authoring model. The stylesheet acts just like a JavaScript module for a function, accepting (in this example) two arguments.

Perhaps you'd like to pass multiple props around in objects? Because that's easier to read and work with in JS? No problem: a small mapping function can convert from an object into a custom property style string with ease:

const toCustomProps = obj => {
return Object.entries(obj).map(entry => {
return `--${entry[0]}:${entry[1]};`
}).join('')
}

const styleProps = {
columns: '20ch 1fr 50px',
gap: '20rem'
}

let styleString = toCustomProps(styleProps)
// → '--columns:20ch 1fr 50px;--gap:20rem;'

Since we are only dealing with custom properties, for which we can choose any name, we don't have to worry about things like the difference between CSS's background-color and JavaScript's equivalent backgroundColor.

Portability

In many ways, the CSS components I'm proposing are like Every Layout's custom element layout primitives. But because they use class names instead of element definitions, they are more portable.

They can be applied to any element or component, inluding any semantic HTML. One of the shortcomings of custom elements is that they are unsemantic by default, meaning you have to bolt on semantics using ARIA. In previous examples, I used <ul> and <li>. Using custom elements instead, I'd have to apply the list and listitem roles.

<my-list role="list">
<my-listitem role="listitem">...</my-listitem>
<my-listitem role="listitem">...</my-listitem>
<my-listitem role="listitem">...</my-listitem>
</my-list>

Limitations

I think there's a lot of potential in this approach. I like how it gives granular control over styling without encouraging the proliferation of either manually-written classes or JavaScript generated selectors.

The glaring, though not necessarily deal-breaking, limitation is that only CSS property values can be affected. I can't interpolate custom properties into CSS selectors. The ability to write selectors dynamically, just in CSS, would make for an interesting feature.

.stripes > :nth-child(var(--pattern, 'even')) {
background-color: grey;
}

.stripes.odd {
--pattern: 'odd';
}

If you find yourself wrestling with CSS layout, it’s likely you’re making decisions for browsers they should be making themselves. Through a series of simple, composable layouts, Every Layout will teach you how to better harness the built-in algorithms that power browsers and CSS.

Buy Every Layout