CSS

tosijs provides a collection of utilities for working with CSS rules that help leverage CSS variables to produce highly maintainable and lightweight code that is nonetheless easy to customize.

The design goal of css, vars, and varDefault is to make working with CSS3 more intuitive, maintainable, easier, and expressive than using libraries like Tailwind.

The basic goal is to be able to implement some or all of our CSS very efficiently, compactly, and reusably in Javascript because:

The css module attempts to implement all this the simplest and most obvious way possible, providing syntax sugar to help with best-practices such as css-variables and the use of @media queries to drive consistency, themes, and accessibility.

css(styleMap: XinStyleMap): string

A function that, given a XinStyleMap renders CSS code. What is a XinStyleMap? It's kind of what you'd expect if you wanted to represent CSS as Javascript in the most straightforward way possible. It allows for things like @import, @keyframes and so forth, but knows just enough about CSS to help with things like autocompletion of CSS rules (rendered as camelcase) so that, unlike me, it can remind you that it's whiteSpace and not whitespace.

import {elements, css} from 'tosijs'
const {style} = elements

const myStyleMap = {
  body: {
    color: 'red'
  },
  button: {
    borderRadius: 5
  }
}

document.head.append(style(css(myStyleMap)))

There's a convenient Stylesheet() function that does all this and adds an id to the resulting <style> element to make it easier to figure out where a given stylesheet came from.

Stylesheet('my-styles', {
  body: {
    color: 'red'
  },
  button: {
    borderRadius: 5
  }
})

…inserts the following in the document.head:

<style id="my-styles">
body {
  color: red;
}
button {
  border-radius: 5px;
}
</style>

If a bare, non-zero number is assigned to a CSS property it will have 'px' suffixed to it automatically. There are no bare numericele properties in CSS except 0.

Why px? Well the other obvious options would be rem and em but px seems the least surprising option.

css should render nested rules, such as @keyframes and @media correctly.

Initializing CSS Variables

You can initialize CSS variables using _ or __ prefixes on property names. One bar, turns the camelCase property-name into a --snake-case CSS variable name, while two creates a default that can be overridden.

StyleSheet('my-theme', {
  ':root', {
    _fooBar: 'red',
    __bazBar: '10px'
  }
})

Will produce:

<style id="my-theme">
  :root {
    --foo-bar: red;
    --baz-bar: var(--baz-bar-default, 10px);
  }
</style>
import { elements, vars } from 'tosijs'
const { div } = elements

window.CSS.registerProperty({
  name: '--at-bar',
  syntax: '<color>',
  inherits: true,
  initialValue: 'green',
})

preview.append(
  div(
    {
      style: {
        _fooBar: 'red',
        __bazBar: 'blue',
      }
    },
    div(
      {
        style: { color: vars.fooBar },
      },
      'fooBar'
    ),
    div(
      {
        style: { color: vars.bazBar },
      },
      'bazBar'
    ),
    div(
      {
        style: { color: vars.atBar },
      },
      'atBar'
    ),
  )
)

@property and CSS.registerProperty() considered harmful

This [new CSS feature}(https://developer.mozilla.org/en-US/docs/Web/CSS/@property) is well-intentioned but ill-considered. I advise against using it yourself until its serious flaws are addressed. The problem is that if someone registers a variable you're using or you register a variable someone else is using then your CSS may be broken. And you can't re-register a variable either.

This is a bit like the problem that tosijs Component works around with tagNames, but in practice far more difficult to solve. It is impossible to tell if a given instance of a given variable name is an intentional reuse or a new separate variable. No-one intentionally defines two different components with the same tag.

invertLuminance({[key: string]: any}) => {[key: string]: string}

Given a map of CSS properties (in camelCase) emit a map of those properties that has color values with their luminance inverted.

const myStyleMap = {
  ':root': cssVars,                      // includes --font-size
  '@media (prefers-color-scheme: dark)': {
    ':root': invertLuminance(cssVars)    // omits --font-size
  },
}

vars

vars is a proxy object that will return a css variable string from a camelCase property, e.g.

vars.camelCase // 'var(--camel-case)'

it isn't called var because that's a reserved word!

varDefault

varDefault is a proxy object just like vars except that it returns a function that takes a property and renders it as a css variable reference with a default, e.g

varDefault.borderColor('red') // `var(--border-color, red)`

getCssVar(variable: string, atElement = document.body): string

getCssVar() obtains the css variable evaluated at the specified element (an element defined at :root can be evaluated at document.body). You can provide the name, e.g. --foo-bar, or "wrapped", e.g. var(--foo-bar).

Syntax Sugar for calc(...)

More importantly, vars allows you to conveniently perform calculations on css (dimensional) variables by a percentage:

vars.camelSize50    // 'calc(var(--camel-size) * 0.5)'
vars.camelSize_50   // 'calc(var(--camel-size) * -0.5)'

Computed Colors

Notes

color() and color-mix() are now enjoy 91% support as of writing. See color-mix() documentation. Where they meet your needs, I'd suggest using them.

contrast-color() is coming in Safari 26, but currently enjoys 0% upport.

Caution although these look superficially like the vars syntax sugar for calc() performed on dimensional variables, they are in fact color calculations are performed on colors evaluated on document.body at execution time. (So they won'b automatically be recomputed on theme change.)

You can write:

const styleSpec = {
  _lineHeight: 24,
  _spacing: 5,
  _buttonHeight: calc(`vars.lineHeight + vars.spacing200`)
)

And then render this as CSS and stick it into a StyleNode and it will work.

You cannot write:

const styleSpec = {
  _background: '#fafafa',
  _blockColor: vars.background_5b
}

Because --background isn't defined on document.body yet, so vars.background_5b won't be able to tell what --background is going to be yet. So either you need to do this in two stags (create a StyleNode that defines the base color --background then define the computed colors and add this) OR use a Color instance:

const background = Color.fromCss('#fafafa')

initVars({
  background: background.toHTML,
  blockColor: background.brighten(-0.05).toHTML
})

Until browsers support color calculations the way they support dimension arithmetic with calc() this is the miserable existence we all lead. That, or defining huge arrays of color values that we mostly don't use and are often not exactly what we want. You choose!

New color now supports CSS named colors, such as black, red, and aliceblue.

vars also allows you to perform color calculations on css (color) variables:

Change luminance with b (for brighten) suffix

The scale value is treated as a percentage and moves the brightness that far from its current value to 100% (if positive) or 0% (if negattive).

vars.textColor50b   // increases the luminance of textColor
vars.textColor_50b  // halves the luminance of textColor

Change saturation with s suffix

The scale value is treated as a percentage and moves the saturation that far from its current value to 100% (if positive) or 0% (if negattive).

vars.textColor50s   // increases the saturation of textColor
vars.textColor_50s  // halves the saturation of textColor

Rotate hue with h suffix

vars.textColor30h   // rotates the hue of textColor by 30°
vars.textColor_90h  // rotates the hue of textColor by -90°

Set Opacity with o suffix

Unlike the other modifiers, o simply sets the opacity of the resulting color to the value provided.

vars.textColor50o   // textColor with opacity set to 0.5

More to follow?

The more I use the css module, the more I like it and the more ideas I have to make it even better, but I have a very tight size/complexity target for tosijs so these new ideas really have to earn a spot. Perhaps the feature I have come closest to adding and then decided against was providing syntax-sugar for classs so that:

css({
  _foo: {
    color: 'red'
  }
})

Would render:

.foo {
  color: 'red'
}

But looking at the code I and others have written, the case for this is weak as most class declarations are not just bare classes. This doesn't help with declarations for input.foo or .foo::after or .foo > * and now there'd be things that look different which violates the "principle of least surprise". So, no.

Something to Declare

Where I am always looking to improve this module (and all of tosijs) is to do a better job of declaring things to improve autocomplete behavior and minimize casting and other Typescript antipatterns. E.g. adding a ton of declarations to elements and css has done wonders to reduce the need for stuff like const nameElement = this.parts.nameField as unknown as HTMLInputElement and prevent css property typos without adding a single byte to the size of the javascript payload.

onStylesheetChange(callback: () => void): () => void

Registers a callback that fires whenever any observant stylesheet regenerates (i.e., when a proxy-backed StyleSheet detects a change and rewrites its CSS). Returns an unsubscribe function.

const unsub = onStylesheetChange(() => {
  console.log('a stylesheet was updated')
})

// later
unsub()