Fstyle is a JavaScript library that lets you parameterize fragments of arbitrary CSS, compose them together, and use them to style web applications. It is designed to be used in sizeable, highly dynamic web applications where good modularity is crucial.
Styles defined using Fstyle are not tied to any particular framework, so can be very portable. The rationale for Fstyle's approach is given in the blog post Styling Web Applications.
Fstyle is in the Public Domain.
The following examples demonstrate how Fstyle might be used to style a trivial web application.
import fstyle from "./fstyle.js";
// Two primitive stylers are made by passing template functions to
// 'fstyle.rule'.
const button_styler = fstyle.rule(function button() {
return `
border: 0.1em solid black;
color: gold;
background: fuchsia;
padding: 1em;
`;
});
const centered_styler = fstyle.rule(function centered() {
return `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`;
});
// These stylers are mixed together to form a composite styler.
function centered_button_styler() {
return [button_styler(), centered_styler()];
}
// A context is created. It is responsible for injecting CSS fragments into the
// page without duplication.
const context = fstyle.context();
// The composite styler is "required", producing a handle object.
const handle = context.require(centered_button_styler());
// The handle's classes are assigned to an element.
const button = document.createElement("button");
button.textContent = "Fabulous";
button.classList.add(...handle.classes);
document.body.append(button);
The page now looks something like this:
<head>
<style>
.f0⎧button⎭ {
border: 0.1em solid black;
color: black;
background: fuchsia;
padding: 1em;
}
</style>
<style>
.f1⎧centered⎭ {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<button class="f0⎧button⎭ f1⎧centered⎭">
Fabulous
</button>
</body>
Note that f0⎧button⎭
is a valid class. Fstyle places Unicode characters in
classes to make them easier to read. (This becomes especially important when
stylers are parameterized.)
Notice how Fstyle incorporates the names of the template functions, "button" and "centered", into the classes. Even so, the context ensures that classes produced by different stylers never collide. For example, suppose we create another styler whose template function is also named "button":
const other_button_styler = fstyle.rule(function button() {
return "color: limegreen";
});
Rest assured that other_button_styler
produces a distinct class, f2⎧button⎭
.
That is because, within context
, there is an exact correspondence of f0
to
button_styler
, and f2
to other_button_styler
. The name is only included
in the class to aid debugging.
For Fstyle to generate CSS without duplication, stylers should not be
recreated needlessly. For best results, do not call fstyle.rule
or
fstyle.css
within functions or loops.
The following styler takes two parameters, enabled
and selected
. The
template function, tabber
, transforms the parameters into CSS values that are
then used by the template.
const tabber_styler = fstyle.rule(function tabber({disabled, selected}) {
const background = "black";
const color = (
selected
? "yellow"
: "white"
);
const opacity = (
disabled
? 0.4
: 1
);
return `
color: ${color};
border: 2px solid ${color};
border-radius: 6px;
padding: 0.5em;
background: ${background};
opacity: ${opacity};
`;
});
const handle = context.require(
tabber_styler({disabled: true, selected: true})
);
const tabber = document.createElement("div");
tabber.innerText = "Tab me";
tabber.classList.add(...handle.classes);
document.body.append(tabber);
The page might look something like this:
<head>
<style>
.f3⎧tabber⎭·selected→true·disabled→true {
color: yellow;
border: 2px solid yellow;
border-radius: 6px;
padding: 0.5em;
background: black;
opacity: 0.4;
}
</style>
</head>
<body>
<div class="f3⎧tabber⎭·selected→true·disabled→true">
Tab me
</div>
</body>
Yes, that is a still a valid class.
A styler is any function that takes an optional parameters object and returns a requireable.
A requireable is passed to context.require
to inject zero or more
fragments of CSS onto the page. A requireable is either:
- an opaque value returned by a styler made by
fstyle.rule
orfstyle.css
, or - an array of requireables.
A styler can be wrapped in a function if its parameters need to be transformed or predefined.
function disabled_tabber_styler() {
return tabber_styler({disabled: true});
}
Stylers can be mixed together by defining a function that returns an array of
requireables. Notice how spinning_link_styler
distributes its parameters to
the stylers within, and how the empty array []
represents the absence of
style.
function spinning_link_styler({animating, color}) {
return [
(
animating
? spinner_styler({color, duration: "500ms"})
: []
),
link_styler({color})
];
}
Stylers containing conflicting declarations should not be mixed. Attempting to do so results in unpredictable behaviour.
const red_styler = fstyle.rule(function red() {
return "color: red";
});
const green_styler = fstyle.rule(function green() {
return "color: green";
});
function bad_styler() {
return [red_styler(), green_styler()];
}
Instead of attempting to mix the unmixable, make a styler that takes a color
parameter.
const good_styler = fstyle.rule(function good({color}) {
return `color: ${color}`;
});
An object containing four functions is exported by fstyle.js:
import fstyle from "./fstyle.js";
const {rule, css, context, domsert} = fstyle;
The fstyle.rule
and fstyle.css
functions make stylers, and are the most
commonly used.
The fstyle.context
function is generally called only once per application.
The fstyle.domsert
function offers additional control when configuring a
context.
The rule function makes a styler representing a single CSS ruleset. The template parameter is a function that takes a parameters object and returns a string containing any number of CSS declarations.
const link_styler = fstyle.rule(function link({color}) {
return `
font-weight: bold;
color: ${color};
`;
});
The ruleset's class incorporates the names and values of the parameters. For
example, link_styler
might produce the following CSS if color
was "red"
:
.f4⎧link⎭·color→red {
font-weight: bold;
color: red;
}
Characters not permitted within a class are safely escaped.
The css function makes a styler representing an arbitrary fragment of CSS.
The template parameter is a function that takes a parameters object and
returns an arbitrary CSS string. Any instances of the placeholder []
in the
CSS string are replaced with the generated class.
In the following example, the generated class is used both to identify a set of animation keyframes and a ruleset.
const spinner_styler = fstyle.css(function spinner({color, duration}) {
return `
@keyframes [] {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.[] {
display: block;
width: 20px;
height: 20px;
border-radius: 10px;
border-top: 4px solid ${color};
animation: [] ${duration} linear infinite;
}
`;
});
Calling
context.require(
spinner_styler({color: "#a020f0", duration: "500ms"})
)
might produce the following CSS:
@keyframes f5⎧spinner⎭·color→#a020f0·duration→500ms {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.f5⎧spinner⎭·color→#a020f0·duration→500ms {
display: block;
width: 20px;
height: 20px;
border-radius: 10px;
border-top: 4px solid #a020f0;
animation: f5⎧spinner⎭·color→#a020f0·duration→500ms 500ms linear infinite;
}
The context function makes a context object, which manages the insertion and removal of CSS fragments without duplication. Generally, there should only be a single context per application.
const context = fstyle.context();
The spec parameter, if provided, configures the context. It is an object containing any of the following properties:
A function that takes a fragment and inserts its CSS into the page. It may return a remove function, called to remove the fragment from the page.
The fragment is an object like {class, statements}
, where class
is a CSS
class string and statements
is a string containing CSS statements.
If spec.insert
is undefined, fstyle.domsert
is used.
A boolean indicating whether classes should be radically shortened. Defaults to false.
Interning makes classes much shorter, but consumes additional memory for the lifetime of the context. Use interning only in production scenarios where HTML and CSS is being generated on the server and long classes would just waste network bandwidth.
The context object returned by fstyle.context
has two methods:
Guarantees that a styler's CSS remains available on the page until explicitly released.
const handle = context.require(link_styler());
The returned handle is an object with the following properties:
classes
: An array of class strings. Apply these immediately to any elements that want styling.release
: The releaser function.
If there comes a time when the handle is no longer needed, it can be released to conserve resources.
handle.release();
The dispose function releases all of the context's handles and renders the context inoperable.
context.dispose();
The domsert function may be used in conjunction with fstyle.context
when
the DOM is available. Each time it is called, a new style element is populated
with the fragment's CSS and inserted into the parent.
The parent is a DOM Element
or DocumentFragment
, for example a
ShadowRoot
. It defaults to the head of the current document if omitted.
The returned remove function removes the style element from the parent.