Sketch
Sketch provides CSS support in Gleam in its simpler — yet complete — form. Sketch does not add complicated API on top of CSS. If you have CSS knowledge, you’ll feel right at home, with all the niceties offered by Sketch, i.e. type-checking of dimensions and push-to-browser stylesheets of your classes, as well as SSR support or CSS files generation.
Sketch supports both runtime of Gleam, and will let you write your CSS without over-thinking about it. Let Sketch handle the hard task for you of CSS caching, generation and pushing it in the browser. Sketch do the right choices for you, to maximise performance in the browser and on BEAM.
Write your styles once, use them anywhere you want.
Distributions
Sketch is thought as bare package, built as a foundation for every CSS packages
that want to leverage it. In the core package, you’ll find all CSS properties
accessible and a way to convert them directly in plain CSS.
Sketch package is also made for framework developers, to provide a common
basement, reusable across the entire Gleam ecosystem, letting users reuse their
knowledge no matter what they are coding.
Sketch supports officially three compilation target:
Lustre, with
sketch_lustre
,
Redraw with
sketch_redraw
and pure CSS generation à
la CSS Modules, with sketch_css
. Lustre has
also two versions, with one containing experimental modifications. As a user,
you want to grab one of those package, and start building! All targets can be
mixed too, to reach whoever you want! For framework authors, let’s see you at
integration part!
Installation
Sketch is published on Hex. Add it to your project by using the gleam CLI.
# For Lustre integration
gleam add sketch sketch_lustre
# For CSS generation
gleam add sketch sketch_css
Core concept
Sketch focuses on the concept of generating CSS in a performant way. To do it, Sketch uses a virtual stylesheet. That stylesheet allows to avoid repeating unneeded computations, and ensure consistency across repaints. Because the browser likes static CSS, using a virtual stylesheet make sure the browser will not undergo unneeded computations to recompute styles at every repaint.
Almost every CSS has to reside in a class to take effect. Sketch reuse the same
concepts, and ask you to write Sketch classes, and then to pass those classes
around to use them. A class is made of CSS declarations, which are accessible in
the module sketch/css
. Simply use functions in this module to generate the CSS
you want.
This may seem to add a bit of boilerplate, but Sketch is compatible with every runtime, and can also generate static CSS. As such, reusing the class abstraction allows a greater flexibility, without adding too much burden, as they’re still all generated at runtime. Sketch favour explicitness and CSS generation for every node instead of relying on cascading and inheritance.
Examples
Want to see examples to jump directly on subject? Take a look at e2e folder on GitHub to see how it works!
Sketch Lustre
Setup
If you’re using Lustre (which is strongly recommended), sketch_lustre
got you.
sketch_lustre
exposes one entrypoint, sketch/lustre
, containing everything
needed to get started.
// main.gleam
import lustre
import sketch
import sketch/lustre as sketch_lustre
pub fn main() {
// Initialise the cache. Two strategies can be used. Ephemeral caches are designed as throw-away caches.
let assert Ok(stylesheet) = sketch.stylesheet(strategy: sketch.Ephemeral)
// Generate the partial view function, compatible with Lustre's runtime.
lustre.simple(init, update, view(_, stylesheet))
// And voilà!
|> lustre.start("#app", Nil)
}
fn view(model, stylesheet) {
// Add the sketch CSS generation "view middleware".
use <- sketch_lustre.render(stylesheet, [sketch_lustre.node()])
// Run your actual view function.
my_view(model)
}
Usage
sketch_lustre
exposes two modules to help you build your site, similarly to
Lustre: sketch/lustre/element
and sketch/lustre/element/html
. The first one
let you use raw element generation and exposes the Sketch Lustre Element(msg)
type, that can be used (almost) interchangeably with Lustre, and element
helpers, i.e. element
, fragment
, or even keyed
.
Because a sketch_lustre
view function expects an
sketch/lustre/element.Element(msg)
to paint, you should now write all your
view functions to return Sketch elements. All Sketch elements can be
instanciated with element
, or with the corresponding
sketch/lustre/element/html.element
. An element accepts the same thing as a
Lustre element, but includes a sketch.Class
value as first argument. That
class will be applied to the final generated element.
NB: all elements can be generated using the correct function, or using its “underscored” version. In the second case, Sketch Lustre behaves exactly like Lustre, and will not add another class. This is helpful when you want to use a simple node, without any class linked on it.
import sketch/css
import sketch/css/length.{px}
import sketch/lustre/element
import sketch/lustre/element/html
fn main_style() {
css.class([
css.background("red"),
css.font_size(px(16)),
])
}
fn view(model: Int) {
html.div(main_style(), [], [
html.div_([], [
html.text(int.to_string(model)),
]),
])
}
And you’re done! Enjoy your Lustre app, Sketch-enhanced!
Final notes
On Sketch Lustre Element
A Sketch Element(msg)
is extremely similar to a Lustre Element(msg)
,
excepted it carries styles information on top. Going from a
sketch/lustre/element.Element(msg)
to a lustre/element.Element(msg)
is
straightforward, by using sketch/lustre/element.unstyled
. The opposite (going
from a Lustre element to a Sketch Lustre element) is also possible by using
sketch/lustre/element.styled
!
Sketch Lustre Experimental
Because sometimes you may want to avoid the Element(msg)
overhead, you can try
the experimental Sketch Lustre runtime, sketch_lustre_experimental
. That
runtime works in the same way, excepts it does not implements its own Element
type on top of Lustre’s Element
. Most of the time, you should not see any
differences. Keep in mind that it can bug though, as it’s still experimental. If
you try to use it, please, report any bugs you can find.
Usage with Shadow DOM
In browser, Sketch can work with a Shadow DOM, in order to hide the compiled
styles from the rest of the application. With a proper shadow root (represented
as a Dynamic
in Gleam), you can use
sketch/lustre.shadow()
to render a stylesheet in the shadow root directly. In the same way you can
initialize the cache to render in document or in a style
node, you can use a
shadow root to paint styles in your application!
Sketch Redraw
Setup
When you’re using Redraw, sketch_redraw
covers you. sketch_redraw
exposes
one entrypoint, sketch/redraw
, containing everything needed to get started.
// main.gleam
import redraw
import sketch/redraw as sketch_redraw
pub fn main() {
let root = client.create_root("root")
client.render(root,
redraw.strict_mode([
// Initialise the cache. Sketch Redraw handles the details for you.
sketch_redraw.provider([
// Here comes your components!
])
])
)
}
Usage
sketch_redraw
exposes one module to help you build your site, similarly to
redraw: sketch/redraw/dom/html
. html
is simply a supercharged component,
accepting a sketch.Class
as first argument, and applies that style to the
node. Because it’s a simple component, sketch/redraw/dom/html
and
redraw/html
can be mixed in the same code without issue! Because of that
property, sketch_redraw
does not expose text
and none
function at that
time.
import redraw/html as h
import sketch/css
import sketch/css/length.{px}
import sketch/redraw/html
fn main_style() {
css.class([
css.background("red"),
css.font_size(px(16)),
])
}
fn view(model: Int) {
html.div(main_style(), [], [
h.div([], [
h.text(int.to_string(model))
]),
])
}
And you’re done! Enjoy your Redraw app, Sketch-enhanced!
Final notes
Sketch Redraw tries to integrate nicely with React Devtools! In case you’re seeing something weird, signal the bug!
Sketch CSS
Because pure CSS generation is straightforward, sketch_css
does not need a
cache to generate correct CSS files. Instead, sketch_css
ships with a CLI
tool, able to read your Gleam styles files, and output corresponding CSS
automagically, while providing an abstraction layer written in Gleam, to make
sure you’re using the right classes! It’s an other way to leverage Sketch core
and enjoy the styling in Gleam, while taking advantage of all the static CSS
power!
To run the generator, you have to use the command
gleam run -m sketch/css generate
at the root of your project. By default,
sketch_css
will try to read all files named *_styles.gleam
, *_css.gleam
and *_sketch.gleam
in your src
folder, no matter where they are. You can put
them at root, nested, or in a folder called css
, sketch_css
does not care!
After fetching the styles files, sketch_css
will output your generated CSS
files in a styles
folder, at the root of the project. They can then be served
in the way you want. In the same time, sketch_css
will output Gleam interfaces
in src/sketch/styles
, matching your styles files, to use in your project!
Options
Sketch CSS generation has strong defaults, but everything can be customised. To pass options to Sketch CSS, you have three ways:
- Pass them directly on the CLI. Every option has its equivalent exposed in the CLI.
- Write them in a
sketch_css.toml
file, at root of your project, nextgleam.toml
. - Write them directly in
gleam.toml
, under[sketch_css]
section.
Sketch CSS has 3 distinct options:
--dest
, accepting a folder, relative to current directory. It defaults tostyles
.--src
, accepting a folder, relative to current directory. It defaults tosrc
.--interface
, accepting a folder, relative to current directory. It defaults tosrc/sketch/styles
.
Write directly the folder, path resolution is done with current working directory as root.
Examples
gleam run -m sketch_css generate --src="src" --dest="styles" --interface="src/sketch/styles"
# sketch_css.toml
src = "src"
dest = "styles"
interface = "src/sketch/styles"
# gleam.toml
name = "name"
version = "1.0.0"
[sketch_css]
src = "src"
dst = "styles"
interface = "src/sketch/styles"
[dependencies]
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
sketch = ">= 4.0.0 and < 5.0.0"
sketch_css = ">= 2.0.0 and < 3.0.0"
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
A note on generation algorithm
Because a Sketch Class
can be generated in multiple ways, and with variable,
Sketch CSS takes that into account. Every simple Sketch Class
will be iso
generated in CSS, but every Sketch Class
that contains variable will be
generated with the variable taken into account! Sketch CSS being opinionated, it
generates the class, with a CSS variable, letting you update it, override it,
etc.
Sketch CSS also acts as a basic interpreter. It means you can write basic constants or variables, and they will be taking into account. Be sure to write classes like you would do in CSS yet: Sketch CSS does not execute your functions!
Example
// src/main_styles.gleam
import sketch/css
pub fn flexer() {
let display = "flex"
css.class([css.display(display)])
}
fn direction(flex_direction: String) {
css.flex_direction(flex_direction)
}
pub fn flexer_direction(flex_direction: String) {
css.class([
css.compose(flexer()),
direction(flex_direction),
])
}
/* styles/main_styles.css */
.main_styles-flexer {
display: flex;
}
.main_styles-flexer_direction {
display: flex;
flex-direction: var(--flex-direction);
}
// src/sketch/styles/main_styles.gleam
pub const flexer = "main_styles-flexer"
pub const flexer_direction = "main_styles-flexer_direction"
Sketch general usage
At its core, Sketch relies on sketch.class
, which let you define a class. A
class is made of CSS properties. All of those can be accessed in sketch
module. Build your classes, and use them across your codebase! But a Sketch
class contains more than CSS properties, it can also contains every piece of
information used to defined a CSS class. This includes media queries,
pseudo-selectors & combinators! This allows to think to your styles in
isolation, without worrying with the global scope.
Using media queries and pseudo-selectors
Because we’re building CSS, we can leverage its full power, contrarily to inline styling. This mean we can use media queries and pseudo-selectors! You only need to call the proper functions, and Sketch will take care of the rest.
import sketch/css
import sketch/css/length.{px}
import sketch/css/media
fn my_class() {
css.class([
css.display("flex"),
css.flex_direction("row"),
css.background("red"),
css.hover([
css.background("blue"),
]),
css.media(media.max_width(px(320)), [
css.flex_direction("column"),
css.hover([
css.background("green"),
]),
]),
])
}
The example above will be compiled to the following CSS.
.my-class {
display: flex;
flex-direction: row;
background: red;
}
.my-class:hover {
background: blue;
}
@media (max-width: 320px) {
.my-class {
flex-direction: column;
}
.my-class:hover {
background: green;
}
}
Combinators
Sometimes, you need to modify some CSS of a child, when a parent state changes.
In such cases, you need to use a combinator. Combinators are common in CSS, and
they can be used easily in Sketch. Sketch exposes the 4 main combinators:
child
, descendant
, next_sibling
and subsequent_sibling
, i.e. >
,
,
+
and ~
. They have to be used with target classes, and they will be combined
in the resulting CSS!
import sketch/css
fn my_class() {
css.class([
css.background("red"),
css.hover([
css.background("blue"),
css.child(button_class(), [
css.background("yellow"),
]),
]),
css.child(button_class(), [
css.background("green"),
]),
])
}
fn button_class() {
css.class([
css.appearance("none"),
css.background("none"),
css.border("1px solid black"),
css.font_family("inherit"),
css.font_size_("inherit"),
])
}
Will give the following CSS.
.my_class {
background: red;
}
.my_class:hover {
background: blue;
}
.my_class:hover > .button_class {
background: yellow;
}
.my_class > .button_class {
background: green;
}
.button_class {
appearance: none;
background: none;
border: 1px solid black;
font-family: inherit;
font-size: inherit;
}
Composition
Because we oftentimes need to compose CSS classes, Sketch provides a compose
function. This allow you to reuse CSS properties from another class, without
having the burden of copy-pasting the styles, or having to think on the class
names to put in your nodes! Of course, this remains totally optional. An
example:
fn button_style() {
css.class([
css.appearance("none"),
css.border("none"),
css.border_radius(px(10)),
css.transition("all .2s"),
])
}
fn enabled_button_style() {
css.class([
css.compose(button_style()),
css.background("red"),
css.color("white"),
])
}
fn disabled_button_style() {
css.class([
css.compose(button_style()),
css.background("grey"),
css.color("black"),
])
}
fn button(disabled) {
let class = case disabled {
True -> disabled_button_style()
False -> enabled_button_style()
}
html.button(class, [], [html.text("Yay!")])
}
Top-level CSS
Sometimes, you need to write CSS directly in stylesheets, at the top-level. Sketch implements a cherry-picked subset of @rules. You can use them directly on stylesheet, and they will be bundled in your resulting stylesheet!
import sketch
import sketch/css
pub fn main() {
let assert Ok(stylesheet) = sketch.stylesheet(strategy: sketch.Ephemeral)
let stylesheet = sketch.at_rule(my_keyframe(), stylesheet)
let content = stylesheet.render(stylesheet)
}
fn my_keyframe() {
css.keyframes("fade-out", [
keyframe.from([css.opacity(1.0)]),
keyframe.at(50, [css.opacity(0.5)]),
keyframe.to([css.opacity(0.0)]),
])
}
In the above code, content
will contains the following CSS.
@keyframes fade-out {
from {
opacity: 1;
}
50% {
opacity: 0.5;
}
to {
opacity: 0;
}
}
Similarly, you can use @font-face
to define your own fonts!
Some opinions on properties
All standard widely supported properties are accessible directly through the
sketch/css
package. But with time, some could be added, and new features for
existing properties can appear. Prefixed properties, like -moz
or -webkit
,
can also be necessary, when targeting some browsers. That’s why Sketch will
never try to be on your way: at any time you can access
css.property()
, which
allows you to push any arbitrary property in a class. Another thing is that
Sketch will always let you access raw, low-level properties. If you’re trying to
use something like sketch.width("auto")
and the property does not support
String, look for a variant with an underscore (_
), it should fullfill your
needs, like sketch.width_("auto")
! In case something is missing or a property
does not have its underscore alternative,
open an issue — or better, a PR — on the repo!
In the same idea, selectors are plainly supported too! Even if most of them are
already implemented, like hover
, you could want to define some specific
selectors. In that case, look for
css.selector()
!
Usagi with Chrome Extensions
At its core, Sketch uses Wasm to compute classes hash, to make sure there’s no
performance bottleneck. Unfortunately, Google is conservative on Wasm in Chrome
Extension. To get Sketch running in Chrome Extension, you should put the
following code in your manifest.json
. This allows Chrome to load Wasm code in
your extension!
"content_security_policy": {
"extension_pages":"script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
}
Integration
This part is new, and subject to modification. Because nobody integrated Sketch in their framework yet, it’s hard to write a correct guide, that is useful and not redundant. If you’re in the case of writing a framework binding, please, let’s keep in touch directly, and I’ll help you integrate Sketch. That would be immensely helpful, to write a correct guide after this! Meanwhile, you can find necessary pointers below to help you get started by yourself!
If you’re here, it means you’re interested in integrating Sketch in your framework! What a wonderful idea!
To integrate Sketch in your framework, you have 2 choices:
- run Sketch in your repaint function.
- compiles all Sketch files as static code.
To run Sketch in your repaint function, your only need is to run
css.class_name
on a css.Class
. Let your users write css.Class
, and then,
do the hard work of wiring everything up by calling css.class_name
. This
requires a sketch.StyleSheet
to run correctly. Take a look at what is
happening in sketch_lustre
to figure out how everything works.
A nice way is also to precompile everything, like sketch_css
is doing. Instead
of generating the CSS on-the-fly, which browsers does not really like, you can
precompute everything. By using a Gleam parser, like glance
, you could compile
everything to plain CSS. This area is subject of exploration, and is the way
sketch_lustre
tries to follow in some specific environments, like Vite and
Lustre Dev tools. If you’re interested in the subject, let’s keep in touch!