Skip to content

Instantly share code, notes, and snippets.

@zaydek-old
Last active September 1, 2019 06:54
Show Gist options
  • Save zaydek-old/5819a7f01e685b260833fdc25606788a to your computer and use it in GitHub Desktop.
Save zaydek-old/5819a7f01e685b260833fdc25606788a to your computer and use it in GitHub Desktop.

Obscuring CSS util classes with components

Vue is an interesting framework for building on the success of a functional CSS framework. Functional as meaning not concerned with how each components is built, but rather, composed.

A component in Bulma or Fortune is often described in two steps. First, sometimes the presence of a parent or a child element is required. Such as columns and column. There's nothing wrong with this and I'm aware Fortune make some similar decisions for as far orthogonal classes go. The first step in reducing the friction for first-time developers, even experienced developers in terms of speed, is de-cluttering the HTML from the CSS.

Where in Bulma and Fortune this would be required:

<!-- Bulma -->
<div class="columns">
  <div class="column"></div>
  <div class="column"></div>
  <div class="column"></div>
</div>
<!-- Fortune -->
<div class="cols">
  <div class="col"></div>
  <div class="col"></div>
  <div class="col"></div>
</div>

Semantics aside, this could be redueced to the following using Vue:

<!-- Bulma -->
<Columns>
  <Column></Column>
  <Column></Column>
  <Column></Column>
</Columns>
<!-- Fortune -->
<Cols>
  <Col></Col>
  <Col></Col>
  <Col></Col>
</Cols>

Bear in mind that there is at least one obvious trade-off. In doing some, I'm unaware as to how to allow the developer to decide which HTML element is used in the implementation. E.g. given <Col> and <Cols> are obscuring the HTML, it needs to be decided in advance whether to proceed with div or something more semantic e.g. figure or something else. This isn't too relevant to this particular example as it depends on the class in question, but something to be aware of.

Here is a code demo: https://codepen.io/zaydek/pen/BPpVyP?editors=1000

Note in the example the intentional decision to make the components capitcal case. This is because sharing namespaces with existing HTML elements can lead to issues. Using uppercase instead obviates this issue which is a nice and subtle trick. Also note the use of <slot></slot> delimits where nested elements/content will appear. This addresses 'recombining components' which I would describe as nested components. More on this later.

Using attributes to decorate components

In this deliverable, I detail some examples of stateful components. In the previous deliverable, I just discussed how to program a component that obscures its HTML implementation. In this deliverable, I'm going to focus on stateful components that make use of an attribute to describe a desired state or combination of classes. This will be in the same spirit as the first one, focusing on reducing the friction for new programmers to create things of value.

For example:

<Button>This is a button</Button>
<Button disabled>This is a disabled button</Button>

Here is a simple demo that demonstrates one state: https://codepen.io/zaydek/pen/WKRyYN?editors=1000

One clever thing is the use of :class="{ center }" or :class="{ button }". In Vue, objects are translates into classes. But instead of binding the center or button classes to true or false, instead these are conditional to the prop center or disabled which are attributes attached to a component. While this should be :class="{ center: center }" or :class="{ button: button }", because of an ES6 feature known as Object Property Value Shorthand this can be reduced to :class="{ center }" or :class="{ button }".

Also note that while some classes require the presence of another class, much like cols and col, states are classes that characterize some parent class such as disabled characterizes button. This however does not limit other classes from being applied, as pointed out in the demo: notice how class="g-3" is being applied in addition to the disabled attribute. So this is a powerful idiom; componets expose states via attributes.

For example:

<Button>This is a button</Button>
<Button disabled>This is a  disabled button</Button>
<Button inverted>This is an inverted button</Button>
<Button outlined>This is an outlined button</Button>
<Button block>This is a block button</Button>
<Button link>This is a link button</Button>

Here is a more complex demo that demonstrates n-states: https://codepen.io/zaydek/pen/xJgzaL?editors=1000

I would recommend refactoring the button state classes to one word like disabled. I assume this will open-source development and ease-of-use. This also means Object Property Value Shorthand can be used. However, in the code demo I demonstrate how to map inconsistent naming schemes e.g. the state inverted to the class button-invert. To correct for this, I use a method classed map_classes() which binds CSS class names to attribute names. However, this is what would be possible with which doesn't also require a methods:

<div class="button" :class="{ disabled, inverted, outlined, block, link }">
  This is a button
</Button>

Extending a Component with a CSS Variable

This deliverable will be focused on extending Vue components with CSS variables. The idea is that a component should be able to be customized with a CSS variable without breaking other or influencing other components. And where a CSS variable can be used, so can a literal value, e.g. #FFAA00. To do this, we need to not override the :root { ... } declaration of CSS variable and instead scope it.

One simple and clever technique to do this is overriding the parent HTML element of the component e.g. <Button>'s parent HTML element is <div class="button">. In vanilla HTML and CSS, we can do <div class="button" style="--button-bg-color: #FFAA00"> and that scopes the variable reassignment from that scope and on, just like :root scopes from the html element and on. Here's how we can obscure this process using Vue.

First, let's predefine some CSS variables we'll use to override the idiomatic blue button. I'm borrowing these colors from Tailwind and defining them in vanilla CSS like so:

:root {
	--tw-red-lightest : #FCEBEA;
	--tw-red-lighter  : #F9ACAA;
	--tw-red-light    : #EF5753;
	--tw-red          : #E3342F;
	--tw-red-dark     : #CC1F1A;
	--tw-red-darker   : #621B18;
	--tw-red-darkest  : #3B0D0C;
}

With a slight modification to how the <Button> component works, I can now use the variable name as the attribute name and pass the name of either the CSS variable or a token, e.g. #FFAA00, like so:

<Button button-color="var(--tw-red-lightest)"></Button>

Here is an example demo: https://codepen.io/zaydek/pen/NBprjG?editors=1000

What's clever here is that because Fortune implements disabled via opacity, this also just works:

<Button button-color="var(--tw-red-lightest)" disabled></Button>

This is a little naive however, because we're just updating one variable and not a slew of CSS variables. In the follow-up deliverable, I'll demonstrate how we can refactor the code to instead pass an object as an argument and not a string.

Extending a Component with CSS Variables

In the previous deliverable we worked out how to customize an instance of a component with a custom CSS variable. This is great, but in practice developers will want to do more; override a slew of CSS variables per component. This can be done in the CSS of course, but requires extra attention and detail.

Now, I'll demonstrate how we can iterate on the attribute interface we've been using to instead of passing a string as an argument, pass a JavaScript object. The idea here is to map a component's exposed CSS variables to values to be overridden with. And we can be flexible here, of course not requiring all exposed CSS variables to be overrode but a subset.

In Vue, we can pass a string to a component like this attribute="value". If we want to do something more, we can use the smart attribute e.g. :attribute="value" to instead bind native JavaScript instead. This presents a practical means to override a slew of CSS variables. Returning to the button example, let's now work with both the button-color and button-bg-color; this being indiciative of overriding multiple CSS variables.

Given we have predefined CSS variables to use to override a component's CSS variables, e.g.:

:root {
	--tw-green-lightest  : #E3FCEC;
	--tw-green-lighter   : #A2F5BF;
	--tw-green-light     : #51D88A;
	--tw-green           : #38C172;
	--tw-green-dark      : #1F9D55;
	--tw-green-darker    : #1A4731;
	--tw-green-darkest   : #0F2F21;
}

We can then in Vue create an object to map the desired CSS variables with the component's CSS variables:

greenButton: {
  "--button-color"    : "var(--tw-green-lightest)",
  "--button-bg-color" : "var(--tw-green)"
}

Now we can pass this to Vue using the smart attribute discussed earlier, e.g. :attribute="value". Given the component handles the internal implemenation, we can now do this, in addition to the previous example (overriding one component):

<Button :custom="greenButton"></Button>

Here is an example demo: https://codepen.io/zaydek/pen/mjWrow?editors=1000

One of the benefits of the benefits of obscuring one-off variables and instead using an object is the implicit refactoring that takes place. Just like how :class="{ disabled }" just works because of ES6, so does :style="custom" without additional inteference, which cannot be said about one-off CSS variable binding, and rather binding to an object that maps to either CSS variables or literal values e.g. #FFAA00.

Asserting Component States

In CSS there is an idiom for describing states using psuedo-classes. This is ... OK because while the implementation is handled, similar to how text-decoration: line-through is handled for us without us needing to draw a line and so on, we also need to deal with whatever limitations that imposes. Rather, if we think about states in terms of pure CSS classes instead, e.g. not psuedo-classes, than we can achieve a lot more if we can bind CSS classes to our app's state or the data. This is what's otherwise known as 'reactive' because our app is reacting.

First, let me provide a simple and clear example that demonstrates this. Here is a simple enough example that demonstrates that our app's background color is either 'on' or 'off'. We can open the developer tools and set a boolean to either true or false to set the background. This is simple and concise and demonstrates that we need not be concerned with the implementation, rather, the higher-level abstraction of our app resolving itself.

Note that the following demo relies on the presence of two vanilla CSS classes, .on and .off:

.on  { background: green ; }
.off { background: yellow; }

Then in order to create a relationship between these classes and our app's state, I've defined this method:

reactive_background() {
  return {
    on  :  this.on,
    off : !this.on
  }
}

Now to toggle our app's background, I can open the developer tools and enter the following into the console:

app.on = false // applies .off e.g. yellow
app.on = true  // applies .on  e.g. green

Here is an example demo: https://codepen.io/zaydek/pen/JBWbXK

This is reactive class binding. Instead of making a function that updates an element's classList, I can think about vanilla CSS classes and toggle them dependent on our app's state. Where this becomes interesting and of value is that vanilla CSS classes can also be used to represent state. If we remove the abstraction of psuedo-classes altogether, and just concern ourself with CSS classes, instead we can leverage Vue for the implicit, reactive implementation.

Taking the disabled psuedo class, let's intead make it both a normal CSS class and a psuedo-class. This means we can optomize for both CSS and Vue developers irrespective of either. Because :disabled is defined Fortune, I'm just going to add the following class:

.button.disabled {
	opacity: 0.2;
	text-decoration: none;
	opacity: .5;
	cursor: not-allowed;
}

I am aware both the psuedo and class are defined in Fortune, but this is indicative for all states, e.g. :hover, etc.

Here is a working demo: https://codepen.io/zaydek/pen/EpWNXy

The code is a little terse but here's how it works. When the button is clicked, it updates both the button text and the CSS class/es being applied to the button. Clicking it again will reverse the toggle back to it's normal state. The point being that because of Vue, psuedo-classes are overhead and not essential for describing a state. Using JavaScript instead, states can be discerned with its various v-bind:event or @event handlers. In this example, I'm making use @click="toggle()". Vue supports mouseover and mouseout among others.

Generative, Semantic Font Weights and Sizes

One thing I've struggled with for a long time is which font-weight and -size to use. It seems like there should be some golden number, or golden ratio (well––there is...), and that if I could just find it, some website would like perfect across all browsers and devices. I realized that's a bit naive, but I came up with a clever, build-free solution that makes use of native CSS techniques, CSS variables, and the infamous calc.

Looking at the sources for Tailwind and Bulma, each have their own idea for what the idealistic font-sizes are:

/*
 * Tailwind
 */
.text-xs   { font-size: 0.750rem; }
.text-sm   { font-size: 0.875rem; }
.text-base { font-size: 1.000rem; }
.text-lg   { font-size: 1.125rem; }
.text-xl   { font-size: 1.250rem; }
.text-2xl  { font-size: 1.500rem; }
.text-3xl  { font-size: 1.875rem; }
.text-4xl  { font-size: 2.250rem; }
.text-5xl  { font-size: 3.000rem; }

/*
 * Bulma
 */
is-size-7 { font-size: 0.75rem; }
is-size-6 { font-size: 1.00rem; }
is-size-5 { font-size: 1.25rem; }
is-size-4 { font-size: 1.50rem; }
is-size-3 { font-size: 2.00rem; }
is-size-2 { font-size: 2.50rem; }
is-size-1 { font-size: 3.00rem; }

To me, there's something wrong with this. How can both be right?? It's not possible. So if God is in the details (so to speak), perhaps rather than emphasize a set of 'perfect' numbers which seems illogical, what if could generate the perfect set of numbers for a given need. This is known as design based on constraint, where we let the computer do that hard bit, and we just indicate what the parameters are.

When I think about this, the parameters seem obvious: what is the minimum and maximum font sizes I care for? That, and what amount of font-sizes do I want? From working with Tailwind, I've learned how much I appreciate having orthogonal class names like these:

.smallest { ... }
.smaller  { ... }
.small    { ... }
.normal   { ... }
.large    { ... }
.larger   { ... }
.largest  { ... }

Now because I'm also going to talk about font-weight, .normal is going to become ambiguous fast. So instead, let's prefix font-size as size and font-weight as weight. Here's how our idealogical class names can follow:

.weight-lightest { ... }
.weight-lighter  { ... }
.weight-light    { ... }
.weight-normal   { ... }
.weight-heavy    { ... }
.weight-heavier  { ... }
.weight-heaviest { ... }

.size-smallest   { ... }
.size-smaller    { ... }
.size-small      { ... }
.size-normal     { ... }
.size-large      { ... }
.size-larger     { ... }
.size-largest    { ... }

This is fascinating to me because now I can describe CSS with the same ease as describing an object in the real world, with a limited but expressive set of terms adapted for each circumstance. This can also be extended for color, too! For example, *-lightest through *-darkest and so on. OK so now how do we generate our font-weights font-sizes given minimum and maximum parameters?

I came up with a recursive solution. The equation is simple: For 7 steps (e.g. *-lightest through *-heaviest), iterate through all the values using a linear function:

/*
 * weight (font-weight)
 */
:root {
	--min-weight: 400;
	--max-weight: 900;

	--weight-lightest : calc(var(--min-weight) + 0/6 * (var(--max-weight) - var(--min-weight)));
	--weight-lighter  : calc(var(--min-weight) + 1/6 * (var(--max-weight) - var(--min-weight)));
	--weight-light    : calc(var(--min-weight) + 2/6 * (var(--max-weight) - var(--min-weight)));
	--weight-normal   : calc(var(--min-weight) + 3/6 * (var(--max-weight) - var(--min-weight)));
	--weight-heavy    : calc(var(--min-weight) + 4/6 * (var(--max-weight) - var(--min-weight)));
	--weight-heavier  : calc(var(--min-weight) + 5/6 * (var(--max-weight) - var(--min-weight)));
	--weight-heaviest : calc(var(--min-weight) + 6/6 * (var(--max-weight) - var(--min-weight)));
}

/*
 * (size) font-weight
 */
:root {
	--min-size: 1rem;
	--max-size: 4rem;

	--size-smallest : calc(var(--min-size) + 0/6 * (var(--max-size) - var(--min-size)));
	--size-smaller  : calc(var(--min-size) + 1/6 * (var(--max-size) - var(--min-size)));
	--size-small    : calc(var(--min-size) + 2/6 * (var(--max-size) - var(--min-size)));
	--size-normal   : calc(var(--min-size) + 3/6 * (var(--max-size) - var(--min-size)));
	--size-large    : calc(var(--min-size) + 4/6 * (var(--max-size) - var(--min-size)));
	--size-larger   : calc(var(--min-size) + 5/6 * (var(--max-size) - var(--min-size)));
	--size-largest  : calc(var(--min-size) + 6/6 * (var(--max-size) - var(--min-size)));
}

Here is a working demo: https://codepen.io/zaydek/pen/EpWMQo?editors=1000

This surprises and interests me because for a few reasons. The scaling function doesn't have to be linear, and in the circumstance that vanilla CSS can't perform a scaling function, for example with color, a preprocessor can be extended to generate the ouput and still be based on this architecture of just indicating the extremes and letting the computer (not the programmer/designer) resolve the design.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment