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, andvarDefaultis 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:
- Javascript quality tooling is really good, CSS quality tooling is terrible
- Having to write CSS in Javascript is inevitable so it might as well be consistent and painless
- It turns out you can get by with much less and generally simpler CSS this way
- You get some natural wins this way. E.g. writing two definitions of
body {}is easy to do and bad in CSS. In Javascript it's simply an error!
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
varbecause 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()andcolor-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
varssyntax sugar forcalc()performed on dimensional variables, they are in fact color calculations are performed on colors evaluated ondocument.bodyat 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, andaliceblue.
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()