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:

Sketch CSS has 3 distinct options:

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:

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!

Search Document