{} / docs playground →

GSS — GPU Style Sheets

A CSS-like language for 3D scenes, compiled to a GPU shader.

Getting started

Why GSS

Front-end developers already describe how things look with selectors, properties, the cascade and @keyframes. To go 3D, they meet GLSL, the language of the GPU: powerful, but written in distance functions, vectors and math, far from the way they think about design.

  1. scene.gss
  2. one shader
  3. the GPU draws it

GSS bridges that gap. If you can write CSS, you can already read GSS:

Selectors
sphere, .glass, #ball, :hover, :has(), the cascade and !important
Properties
color, translate, opacity, filter, in deg, s and %
Motion
@keyframes, animation, transition, cubic-bezier()
Values
calc(), var(), @property, oklch(), color-mix()
Media queries
@media, prefers-color-scheme, light-dark()

GSS does not replace GLSL. It is a way in: a familiar syntax to start creating in 3D today, and a readable path toward the shader underneath, for those who want to go further.

Your first scene

A glass ball that floats above the floor. Press Try it to edit it live.

@scene {
  sphere#ball;
}

#ball {
  translate: 0 1 0;
  color: #ff5a36;
  material: glass(1.5, frosted 0.3);
  animation: float 2s ease-in-out alternate;
}

@keyframes float {
  from {
    translate: 0 1 0;
  }
  to {
    translate: 0 1.5 0;
  }
}
  1. sphere#ball;
  2. translate: 0 1 0;
  3. color: #ff5a36;
  4. material: glass(1.5, frosted 0.3);
  5. animation: float 2s ease-in-out alternate;

@scene lists the shapes. A rule styles them, like CSS: #ball lifts the sphere, colors it and turns it into glass. @keyframes makes it float.

Next, put the scene on a page with Embedding a scene, or read the reference: each page describes one at-rule, selector, property or function, with examples to try.

Installation

Embedding a scene

A GSS scene runs on any web page, in three ways. All three use the package gss-lang@0.0.7.

<gss-scene src="scene.gss"></gss-scene>

GSS is open source, under the Apache-2.0 license: its code is on GitHub, where you can report a bug. Questions, ideas, a scene to show: say hi on X, @GSS_lang.

A tag
No build step: load embed.js once, then write <gss-scene>.
A function
mount(canvas, source) from gss-lang compiles the scene in the page.
A build step
The Vite plugin compiles .gss files at build time; mount() from gss-lang/runtime draws them without the compiler.

A tag

CDN · v0.0.7 · no build step

<script type="module" src="https://cdn.jsdelivr.net/npm/gss-lang@0.0.7/lib/embed.js"></script>

<gss-scene src="logo.gss"></gss-scene>
<gss-scene controls="none">
  <script type="text/gss"> @scene { sphere; } </script>
</gss-scene>

A function

npm install gss-lang@0.0.7

import { mount } from "gss-lang";

const scene = mount(canvas, "@scene { sphere; }");
scene.update(otherSource);
scene.destroy();

A build step

gss-lang@0.0.7 · compiled with Vite

// vite.config.ts
import gss from "gss-lang/vite";
export default { plugins: [gss()] };

// main.ts: the compiler stays out of the page
import { mount } from "gss-lang/runtime";
import logo from "./logo.gss";
mount(canvas, logo);

In the page

Camera
A drag turns it, the wheel zooms, :hover works. controls="none" (or controls: false) keeps only :hover, and the page scrolls over the scene. The GSS can choose too: see controls.
Off screen
The scene starts when it comes into view, and sleeps when it leaves it.
Reduced motion
The scene stands still under prefers-reduced-motion.
A still scene
It draws nothing between two changes (the mouse, a variable, an image that arrives): the GPU rests.
Images
A path starts from the .gss file, like url() in a stylesheet.
Background
The canvas is not transparent: give the scene the background of your page.

See embedded scenes live in the showcase.

From a script

Once <gss-scene> fires load, its scene property gives the scene, to pause() it, play() it or set its variables.

Package (npm)

Install gss-lang@0.0.7 in your application. The package holds the compiler, the renderer and the TypeScript declarations.

  1. npm install gss-lang
  2. mount(canvas, source)
  3. a canvas of the page
npm install gss-lang@0.0.7
import { mount } from "gss-lang";

const scene = mount(canvas, "@scene { sphere; }");
scene.update(otherSource);
scene.destroy();

mount() takes a canvas that is already in the page, and returns the scene: scene.update(source) replaces it, scene.destroy() removes it.

A variable the scene registers with @property can be set from the page without compiling again, like a CSS custom property: scene.setProperty("--lift", "2"). See Set variables from JavaScript.

Vite plugin

The Vite plugin comes with the package. It compiles the .gss imports at build time, so the browser loads only the renderer and the compiled scene.

  1. scene.gss
  2. vite build
  3. the scene, compiled
npm install gss-lang@0.0.7
npm install --save-dev vite
// vite.config.ts
import gss from "gss-lang/vite";
export default { plugins: [gss()] };

// main.ts: the compiler stays out of the page
import { mount } from "gss-lang/runtime";
import logo from "./logo.gss";
mount(canvas, logo);

With TypeScript, add "gss-lang/client" to compilerOptions.types in tsconfig.json: a .gss import then has a type. Draw the compiled scene with mount() from gss-lang/runtime.

CDN (no build step)

Load the browser module of v0.0.7 from jsDelivr: no install, no bundler. The version is pinned, so an update never changes your scene unexpectedly.

  1. <script src="embed.js">
  2. <gss-scene>
  3. the scene
<script type="module" src="https://cdn.jsdelivr.net/npm/gss-lang@0.0.7/lib/embed.js"></script>

<gss-scene src="logo.gss"></gss-scene>
<gss-scene controls="none">
  <script type="text/gss"> @scene { sphere; } </script>
</gss-scene>

The script defines <gss-scene>. Point its src to a .gss file, or write the GSS inline, as above. Serve the page over HTTP(S). The module: https://cdn.jsdelivr.net/npm/gss-lang@0.0.7/lib/embed.js.

To drive the scene from a script, see Set variables from JavaScript.

Slow computers

poster
poster="cover.jpg" shows an image until the scene draws, like the poster of a <video>.
No graphics card
<gss-scene> shows its poster and a button, and draws the scene only when the reader asks. With mount(), softwareRendering(), from gss-lang or gss-lang/runtime, returns true on such a computer, to do the same.
A weak graphics card
The scene starts at a density of 0.5, then climbs to its dpr while the frames keep up.
Still too slow
The scene stops: <gss-scene> shows a button that draws it anyway, and mount() fires a gss-too-heavy event on the canvas, where scene.play() draws it anyway.
adaptDpr: false
mount(canvas, scene, { adaptDpr: false }) keeps the dpr as written and never stops, for a capture or a benchmark.

Set variables from JavaScript

A variable the scene registers with @property can be set by the page at any moment, without compiling the scene again, the way element.style.setProperty() sets a CSS custom property. Drive a scene from a slider, the scroll position or your data. Move the slider:

scene.setProperty("--lift", "1.5");

The scene registers --lift and reads it in translate and in its color; the page sets it when the slider moves:

<gss-scene id="ball" controls="none">
  <script type="text/gss">
    @property --lift { syntax: "<number>"; inherits: false; initial-value: 1; }
    @scene { sphere; }
    sphere {
      translate: 0 var(--lift) 0;
      radius: 0.5;
      color: oklch(70% 0.17 calc(var(--lift) * 80));
    }
  </script>
</gss-scene>
<input id="lift" type="range" min="0.5" max="1.6" step="0.01" value="1">
const ball = document.querySelector("#ball");
const lift = document.querySelector("#lift");

// ball.scene is null until the element fires "load"
const apply = () => ball.scene?.setProperty("--lift", lift.value);
ball.addEventListener("load", apply);
lift.addEventListener("input", apply);

Three methods

Named like those of element.style:

setProperty()
Sets the variable: scene.setProperty("--lift", "1.5"). The value is a string written like CSS (below); a "<number>" also takes a number.
getPropertyValue()
Reads it back, written like a computed CSS value: "90deg", "#ff5a36". "" for a variable the scene does not register.
removeProperty()
Returns to the start value: the one scene { --lift: 2; } gives, or else initial-value.

Unlike CSS, which ignores a value it cannot read, setProperty() throws an error for a value that does not match the syntax of the variable, and for a variable the scene does not register. Only a registered variable changes while the scene runs: an unregistered one (--size: 2) is computed once, when the scene compiles.

Values

The value is written by the syntax of the variable:

"<number>"
"2", "-0.5", or the number 2
"<angle>"
"90deg", "1.57rad", "100grad", "0.25turn"
"<percentage>"
"50%"
"<color>"
any CSS color: "#ff5a36", "tomato", "hsl(10 100% 60%)", "oklch(70% 0.15 40)", "color-mix(in oklab, red, blue)"
"<length>"
"4px", in px only: the radius of blur() and bloom()

The new value is drawn at the next frame, wherever the variable goes: transforms, colors, sizes, materials, lights, fog, filters, alone or inside calc() and the color functions. A value out of its range is kept in it: a size never goes below 0. @property lists what a variable can change, and what it cannot: what the scene builds when it compiles, like the copies of * n.

From mount()

mount() returns the scene at once, mountAsync() a promise of it. mount() from gss-lang/runtime, for a scene compiled by the Vite plugin, has the same methods.

import { mount, mountAsync } from "gss-lang";

const scene = mount(canvas, source);
scene.setProperty("--lift", "1.5");
scene.getPropertyValue("--lift"); // "1.5"
scene.removeProperty("--lift"); // back to its start value

// mountAsync() gives the same scene, once its backend is ready
const other = await mountAsync(canvas2, source, { backend: "webgpu" });
other.setProperty("--lift", 2);

A value set this way stays when scene.update(source) gives another scene, as long as the new scene registers the variable with the same syntax.

In the playground

With @property-panel { display: open; } in the scene, the playground, and Try it under the examples of these docs, show a panel over the render with a control for each registered variable: a slider and its number, or a color picker for a "<color>". It calls setProperty() as it moves, and its reset button calls removeProperty(). A page that embeds the scene shows no panel. See @property-panel.

From <gss-scene>

The scene property of the element is the scene, with the same methods. It is null until the element fires "load": set your values in a "load" listener, as the demo does.

The element fires "load" again each time it starts a new scene: a new src, controls or backend, or the element moved in the page. A new scene starts from its start values, so the listener sets yours again.

Editor support

The GSS extension colors and formats .gss files in VS Code and in the editors built on it, like Cursor, VSCodium and Windsurf. It is not needed to run a scene.

scene.gss

For VS Code and CursorHighlighting, formatting and the .gss file icon.VS Code Marketplace · Open VSX for Cursor, VSCodium and Windsurf

Highlighting
Selectors, properties, values, colors, numbers and units, the at-rules, nested rules and &.
Formatting
Format Document, or saving the file, writes the code in the style of these docs: one shape per line in @scene, one declaration per line, two-space indentation, comments kept.
File icon
.gss files get the GSS icon in the explorer.

Install

In VS Code, open the Extensions view (Ctrl+Shift+X, or Cmd+Shift+X on a Mac), search for GSS and install GSS — GPU Style Sheets. Or install it from the Visual Studio Marketplace, or from the command line:

code --install-extension lukyvj.gss-language

Cursor, VSCodium and Windsurf install their extensions from Open VSX: search for GSS in their Extensions view.

Format on save

Formatting on save is on by default for .gss files. To turn it off, add this to your settings.json:

"[gss]": { "editor.formatOnSave": false }

At-rules

@keyframes

Defines the steps of an animation, played by the animation property, like CSS.

animation: float 2s ease-in-out alternate;
Syntax
@keyframes <name> { <offset>[, <offset>]* { <declaration>* } … }

Offsets

from
The start: 0%.
to
The end: 100%.
<percentage>
A step in between. Several offsets can share a frame: 0%, 100% { … }.

A missing 0% or 100% uses the object's own value, and a frame changes only the properties it declares. A frame takes animatable properties only.

up and down

Two frames, played forward then backward by alternate.

@scene {
  sphere;
}

sphere {
  animation: float 2s ease-in-out alternate;
}

@keyframes float {
  from {
    translate: 0 1 0;
  }
  to {
    translate: 0 2 0;
  }
}

a pulse

0% and 100% share a frame; at 50%, the cube grows and changes color.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
  animation: pulse 1s;
}

@keyframes pulse {
  0%, 100% {
    scale: 1;
  }
  50% {
    scale: 1.3;
    color: #ff5a36;
  }
}

@media

Applies rules only when the screen matches a media query, like CSS. When the screen changes, a window resized or the system switching to dark mode, the scene follows at once and the camera stays where it is.

  1. 900px wide
  2. 480px wide
@media (max-width: 600px) { sphere { color: #ff5a36; } }
Syntax
@media <media-query> { <rule> … }

Queries

(max-width: 600px)
The width of the viewport; also min-width, in px or em.
(orientation: portrait)
Taller than wide; landscape otherwise.
(prefers-color-scheme: dark)
The dark mode of the system; light for the light mode.
(prefers-reduced-motion)
The visitor asks for less motion.
and, not, ,
Combine queries, like CSS.

Any media query CSS knows works: the browser reads it. @media holds rules only, so @scene and @keyframes go outside it.

More details

Inside, the rules join the cascade where the @media is written, with their usual specificity. A scene can use up to 4 different queries. On a page, the viewport is the window, like CSS. In the playground and in Try it, it is the render, like the result of CodePen: make the render narrower to see a max-width query apply.

a small screen

Under 600px wide, the render is lighter, with dpr: 1, and the sphere turns blue.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  color: #ff5a36;
}

@media (max-width: 600px) {
  scene {
    dpr: 1;
  }
  sphere {
    color: #3a7bff;
  }
}

less motion

* { animation: none !important; } stops every animation for the visitors who ask for less motion.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  color: #ff5a36;
  animation: bob 2s ease-in-out alternate;
}

@keyframes bob {
  to {
    translate: 0 1.6 0;
  }
}

@media (prefers-reduced-motion) {
  * {
    animation: none !important;
  }
}

dark mode

The background, the floor and the sphere follow the light or dark mode of the system.

@scene {
  sphere;
}

scene {
  background: #f2efe9;
  floor: #e8e3db;
}

sphere {
  translate: 0 1 0;
  color: #ff5a36;
}

@media (prefers-color-scheme: dark) {
  scene {
    background: #080808;
    floor: #1a1a1f;
  }
  sphere {
    color: #3a7bff;
  }
}

@paint

A texture drawn by a fragment shader, for texture: paint(<name>): an image that comes from code, like paint() in CSS. The block is GLSL, the shader language of WebGL, written as it is for the web. WebGL2 only, for now: with WebGPU, the object keeps its color.

  1. @paint rings { … }
  2. 512 × 512 px
  3. texture: paint(rings);
Syntax
@paint <name> { <fragment shader> }
See also
paint(), texture

Uniforms

time
The time of the scene, in seconds, a float. u_time works too. A shader that reads it is drawn again at every frame; one that does not is drawn once.
resolution
The size of the texture in pixels, vec2(512.0, 512.0). u_resolution works too.
mouse
The pointer over the scene, in the pixels of the texture, from its bottom left like gl_FragCoord. u_mouse works too.

The block is a whole fragment shader, with its own main(): write out vec4 color; and set it. It is drawn into a texture of 512 by 512 pixels, which the object shows like an image.

More details

The shader holds its uniforms, its functions and its main(). gl_FragColor works too, as in WebGL1, and GSS adds #version 300 es and a precision when the code has none. The texture goes on each face of the object, with texture-size and image-rendering. Each @paint runs on its own: its names never meet the rest of the scene, and an error in its GLSL names the @paint and its line in the file. A scene with paint() is drawn with WebGL2: <gss-scene>, mountAsync() and the playground pick it by themselves in auto; with WebGPU chosen, the object shows its color instead, and the console says why.

rings that flow

The shader reads time: it is drawn again at every frame, and the colors flow over the cube.

@paint rings {
  uniform float time;
  uniform vec2 resolution;
  out vec4 color;

  void main() {
    vec2 uv = gl_FragCoord.xy / resolution;
    color = vec4(0.5 + 0.5 * cos(time + uv.xyx * 6.0 + vec3(0.0, 2.0, 4.0)), 1.0);
  }
}

@scene {
  cube;
}

cube {
  translate: 0 0.8 0;
  rotate-y: 30deg;
  texture: paint(rings);
}

a checkerboard, drawn once

Without time, the shader is drawn once, and the texture stays.

@paint checks {
  out vec4 color;

  void main() {
    vec2 cell = floor(gl_FragCoord.xy / 64.0);
    float on = mod(cell.x + cell.y, 2.0);
    color = vec4(mix(vec3(0.12), vec3(0.95), on), 1.0);
  }
}

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  texture: paint(checks);
}

@property

Registers a variable that the page changes from JavaScript while the scene runs, without compiling it again, like CSS @property. Like a variable on :root, it has one value for the whole scene.

@property --lift { syntax: "<number>"; inherits: false; initial-value: 1; }
Syntax
@property --<name> { syntax: "<number>" | "<angle>" | "<percentage>" | "<color>" | "<length>"; inherits: true | false; initial-value: <value>; }
See also
Set variables from JavaScript, @property-panel

Descriptors

syntax
What the variable holds: "<number>", "<angle>", "<percentage>", "<color>", or "<length>" in px, for the radius of blur() and bloom().
inherits
Required, like CSS: true or false.
initial-value
Its value until the page sets another one. scene { --lift: 2; } gives another start value; declared anywhere else, on an object, a group, a :hover rule or a frame, the variable is an error. A frame can read it.

A registered variable goes in transforms, colors, sizes, materials, lights, fog and filter. It cannot go where the scene is built when it compiles: the copies of * n, d, timings, dpr and random().

More details

The full list, wherever the shader reads a value at each frame: the transforms (translate, rotate-x, rotate-y, rotate-z, scale, transform-origin), color, opacity, background, mask-image, the sizes of the shapes (size, radius, height, thickness, corner-radius, stroke-width, depth), the numbers of a gradient, material, floor, ambient, light and the intensity of the point lights, fog, camera-target, blend, offset-distance, offset-rotate, texture-size and filter, alone or inside the math and color functions. Out of reach, built when the scene compiles: the copies of * n, d and view-box, the timing of animations and transitions, the camera the mouse moves, dpr and random(). A value set from JavaScript is never refused: it is kept in its range, so a size never goes below 0. With @property-panel, the playground and Try it show a control for each registered variable over the render.

a number

The sphere rises with --lift, which the page sets with scene.setProperty("--lift", "2").

@property --lift {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

@scene {
  sphere;
}

sphere {
  translate: 0 var(--lift) 0;
  radius: 0.5;
  color: #ff5a36;
}

a color and an angle

scene { --tint: … } gives a start value other than initial-value.

@property --tint {
  syntax: "<color>";
  inherits: false;
  initial-value: #3a7bff;
}

@property --turn {
  syntax: "<angle>";
  inherits: false;
  initial-value: 30deg;
}

@scene {
  cube;
}

scene {
  --tint: #ff5a36;
}

cube {
  translate: 0 0.8 0;
  rotate-y: var(--turn);
  color: var(--tint);
}

inside math and color functions

--hue goes through calc(), sin() and oklch(), computed by the GPU at each frame.

@property --hue {
  syntax: "<number>";
  inherits: false;
  initial-value: 20;
}

@scene {
  sphere * 5;
}

sphere {
  radius: 0.35;
  translate: calc(sibling-index() * 0.8 - 2.4) calc(0.6 + sin(var(--hue) * 1deg) * 0.3) 0;
  color: oklch(70% 0.16 calc(var(--hue) + sibling-index() * 30));
}

@property-panel

Shows a control for each variable of @property over the render, in the playground and in Try it: a slider and its number, or a color picker for a color. Moving a control sets the variable without compiling the scene again.

@property-panel { display: open; }
Syntax
@property-panel { display: open | folded | none; range: [<variable> <min> <max> <step>?]#; }
See also
@property, Set variables from JavaScript

Descriptors

display
open, the default: the panel shows its controls. folded: only its title, variables · 2, which opens it. none: no panel, as without the rule.
range
The ends of the slider of a variable, and its step if you like: --col 0 3 1. One per variable, separated by commas: range: --col 0 3, --row 0 3;. They are written like the values of the variable: 0deg 180deg for an angle, 0% 50% for a percentage.

The panel never writes into the code. A page that embeds the scene shows no panel: it sets the variables with setProperty().

More details

A slider goes from 0 to twice the start value of its variable, at least from 0 to 1, and around 0 for a negative value; an angle from 0 to 360deg, a percentage from 0% to 100%. A number typed past the end of a slider widens it, and the reset button gives the start value back. With a range, the slider keeps the ends of the scene: a number typed past one is held there, and the panel says what the end is. A color has a color picker and no range. When the code changes a start value, the code wins over the slider.

a slider

Drag --lift and the sphere rises, without compiling again.

@property --lift {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

@property-panel {
  display: open;
}

@scene {
  sphere;
}

sphere {
  translate: 0 var(--lift) 0;
  radius: 0.5;
  color: #ff5a36;
}

folded, with a color and an angle

The panel starts folded: its title opens it.

@property --tint {
  syntax: "<color>";
  inherits: false;
  initial-value: #3a7bff;
}

@property --turn {
  syntax: "<angle>";
  inherits: false;
  initial-value: 30deg;
}

@property-panel {
  display: folded;
}

@scene {
  cube;
}

cube {
  translate: 0 0.8 0;
  rotate-y: var(--turn);
  color: var(--tint);
}

a cube on a board of 4 by 4

range gives each slider the squares of the board, from 0 to 3, one square at a time.

@property --col {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

@property --row {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

@property-panel {
  range: --col 0 3 1, --row 0 3 1;
}

@scene {
  path#grid;
  cube#piece;
}

#grid {
  d: path("M0 0 H4 M0 1 H4 M0 2 H4 M0 3 H4 M0 4 H4 M0 0 V4 M1 0 V4 M2 0 V4 M3 0 V4 M4 0 V4");
  stroke-width: 0.04;
  rotate-x: 90deg;
  translate: 0 0.02 0;
  color: #1a1a1f;
}

#piece {
  size: 0.6;
  translate: calc(var(--col) - 1.5) 0.3 calc(var(--row) - 1.5);
  color: #ff5a36;
}

@scene

Declares the objects of the scene, one per line: a shape, then an optional #id, any number of .classes, and * n for n copies. Objects are combined in the order they are declared: see operation.

cubespheretorus 1torus 2torus 3
@scene { cube; sphere; torus * 3; }
Syntax
@scene { <shape>[#<id>][.<class>]* [* <integer>][;] … group[#<id>][.<class>]* [* <integer>] { … } }

Parts

<shape>
cube, sphere, torus… one of the shapes, or a point light.
#<id>
One per object. A multiplied id is numbered: torus#ring * 3 makes ring-1, ring-2 and ring-3.
.<class>
Any number of them: sphere.ball.big.
* <integer>
Copies of the object, each a sibling of its own.
group { … }
Holds objects and other groups, to move, turn or scale them together. A multiplied group copies everything inside it.

The ; after an object is optional: a new object or a } is enough.

a single cube

The smallest scene: one object, with the default style.

@scene {
  cube;
}

ids, classes and copies

A #base cube and three .ball spheres, styled by their id and their class.

@scene {
  cube#base;
  sphere.ball * 3;
}

#base {
  translate: 0 0.5 0;
}

.ball {
  translate: 0 1.5 0;
  radius: 0.3;
}

a group

The two cubes of #tower move and turn with their group.

@scene {
  cube#base
  group#tower {
    cube#a
    cube#b
  }
}

#base {
  translate: 1 0.5 0;
}

#tower {
  translate: -1 0 0;
  rotate-y: -30deg;
}

#a {
  translate: 0 0.5 0;
}

#b {
  translate: 0 1.5 0;
  scale: 0.7;
}

@mixin

Names a block of declarations and rules, written once and put into any rule with @apply, like the mixins of CSS. Its parameters are read with var(), and each can have a default. The scene compiles as if the block were written in each rule that applies it.

Syntax
@mixin --<name>[(--<parameter> [<type>] [: <default>], …)] { <declaration> … <rule> … [@contents;] }
See also
@apply, var()

Parts

--<name>
Starts with --, like a variable: @mixin --card { … }.
(--size, --tint: #ff5a36)
The parameters, given by @apply in order and read with var(--size) inside. After :, the default for a value left out. A type, like --size <number>, is read as in CSS; the property that gets the value checks it.
{ … }
Declarations, rules nested with &, @media, and other @apply.
@contents
Where the block given to @apply goes. @contents { … } holds what goes there when @apply gives none.

A mixin goes outside the rules, before or after the rules that apply it. From Sass: @apply replaces @include, var() replaces $, and @contents replaces @content.

More details

With two mixins of the same name, the last one wins, like CSS. A parameter hides a variable of the same name, and cannot be declared inside the mixin. The block given to @apply does not see the parameters: it belongs to the rule that applies the mixin.

a mixin, and the rules it saves

Both cubes take the two declarations of --polished: the scene is the same as with them written in each rule.

@mixin --polished {
  material: metal(0.15);
  corner-radius: 0.12;
}

@scene {
  cube#a;
  cube#b;
}

#a {
  translate: -0.9 0.5 0;
  color: #ff5a36;
  @apply --polished;
}

#b {
  translate: 0.9 0.5 0;
  color: #3a7bff;
  @apply --polished;
}

parameters and their defaults

--ball reads --x, --size and --tint with var(); a value left out takes its default.

@mixin --ball(--x, --size: 0.4, --tint: #3a7bff) {
  translate: var(--x) var(--size) 0;
  radius: var(--size);
  color: var(--tint);
}

@scene {
  sphere#a;
  sphere#b;
}

#a {
  @apply --ball(-1);
}

#b {
  @apply --ball(0.8, 0.7, #ff5a36);
}

nested rules

A mixin holds rules too: its &:hover becomes #a:hover, then #b:hover.

@mixin --lively {
  transition: 0.3s;
  &:hover {
    scale: 1.2;
    color: #ff5a36;
  }
}

@scene {
  cube#a;
  cube#b;
}

#a {
  translate: -0.9 0.5 0;
  @apply --lively;
}

#b {
  translate: 0.9 0.5 0;
  @apply --lively;
}

@contents

--on-small-screens holds the @media, and each rule gives the block to put inside it. Make the render narrower to see it.

@mixin --on-small-screens {
  @media (max-width: 600px) {
    @contents;
  }
}

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  color: #3a7bff;
  @apply --on-small-screens {
    color: #ff5a36;
    radius: 0.6;
  }
}

@apply

Puts the declarations and rules of a @mixin in the rule where it is written, like CSS. Values in parentheses go to the parameters of the mixin, in order; a block goes where the mixin writes @contents.

@mixin --warm { color: #ff5a36; }
Syntax
@apply --<name>[(<value>, …)] [{ <declaration> … }];
See also
@mixin, & (nesting)

Forms

@apply --card;
No values: each parameter takes its default. @apply --card(); is the same.
@apply --card(0.7, #ff5a36);
One value per parameter, in order; those left out take their default. A value with a comma of its own goes between braces: {color 1s, scale 2s}.
@apply --card { … }
The block goes where the mixin writes @contents. Values in parentheses can come before it: @apply --card(0.7) { … }.

The mixin takes the place of @apply in the cascade: a declaration written after it in the same rule wins, one written before loses.

More details

@apply goes in any rule: an object, a group, the scene, a :hover or :active rule, a nested rule, a @media, and a frame of @keyframes when the mixin holds declarations only. An unknown mixin, more values than parameters, a parameter with no value and no default, or a mixin that applies itself is an error that points at it.

where it stands in the rule

The mixin takes the place of @apply: in #a, its color comes after the blue one and wins; in #b, the blue one comes after it and wins.

@mixin --warm {
  color: #ff5a36;
}

@scene {
  cube#a;
  cube#b;
}

#a {
  translate: -0.9 0.5 0;
  color: #3a7bff;
  @apply --warm;
}

#b {
  translate: 0.9 0.5 0;
  @apply --warm;
  color: #3a7bff;
}

in a frame of @keyframes

--pose gives a height and a size, at rest and at the end of the hop.

@mixin --pose(--h, --s) {
  translate: 0 var(--h) 0;
  scale: var(--s);
}

@scene {
  cube;
}

cube {
  color: #ff5a36;
  @apply --pose(0.5, 1);
  animation: hop 1.2s ease-in-out alternate;
}

@keyframes hop {
  to {
    @apply --pose(1.6, 1.3);
  }
}

@for

Writes its block once for each number it counts, with var(--i) replaced by that number, like the @for of Sass. It goes anywhere: in @scene to make objects, around rules, in a rule, in @keyframes. The scene compiles as if each step were written by hand.

--i: 1--i: 2--i: 3--i: 4
@scene { @for --i from 1 through 4 { cube; } }
Syntax
@for --<name> from <number> through | to <number> [by <number>] { … }
See also
@each, sibling-index()

Parts

--<name>
The variable of the loop, read with var(--i) anywhere in the block: in a value, a selector, an attribute of @scene, after *.
from 1 through 4
Counts 1, 2, 3, 4: through counts the last number.
from 1 to 4
Counts 1, 2, 3: to stops before the last number. From the larger to the smaller, the loop counts down.
by 0.5
The size of a step, more than 0. Without it, 1.
0deg, 50%
The numbers can take a unit, the same for all three: from 0deg to 360deg by 45deg.

* n and sibling-index() already repeat an object; @for is for what they cannot do, like a grid that needs two indices, or rules written for each step.

More details

A loop takes 1000 steps at most. Loops can be nested, each with its own name. Inside the block, the variable of the loop hides a variable of the same name. The numbers are written in the file: a var() of a variable or of a parameter cannot set them.

objects and rules for each step

The loop in @scene makes five cubes; the loop around the rule gives each its height, and stands it on the floor.

@scene {
  @for --i from 1 through 5 {
    cube.step;
  }
}

.step {
  color: #ff5a36;
}

@for --i from 1 through 5 {
  .step:nth-child(var(--i)) {
    size: 0.8 calc(var(--i) * 0.4) 0.8;
    translate: calc(var(--i) * 1.1 - 3.3) calc(var(--i) * 0.2) 0;
  }
}

a grid

Two loops, one in the other, give each cube its row and its column in attributes; the rules read them back.

@scene {
  @for --row from 0 to 3 {
    @for --col from 0 to 3 {
      cube[data-row=var(--row)][data-col=var(--col)];
    }
  }
}

cube {
  scale: 0.8;
}

@for --row from 0 to 3 {
  @for --col from 0 to 3 {
    [data-row=var(--row)][data-col=var(--col)] {
      translate: calc(var(--col) - 1) 0.4 calc(var(--row) - 1);
      color: hsl(calc(var(--row) * 50 + var(--col) * 20) 80% 60%);
    }
  }
}

@each

Writes its block once for each value of a list, with var(--x) replaced by that value, like the @each of Sass. A value can be a number, a color, a word, even a shape: @each --shape in cube, sphere { var(--shape); } makes a cube, then a sphere.

cubesphereconetorus
@scene { @each --s in cube, sphere, cone, torus { var(--s); } }
Syntax
@each --<name> in <value>, <value>, … { … }
See also
@for, @scene

Parts

--<name>
The variable of the loop, read with var(--x) anywhere in the block, even in the place of a shape in @scene.
in tomato, gold, teal
The values, split at the commas. A value can hold several words: in 0 1 0, 1 0 0.
{a, b}
A value with a comma of its own goes between braces, like a value of @apply.

It goes anywhere @for goes: in @scene, around rules, in a rule, in @keyframes. Each value is written in place of the var(), as if the block were written by hand for it.

More details

A list takes 1000 values at most. An empty value is an error. Inside the block, the variable of the loop hides a variable of the same name.

a different shape at each step

The variable stands in the place of the shape: four objects of four shapes, in a row.

@scene {
  @each --shape in cube, sphere, cone, torus {
    var(--shape).piece;
  }
}

.piece {
  translate: calc(sibling-index() * 1.6 - 4) 0.6 0;
  color: #3a7bff;
}

a list of colors

Each sphere keeps its color in an attribute, and one rule per color paints it.

@scene {
  @each --c in tomato, gold, teal, orchid {
    sphere[data-color=var(--c)];
  }
}

sphere {
  translate: calc(sibling-index() * 1.3 - 3.25) 0.5 0;
}

@each --c in tomato, gold, teal, orchid {
  [data-color=var(--c)] {
    color: var(--c);
  }
}

Selectors

.class

Targets every object that has this class in @scene. An object can have several classes, and a selector can ask for several at once: .a.b.

.red.red
.red { color: #ff5a36; }
Specificity
100 per class

a class on one of two cubes

@scene {
  cube#a.red;
  cube#b;
}

#a {
  translate: 0.8 0.5 0;
}

#b {
  translate: -0.8 0.5 0;
}

.red {
  color: #ff5a36;
}

#id

Targets the object with this id. A multiplied id is numbered: torus#hero * 3 makes hero-1, hero-2 and hero-3.

#hero
#hero { color: #ff5a36; }
Specificity
10000

one sphere, by its id

@scene {
  sphere#hero;
  sphere;
}

sphere {
  translate: -0.8 0.5 0;
}

#hero {
  translate: 0.8 0.5 0;
  color: #ff5a36;
}

[attribute]

Targets objects by an attribute, like CSS. An object gets its attributes in @scene, like HTML: cube[data-col=2], sphere[lit], or [data-name="top left"] with spaces. Its id and its classes count as the attributes id and class, so [id^="hero-"] finds every copy of torus#hero * 3.

col=1col=2col=2col=3
[data-col=2] { color: #ff5a36; }
Specificity
100, like a class, even [id=…]

Forms

[attr]
The object has the attribute, whatever its value.
[attr=value]
The value is exactly this one. Quotes are optional for a single word or a number.
[attr~=value]
One of the words of the value, split at the spaces: [class~=big].
[attr|=value]
The value, or the value followed by a dash: [lang|=en] matches en and en-US.
[attr^=value]
The value starts with this.
[attr$=value]
The value ends with this.
[attr*=value]
The value contains this.
[attr=value i]
Ignores the case. s keeps it, which is already the default.

In @scene, an attribute takes = only, and every copy of a * n gets the same ones. id and class keep their own syntax there: cube#a.big. Like CSS, ~=, ^=, $= and *= never match an empty value.

[attr=value]

The cubes of column 2 turn red.

@scene {
  cube[data-col=1];
  cube[data-col=2];
  cube[data-col=2];
}

cube {
  size: 0.5;
  translate: calc((sibling-index() - 2) * -0.8) 0.4 0;
}

[data-col="2"] {
  color: #ff5a36;
}

[attr^=value]

Every copy of #hero is numbered: [id^="hero-"] styles them all, not the other torus.

@scene {
  torus#hero * 3;
  torus#other;
}

torus {
  radius: 0.3;
  thickness: 0.1;
  rotate-x: 70deg;
  translate: calc((sibling-index() - 2.5) * -0.9) 0.4 0;
}

[id^="hero-"] {
  color: #3a7bff;
}

[attr~=value] and the i flag

A list of words, read one by one, and a value whatever its case.

@scene {
  sphere[data-tags="round shiny"];
  sphere[data-tags=round];
  sphere[lang=EN];
}

sphere {
  radius: 0.3;
  translate: calc((sibling-index() - 2) * -0.9) 0.4 0;
}

[data-tags~=shiny] {
  material: gold;
}

[lang=en i] {
  color: #3ad16b;
}

*

Targets every object, never the settings of the scene. Any other selector beats it, wherever it is written: use it for defaults.

* { color: #ff5a36; }
Specificity
0

a default for every object

Every object is red, except the sphere, whose own rule wins.

@scene {
  cube;
  sphere;
}

* {
  color: #ff5a36;
}

cube {
  translate: 0.8 0.5 0;
}

sphere {
  translate: -0.8 0.5 0;
  color: #3ad16b;
}

a, b

A selector list gives the same declarations to each selector it names, as if the rule were written once for each.

cube, torus { color: #ff5a36; }
Specificity
Each selector keeps its own

two ids, one rule

@scene {
  cube#a;
  cube#b;
  sphere;
}

#a,
#b {
  color: #ff5a36;
}

#a {
  translate: 1.2 0.5 0;
}

#b {
  translate: 0 0.5 0;
}

sphere {
  translate: -1.2 0.5 0;
}

&

Nesting, like CSS: a rule can hold other rules, and & stands for the selector of the rule around it. In #g { &:hover { … } }, the nested rule is #g:hover.

group#g
#g { cube { color: #ff5a36; } }
Specificity
The sum of the rule around it and of the nested selector

Forms

&:hover
Joined to the parent: #g:hover.
#g &
The parent, placed anywhere: .a { #g & { … } } is #g .a.
cube
Without &, the parent comes first, then a space: #g { cube { … } } is #g cube.
> sphere
A leading combinator: #g { > sphere { … } } is #g > sphere.
@media
A query inside a rule: its declarations apply to that rule when the query matches.

Rules nest at any depth, and & also works inside :has() and :not().

More details

The declarations written after a nested rule come after it in the cascade, like CSS. With a list as the parent, a, b { & c { … } } gives a c and b c, each with its own specificity, where CSS gives both the specificity of the most specific selector of the list.

a group and what it holds

One rule styles the group, its cubes, their :hover, and its direct sphere.

@scene {
  group#row {
    cube.a * 3;
    sphere;
  }
}

#row {
  color: #e6e6e6;
  cube {
    size: 0.6;
    translate: calc((sibling-index() - 2.5) * -1) 0.3 0;
    &:hover {
      color: #ff5a36;
    }
  }
  > sphere {
    radius: 0.35;
    translate: -1.5 0.35 0;
    color: #3a7bff;
  }
}

a @media inside a rule

On a narrow screen, the torus changes color.

@scene {
  torus;
}

torus {
  radius: 0.8;
  thickness: 0.25;
  rotate-x: 70deg;
  color: #3a7bff;
  @media (max-width: 600px) {
    color: #ff5a36;
  }
}

!important

Written after a value, it makes the declaration win against every normal one, whatever their selectors. Between two !important declarations, specificity decides again.

Specificity
Beats every declaration without it

The cascade picks one value and never combines them: * { scale: 0.5 !important; } gives every object a scale of 0.5; it does not halve their own.

a default that wins

#a asks for green, but the !important red of * wins.

@scene {
  cube#a;
  sphere;
}

* {
  color: #ff5a36 !important;
}

#a {
  translate: 0.8 0.5 0;
  color: #3ad16b;
}

sphere {
  translate: -0.8 0.5 0;
}

Combinators

a b

A space means "inside": #letters cube targets the cubes of the group #letters, at any depth.

group#ggroup
#g cube { color: #ff5a36; }
Specificity
The sum of its parts

Like CSS, it reads from right to left: the last part is the object, and each part before it is one of its groups, further out each time.

More details

#letters#S asks for one object with two ids, which never exists: it is an error.

the cubes of a group

The two cubes inside #letters turn red; the one outside does not.

@scene {
  cube#a;
  group#letters {
    cube#b;
    cube#c;
  }
}

cube {
  translate: 1.2 0.5 0;
}

#letters cube {
  color: #ff5a36;
}

#b {
  translate: 0 0.5 0;
}

#c {
  translate: -1.2 0.5 0;
}

a > b

Targets direct children: #g > cube is the cubes right inside #g, not those of a nested group.

group#ggroup
#g > cube { color: #ff5a36; }
Specificity
The sum of its parts

It combines with spaces and sibling combinators: #g > group cube. The spaces around > are optional, and combinators add no specificity.

direct children only

Only #direct turns red: #nested is inside another group.

@scene {
  group#g {
    cube#direct;
    group {
      cube#nested;
    }
  }
}

cube {
  translate: 0.8 0.5 0;
}

#nested {
  translate: -0.8 0.5 0;
}

#g > cube {
  color: #ff5a36;
}

a + b

Targets the next sibling, with the same parent: sphere + cube is a cube declared right after a sphere, in @scene or in a group.

sphere + cube { color: #ff5a36; }
Specificity
The sum of its parts

The order is the order of declaration, copies of * n included, not the position in 3D. Groups count as siblings, empty ones too. sphere:hover + cube reacts only to the sphere right before. The spaces around + are optional.

the cube right after the sphere

#a turns red, and grows while the sphere is hovered; #b does not.

@scene {
  sphere;
  cube#a;
  cube#b;
}

sphere {
  translate: 1.4 0.5 0;
  radius: 0.4;
}

#a {
  translate: 0 0.5 0;
}

#b {
  translate: -1.4 0.5 0;
}

sphere + cube {
  color: #ff5a36;
}

sphere:hover + cube {
  scale: 1.2;
}

a ~ b

Targets every later sibling, with the same parent: sphere ~ cube is every cube after a sphere, even with other objects between them.

sphere ~ cube { color: #ff5a36; }
Specificity
The sum of its parts

It never targets an earlier sibling, nor the children of a sibling. Each copy of * n counts, and so do groups, empty ones too. Chains can mix every combinator, and the spaces around ~ are optional.

every cube after the sphere

Both cubes after the sphere turn red; the one before does not.

@scene {
  cube#before;
  sphere;
  cube#after * 2;
}

* {
  translate: calc((sibling-index() - 2.5) * -1.3) 0.5 0;
}

sphere {
  radius: 0.4;
}

sphere ~ cube {
  color: #ff5a36;
}

Pseudo-classes and cursor

:hover

A pseudo-class, like CSS: the rule applies while the mouse is over the object. With transition, the change glides instead of jumping.

cube:hover { translate: 0 0.25 0; color: #ff5a36; }
Specificity
100, like a class, added to the rest

Where it goes

cube:hover.big
Anywhere after the shape name.
sphere:hover + cube
On an earlier sibling: the cube reacts to the sphere.
#letters:hover cube
On a group: every cube of #letters reacts as soon as the mouse is over any object of the group, like hovering a child hovers its parent in CSS.
#g:has(sphere:hover)
Inside :has(): hovering one object changes another.

A :hover rule changes animatable properties only, and it styles objects, not groups: write #g:hover cube, not #g:hover { … }.

More details

The :hover rules join the cascade like any other: #a { color: blue; } beats cube:hover { color: red; }, and a normal !important beats them all. The animatable properties are the transforms, color, opacity, mask-image, offset-distance and variables: never a face. It can also start an animation: it begins when the pointer arrives and plays to its end, pointer gone or not (see :active).

lift on hover

Each cube rises, rotates and turns red under the mouse.

@scene {
  cube#a;
  cube#b;
}

cube {
  translate: 0.8 0.5 0;
  color: #e6e6e6;
}

#b {
  translate: -0.8 0.5 0;
}

cube:hover {
  translate: 0.8 1 0;
  color: #ff5a36;
  rotate-y: -45deg;
}

#b:hover {
  translate: -0.8 1 0;
}

hover a group

The mouse over any letter lights up every letter of #letters.

@scene {
  group#letters {
    cube#l1;
    cube#l2;
    cube#l3;
  }
  sphere;
}

#letters {
  translate: 1.6 0.5 0;
}

#letters cube {
  size: 0.4 1 0.4;
  color: #e6e6e6;
}

#l2 {
  translate: -0.7 0 0;
}

#l3 {
  translate: -1.4 0 0;
}

#letters:hover cube {
  color: #ff5a36;
  scale: 1.15;
}

sphere {
  translate: -1.4 0.5 0;
  radius: 0.5;
}

:active

A pseudo-class, like CSS: the rule applies while the object is pressed, with the mouse button or a finger. Like CSS, it stays pressed until the button goes up, even if the pointer leaves it.

cube:active { translate: 0 -0.1 0; }
Specificity
100, like a class, added to the rest

Where it goes

#button:active
On the object pressed.
sphere:active + cube
On an earlier sibling.
#g:active cube
On a group: every cube of #g is pressed when any of its objects is.
#lamp:has(#switch:active) #bulb
Inside :has(): press one object, change another.

Write :active after :hover: a pressed object is hovered too, and the later rule wins. Like :hover, it changes animatable properties only, and styles objects, not groups.

More details

cube:hover { scale: 1.1; } cube:active { scale: 0.95; } grows under the mouse and sinks when clicked. With transition, pressing takes the transition of the :active rule, and releasing the one of the hovered state. An animation in a :active rule starts at the press and plays to its end, released or not, like a button that fires: once, unless it says how many times; the pressed state stays on until the animation ends, or for good with forwards, and the next press plays it again.

a button that sinks when pressed

It rises under the mouse, and sinks quickly while pressed.

@scene {
  cube#button;
}

#button {
  size: 1.2 0.3 1.2;
  corner-radius: 0.1;
  translate: 0 0.15 0;
  color: #e6e6e6;
  transition: 0.25s ease-out;
}

#button:hover {
  color: #ff5a36;
  translate: 0 0.25 0;
}

#button:active {
  translate: 0 0.05 0;
  color: #c2401f;
  transition: 0.06s;
}

a click starts an animation

The press starts the fall, which plays to its end even once the button is up; forwards keeps the cube down, and the next click plays it again.

@scene {
  cube;
}

cube {
  size: 0.6 1.6 0.6;
  translate: 0 0.8 0;
  color: #ff5a36;
}

cube:active {
  animation: topple 0.8s ease-in forwards;
}

@keyframes topple {
  to {
    rotate-z: -90deg;
    translate: -0.8 0.3 0;
  }
}

press one, move another

Pressing the switch lights the bulb, through :has().

@scene {
  group#lamp {
    cylinder#switch;
    sphere#bulb;
  }
}

#switch {
  radius: 0.3;
  height: 0.2;
  translate: 0.8 0.1 0;
  color: #888888;
}

#switch:active {
  scale: 0.9;
}

#bulb {
  radius: 0.45;
  translate: -0.6 0.6 0;
  color: #555555;
  transition: 0.4s ease-out;
}

#lamp:has(#switch:active) #bulb {
  color: #ffd27a;
  scale: 1.15;
}

:has()

A pseudo-class, like CSS: a group matches when something inside it, at any depth, matches the selector in the parentheses. With :hover inside, hovering one object can change another.

groupgroup
group:has(sphere) cube { color: #ff5a36; }
Specificity
the most specific selector inside, added to the rest, like CSS

Forms

#g:has(sphere)
The groups that hold a sphere, read once, when the scene compiles.
#g:has(sphere:hover)
While a sphere of #g is under the mouse.
#g:has(#inner sphere)
A descendant selector, read from the group down.
#g:has(sphere, cube)
A list: one match is enough.
group:has(> sphere)
A leading combinator, relative to the subject: here, direct children.
cube:has(+ sphere)
The next sibling, or any later one with ~. Chains work too: cube:has(+ group > sphere).

Without a leading sibling combinator, :has() goes on a group: an object holds nothing, so cube:has(sphere) is an error.

More details

Empty groups can match, and a :has() cannot hold another one. Unlike CSS, a list that mixes a static selector with :hover matches only during a hover.

the groups that hold a sphere

Only the cube of #a, the group with a sphere, turns blue.

@scene {
  group#a {
    sphere#sa;
    cube#ca;
  }
  group#b {
    cube#cb;
  }
}

#a {
  translate: 1 0 0;
}

#b {
  translate: -1 0 0;
}

sphere {
  translate: 0 1.4 0;
  radius: 0.3;
}

cube {
  translate: 0 0.5 0;
  color: #e6e6e6;
}

group:has(sphere) cube {
  color: #3a7bff;
}

the next sibling, under the mouse

The cube reacts when the sphere right after it is hovered.

@scene {
  cube;
  sphere;
}

cube {
  translate: 0.8 0.5 0;
  transition: 0.3s ease-out;
}

sphere {
  translate: -0.8 0.5 0;
  radius: 0.4;
}

cube:has(+ sphere:hover) {
  color: #ff5a36;
  scale: 1.2;
}

hover one object, change another

Hovering the bulb changes the stand of the same lamp.

@scene {
  group#lamp {
    sphere#bulb;
    cylinder#stand;
  }
}

#bulb {
  translate: 0 1.6 0;
  radius: 0.35;
  color: #e6e6e6;
}

#stand {
  translate: 0 0.6 0;
  radius: 0.08;
  height: 1.2;
  color: #888888;
  transition: 0.3s ease-out;
}

#lamp:has(#bulb:hover) #stand {
  color: #ff5a36;
  scale: 1.2;
}

:not()

A pseudo-class, like CSS: the object matches when none of the selectors in the parentheses does. cube:not(.red) is every cube without the class red.

.red
cube:not(.red) { color: #4f8cff; }
Specificity
its most specific selector, added to the rest, like CSS

Forms

:not(.red, torus)
A list: leaves out both.
cube:not(#g cube)
Any selector, read from the object outwards: every cube outside #g.
cube:not(:first-child, :last-child)
The cubes in the middle.
group:not(:has(sphere))
The groups without a sphere.
:not(.a):not(.b)
Several in a row.

It weighs like the most specific selector of its list, like CSS: cube:not(#hero) beats #hero alone. It is read once, when the scene compiles: it cannot hold :hover or a face.

every cube but the red ones

Every cube rises, except the two .red ones.

@scene {
  cube.red;
  cube * 3;
  cube.red;
}

cube {
  size: 0.6;
  translate: calc((sibling-index() - 3) * -0.9) 0.3 0;
}

.red {
  color: #ff5a36;
}

cube:not(.red) {
  color: #e6e6e6;
  translate: calc((sibling-index() - 3) * -0.9) 0.8 0;
}

the ones in the middle

The spheres between the first and the last turn blue.

@scene {
  sphere * 6;
}

sphere {
  radius: 0.3;
  translate: calc((sibling-index() - 3.5) * -0.75) 0.4 0;
  color: #e6e6e6;
}

sphere:not(:first-child,
:last-child) {
  color: #3a7bff;
}

:nth-child(), :nth-last-child()

A pseudo-class, like CSS: the position of the object among its siblings, the objects of the same @scene block or group, counted from 1. :nth-last-child() counts from the end.

12345
:nth-child(odd) { color: #ff5a36; }
Specificity
100, like a class, plus the most specific selector after of

Arguments

3
The third sibling.
odd, even
Every other one: 1, 3, 5… or 2, 4, 6…
An+B
A formula where n runs from 0 up: 2n+1 is 1, 3, 5…, 3n every third, -n+3 the first three.
An+B of <selector>
Counts only the siblings that match the list, and the object must match it too: :nth-child(2 of .red) is the second .red.

Each copy of * n is a sibling of its own. Groups count as siblings, and the count starts again inside each group.

More details

In @scene { cube * 4; sphere; }, cube:nth-child(odd) is cubes 1 and 3, and the sphere is child 5, as sibling-index() counts. The position is read once, when the scene compiles: of cannot hold :hover.

odd, and every third

The odd cubes turn red, and every third one rises.

@scene {
  cube * 7;
}

cube {
  size: 0.6;
  translate: calc((sibling-index() - 4) * -0.8) 0.3 0;
  color: #e6e6e6;
}

cube:nth-child(odd) {
  color: #ff5a36;
}

cube:nth-child(3n) {
  translate: calc((sibling-index() - 4) * -0.8) 1 0;
}

even of .lit, and the last child

The even ones among the .lit spheres turn blue, and the last child grows.

@scene {
  sphere.lit * 3;
  sphere * 2;
  sphere.lit * 3;
}

sphere {
  radius: 0.3;
  translate: calc((sibling-index() - 4.5) * -0.75) 0.4 0;
  color: #555555;
}

.lit {
  color: #e6e6e6;
}

:nth-child(even of .lit) {
  color: #3a7bff;
}

:nth-last-child(1) {
  scale: 1.3;
}

:nth-of-type(), :nth-last-of-type()

Like :nth-child(), counting only the siblings of the same shape: in @scene { cube * 2; sphere; cube; }, cube:nth-of-type(3) is the last cube, though it is the fourth child.

1234
cube:nth-of-type(3) { color: #ff5a36; }
Specificity
100, like a class

The type is the shape name (cube, sphere, group…), like the tag of an HTML element. It takes the same An+B as :nth-child(), without of.

even cubes, last sphere

Counted among their own shape, the even cubes turn red and the last sphere blue.

@scene {
  cube * 2;
  sphere;
  cube * 2;
  sphere;
}

* {
  translate: calc((sibling-index() - 3.5) * -0.9) 0.4 0;
  color: #e6e6e6;
}

cube {
  size: 0.6;
}

sphere {
  radius: 0.35;
}

cube:nth-of-type(even) {
  color: #ff5a36;
}

sphere:nth-last-of-type(1) {
  color: #3a7bff;
}

:first-child, :last-child, :only-child

Shortcuts, like CSS: :first-child is :nth-child(1) and :last-child is :nth-last-child(1). Each has an -of-type form that counts only the siblings of the same shape. They take no argument.

:first-child, :last-child { color: #ff5a36; }
Specificity
100, like a class

Forms

:first-child
The first sibling.
:last-child
The last sibling.
:only-child
An object without siblings.
:first-of-type
The first of its shape: cube:first-of-type is the first cube of its block, even after a sphere.
:last-of-type
The last of its shape.
:only-of-type
The only one of its shape among its siblings.

They read the order of @scene, not the position in 3D.

first and last child

The first cube turns red, the last one blue.

@scene {
  cube * 5;
}

cube {
  size: 0.6;
  translate: calc((sibling-index() - 3) * -0.9) 0.3 0;
  color: #e6e6e6;
}

cube:first-child {
  color: #ff5a36;
}

cube:last-child {
  color: #3a7bff;
}

first of type, only child

In #a, the first cube follows a sphere and is still :first-of-type; the lone cube of #b is an only child.

@scene {
  group#a {
    sphere;
    cube * 2;
  }
  group#b {
    cube;
  }
}

#a {
  translate: 1 0 0;
}

#b {
  translate: -1.4 0 0;
}

* {
  color: #e6e6e6;
}

sphere {
  translate: 0 1.2 0;
  radius: 0.3;
}

cube {
  size: 0.5;
  translate: calc(0.9 - sibling-index() * 0.6) 0.25 0;
}

cube:first-of-type {
  color: #ff5a36;
}

cube:only-child {
  color: #3a7bff;
}

::face(), ::top, ::bottom

A pseudo-element, like ::part() in CSS: the rule styles one face of the object, not the object. A face takes texture and content. It is named, or numbered like :nth-child().

  1. cube::face(1)
  2. pyramid::face(1)
  3. octahedron::face(1)
cube::face(1) { content: "1"; }
Specificity
1, like a tag, added to the rest

Faces

top, bottom
Up (+y) and down, in the object's own space: the faces turn with it.
front, back
Toward +z and toward −z.
right, left
Toward +x and toward −x.
::top, ::bottom
Shortcuts for ::face(top) and ::face(bottom).
<number>
A face by its number: ::face(3). A cube is numbered like a die, its opposite faces adding up to 7: front 1, top 2, right 3, left 4, bottom 5, back 6.
odd, even, An+B
Several faces, like :nth-child(): ::face(odd), ::face(2n), ::face(n + 5) (5 and the faces after it).

A face without a rule of its own shows the texture and the text of the object. Like CSS, the pseudo-element ends the selector: cube.grass::top, never #g::top cube.

More details

Every shape seen from six sides (a cube, a sphere, a cylinder, a cone, a capsule, a tube, a torus, a plane and the shapes drawn with path) has the six faces of the cube and its numbers: on a round shape, a face is the part that looks that way the most. A pyramid has five faces: its slopes from the front, turning right (front 1, right 2, back 3, left 4), then its base, 5 or bottom. An octahedron has eight triangles, with numbers only: 1 to 4 on top, from the front-right turning right (front-right, back-right, back-left, front-left), 5 to 8 below them, in the same order. A number the shape does not have, like cube::face(7), is an error; so is the content of a name the shape does not have, like pyramid::face(top).

::face()

One face gets its own texture; the other five keep the object's.

@scene {
  cube.furnace;
}

.furnace {
  translate: 0 0.5 0;
  rotate-y: -30deg;
  texture: url("/textures/dirt.png");
  image-rendering: pixelated;
}

.furnace::face(front) {
  texture: url("/textures/grass-top.png");
}

::top

A grass block: the top has its own texture.

@scene {
  cube.grass;
}

.grass {
  translate: 0 0.5 0;
  texture: url("/textures/grass-side.png");
  image-rendering: pixelated;
}

.grass::top {
  texture: url("/textures/grass-top.png");
}

::bottom

The block is turned over: its bottom face, now on top, shows dirt.

@scene {
  cube.flip;
}

.flip {
  translate: 0 0.6 0;
  rotate-x: 150deg;
  texture: url("/textures/grass-side.png");
  image-rendering: pixelated;
}

.flip::bottom {
  texture: url("/textures/dirt.png");
}

::face() with a number

A die: each face writes its own number, the opposite faces adding up to 7.

@scene {
  cube;
}

scene {
  camera-angle: 35deg 30deg;
}

cube {
  translate: 0 0.6 0;
  color: #f4f1ea;
  corner-radius: 0.08;
  font-weight: bold;
}

cube::face(1) {
  content: "1";
}

cube::face(2) {
  content: "2";
}

cube::face(3) {
  content: "3";
}

cube::face(4) {
  content: "4";
}

cube::face(5) {
  content: "5";
}

cube::face(6) {
  content: "6";
}

::face(odd), on an octahedron

The faces of an octahedron have numbers only: one in two takes a word, the others stay plain.

@scene {
  octahedron;
}

scene {
  camera-angle: 30deg 25deg;
}

octahedron {
  translate: 0 0.9 0;
  radius: 0.9;
  color: #3ad16b;
  font-weight: bold;
  animation: turn 8s linear infinite;
}

octahedron::face(odd) {
  content: "GSS";
}

@keyframes turn {
  to {
    rotate-y: 360deg;
  }
}

::face(even), on a pyramid

The slopes of a pyramid are 1 to 4 from the front, turning right, and its base is 5: the even faces, its right and left slopes, take their own word.

@scene {
  pyramid;
}

scene {
  camera-angle: 30deg 25deg;
}

pyramid {
  translate: 0 0.7 0;
  size: 1.8 1.4 1.8;
  color: #722cff;
  font-weight: bold;
  content: "odd";
}

pyramid::face(even) {
  content: "even";
}

::face(), ::top and ::bottom together

Two grass blocks, the second turned over: each face keeps its texture as the block turns.

@scene {
  cube.grass * 2;
}

.grass {
  translate: calc(2.1 - sibling-index() * 1.4) 0.5 0;
  rotate-y: -30deg;
  rotate-x: calc(sibling-index() * 150deg - 150deg);
  texture: url("/textures/grass-side.png");
  image-rendering: pixelated;
}

.grass::top {
  texture: url("/textures/grass-top.png");
}

.grass::bottom {
  texture: url("/textures/dirt.png");
}

.grass::face(front) {
  texture: url("/textures/dirt.png");
}

cursor

The mouse pointer over the object, like CSS: pointer says it can be clicked, grab that it can be held. It takes the keywords of CSS cursor.

cursor: pointer;
Syntax
auto | default | pointer | grab | grabbing | help | crosshair | move | not-allowed | zoom-in | zoom-out | …
Initial value
auto
Applies to
objects
Animatable
no

Like CSS, it is inherited: a group gives its cursor to its objects. A :hover or :active rule can change it, like cube:active { cursor: grabbing; }.

More details

A cursor changes at once: it does not glide. Over the background and the floor, the page keeps its own cursor. A hidden object has none, since the mouse goes through it.

a button

The cube shows the hand of a link.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
  color: #ff5a36;
  cursor: pointer;
  transition: 0.2s;
}

cube:active {
  scale: 0.9;
}

grab and grabbing

An open hand over the sphere, closed while it is pressed.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  radius: 0.7;
  color: #3a7bff;
  cursor: grab;
}

sphere:active {
  cursor: grabbing;
}

a cursor each

Hover each cube: pointer, help, crosshair, move, zoom-in and not-allowed.

@scene {
  cube * 6;
}

scene {
  camera-distance: 7;
}

cube {
  size: 0.7;
  translate: calc(sibling-index() * 1 - 3.5) 0.4 0;
  color: #3a7bff;
}

cube:nth-child(1) {
  cursor: pointer;
}

cube:nth-child(2) {
  cursor: help;
}

cube:nth-child(3) {
  cursor: crosshair;
}

cube:nth-child(4) {
  cursor: move;
}

cube:nth-child(5) {
  cursor: zoom-in;
}

cube:nth-child(6) {
  cursor: not-allowed;
  color: #6b6b78;
}

a disabled button

The grey button says it cannot be pressed: not-allowed, and nothing happens; the orange one sinks.

@scene {
  cube#on;
  cube#off;
}

cube {
  size: 1 0.3 1;
  transition: 0.15s;
}

#on {
  translate: -0.8 0.15 0;
  color: #ff5a36;
  cursor: pointer;
}

#on:active {
  translate: -0.8 0.05 0;
}

#off {
  translate: 0.8 0.15 0;
  color: #6b6b78;
  cursor: not-allowed;
}

from a group

The group gives pointer to its three spheres; the cone, outside it, keeps the page's cursor.

@scene {
  group#buttons {
    sphere * 3;
  }
  cone;
}

#buttons {
  cursor: pointer;
}

#buttons sphere {
  radius: 0.3;
  translate: calc(sibling-index() * 0.8 - 1.6) 0.3 0;
  color: #3ad16b;
}

cone {
  translate: 1.6 0.5 0;
  color: #6b6b78;
}

Variables and conditions

if()

Picks a value by condition, like CSS: the first branch whose condition is true gives its value. End with else: when no branch is true, it is an error.

scale: if(media(width < 600px): 0.6; else: 1);
Syntax
if(<condition>: <value>; …; else: <value>)
Computed
at compile time, once per object

Conditions

media(<query>)
True when the screen matches the query, like @media.
style(--x)
True when the custom property is set on the object, or inherited.
style(--x: <value>)
True when it has that value.
else
Always true.
not, and, or
Combine conditions; and and or cannot be mixed without parentheses.

A media() condition makes a version of the scene for its query, like @media, and counts as one of its queries.

if() with style()

Each group sets --theme, and its spheres pick their color from it.

@scene {
  group#warm {
    sphere * 3
  }
  group#cool {
    sphere * 3
  }
}

#warm {
  --theme: warm;
  translate: 1 0 0;
}

#cool {
  --theme: cool;
  translate: -1 0 0;
}

sphere {
  radius: 0.3;
  translate: 0 calc(sibling-index() * 0.7) 0;
  color: if(style(--theme: warm): #ff5a36;
  else: #3a7bff);
}

if() with media()

A smaller sphere on a narrow screen, and a blue one in dark mode.

@scene {
  sphere;
}

sphere {
  radius: if(media(width < 600px): 0.4;
  else: 0.8);
  translate: 0 1 0;
  color: if(media(prefers-color-scheme: dark): #7cb4ff;
  else: #ff5a36);
}

var()

Reads a custom property, like CSS. A property whose name starts with -- is a variable: declared on the scene (like :root), on a group or on an object, it is inherited down to the object, and the closest one wins.

sphere { radius: var(--size); } cube { size: var(--size); }
Syntax
var(--<name>) | var(--<name>, <fallback>)
Computed
at compile time, once per object

Forms

var(--name)
The value of the variable. A missing variable is an error, so a typo is never ignored.
var(--name, fallback)
The fallback, when the variable is not defined.

A variable can hold several values (translate: var(--pos)), use another one (--big: calc(var(--size) * 2)), and go inside calc().

More details

A frame of @keyframes can set a variable: every animatable property that uses it moves with it. The compiler replaces the variables with their values, unless @property registers them: the shader receives only numbers.

a color and a radius

#b overrides the --r of the scene.

@scene {
  sphere#a;
  sphere#b;
}

scene {
  --accent: #ff5a36;
  --r: 0.4;
}

sphere {
  radius: var(--r);
  color: var(--accent);
}

#a {
  translate: 0.8 0.5 0;
}

#b {
  --r: 0.6;
  translate: -0.8 0.6 0;
}

inherited from a group

The cones read --green from their group, and each computes its own --size.

@scene {
  group#tree {
    cone.level * 5;
  }
}

#tree {
  --green: #3ad16b;
}

.level {
  --size: calc(sibling-index() * 0.12);
  radius: var(--size);
  height: var(--size);
  translate: 0 calc(1.3 - sibling-index() * 0.18) 0;
  color: var(--green);
}

set by @keyframes

The frame sets --lift, and the translate of each sphere moves with it.

@scene {
  sphere * 3;
}

sphere {
  --lift: 0;
  radius: 0.3;
  translate: calc(1.8 - sibling-index() * 0.9) calc(0.4 + var(--lift) * sibling-index()) 0;
  color: #ff5a36;
  animation: rise 2s ease-in-out alternate;
}

@keyframes rise {
  to {
    --lift: 0.4;
  }
}

Math

calc()

Computes a value, like CSS calc(): + - * / and parentheses, with numbers, angles (deg, rad, turn) and durations (s, ms). Like CSS, + and - need a space on each side.

rotate-y: calc(360deg / 5 * sibling-index());
Syntax
calc(<expression>)
Computed
at compile time, once per object

The compiler computes it once per object: the shader receives only the result, so the math costs nothing on the GPU. calc() and the other math functions work in any value, even inside another function: metal(#fff, calc(0.1 * 2)).

a staircase

Each copy climbs a step higher, with sibling-index().

@scene {
  cube.step * 5;
}

.step {
  size: 0.4;
  translate: calc(1.8 - sibling-index() * 0.6) calc(sibling-index() * 0.25) 0;
  color: #ff5a36;
}

abs(), sqrt(), pow()

Like CSS: abs() drops the sign, sqrt() is the square root, and pow(a, b) is a to the power b. pow() grows fast: good for sizes that double.

scale: pow(2, sibling-index());
Syntax
abs(<value>) | sqrt(<number>) | pow(<number>, <number>)
Computed
at compile time, once per object

abs()

A V: the bars grow away from the middle.

@scene {
  cube.v * 9;
}

.v {
  --h: calc(0.3 + abs(sibling-index() - 5) * 0.25);
  size: 0.3 var(--h) 0.3;
  translate: calc(2.25 - sibling-index() * 0.45) calc(var(--h) / 2) 0;
  color: #7cb4ff;
}

sqrt()

Sizes that grow slower and slower.

@scene {
  sphere.dot * 6;
}

.dot {
  --r: calc(sqrt(sibling-index()) * 0.15);
  radius: var(--r);
  translate: calc(2.45 - sibling-index() * 0.7) var(--r) 0;
  color: #3ad16b;
}

pow()

Each sphere 1.4 times bigger than the one before.

@scene {
  sphere.dot * 5;
}

.dot {
  radius: calc(pow(1.4, sibling-index()) * 0.08);
  translate: calc(2.4 - sibling-index() * 0.8) 0.6 0;
  color: #ff5a36;
}

abs(), sqrt() and pow() together

A sunflower head of 24 seeds.

@scene {
  sphere.seed * 24;
}

.seed {
  --d: calc(sqrt(sibling-index()) * 0.4);
  radius: calc(0.06 + pow(sibling-index() / 24, 2) * 0.14);
  translate: calc(-1 * cos(sibling-index() * 137.5deg) * var(--d)) calc(0.25 + abs(sibling-index() - 12) * 0.03) calc(sin(sibling-index() * 137.5deg) * var(--d));
  color: hsl(calc(sibling-index() * 15) 80% 60%);
}

asin(), acos(), atan(), atan2()

The inverse trigonometric functions of CSS: they take a number and return an angle, in deg. atan2(y, x) gives the angle of the point (x, y), whatever its quarter: the way to turn an object toward a point. Its two values must share a unit.

rotate-z: atan2(<y>, <x>);
Syntax
asin(<number>) | acos(<number>) | atan(<number>) | atan2(<y>, <x>)
Computed
at compile time, once per object

asin()

Bars tilted by asin().

@scene {
  cube.tilt * 5;
}

.tilt {
  size: 0.2 0.9 0.2;
  translate: calc(1.8 - sibling-index() * 0.6) 0.45 0;
  rotate-z: asin(calc(0.6 - sibling-index() * 0.2));
  color: #ff5a36;
}

acos()

A fan opened by acos().

@scene {
  cube.fan * 5;
}

.fan {
  size: 0.9 0.1 0.2;
  translate: 0 calc(sibling-index() * 0.3) 0;
  rotate-y: acos(calc(1.2 - sibling-index() * 0.4));
  color: #7cb4ff;
}

atan()

Ramps that tilt more and more, toward 90°.

@scene {
  cube.ramp * 5;
}

.ramp {
  size: 0.8 0.08 0.3;
  translate: calc(2.7 - sibling-index() * 0.9) 0.5 0;
  rotate-z: atan(calc(sibling-index() * -0.4));
  color: #3ad16b;
}

atan2()

Needles turned along the ring by atan2().

@scene {
  cube.needle * 8;
}

.needle {
  --a: calc(sibling-index() * 45deg);
  size: 0.5 0.08 0.08;
  translate: calc(cos(var(--a)) * -1.4) 0.5 calc(sin(var(--a)) * 1.4);
  rotate-y: atan2(sin(var(--a)), cos(var(--a)));
  color: #ff5a36;
}

asin(), acos(), atan() and atan2() together

A curve drawn with all four.

@scene {
  sphere.dot * 10;
}

.dot {
  --x: calc(sibling-index() * 0.2 - 1.1);
  radius: 0.12;
  translate: calc(var(--x) * -2) calc(1 + sin(asin(var(--x)) + acos(var(--x))) * 0.4) calc(atan(var(--x)) / 90deg);
  rotate-y: atan2(var(--x), 1);
  color: hsl(calc(sibling-index() * 36) 80% 60%);
}

hypot(), log(), exp()

The exponential functions of CSS, for sizes and distances that grow fast or slowly.

scale: exp(sibling-index() / 4);
Syntax
hypot(<value>, …) | log(<number>, <base>?) | exp(<number>)
Computed
at compile time, once per object

Functions

hypot(<value>, …)
The length of a vector: hypot(3, 4) is 5, the distance from the center to the point (3, 4).
log(<number>, <base>)
The natural logarithm, or the logarithm in a base: log(8, 2) is 3.
exp(<number>)
e to a power. e is also a constant.

hypot()

Bigger and bigger away from the center.

@scene {
  sphere.dot * 9;
}

.dot {
  --x: calc(mod(sibling-index() - 1, 3) - 1);
  --z: calc(round(down, calc((sibling-index() - 1) / 3)) - 1);
  radius: calc(0.12 + hypot(var(--x), var(--z)) * 0.1);
  translate: calc(var(--x) * -0.9) 0.4 calc(var(--z) * 0.9);
  color: #7cb4ff;
}

log()

Bars that grow slower and slower.

@scene {
  cube.bar * 8;
}

.bar {
  --h: calc(0.2 + log(sibling-index(), 2) * 0.4);
  size: 0.3 var(--h) 0.3;
  translate: calc(2 - sibling-index() * 0.45) calc(var(--h) / 2) 0;
  color: #3ad16b;
}

exp()

Spheres that grow faster and faster.

@scene {
  sphere.dot * 6;
}

.dot {
  radius: calc(exp(sibling-index() / 3) * 0.06);
  translate: calc(2.45 - sibling-index() * 0.7) 0.6 0;
  color: #ff5a36;
}

hypot(), log() and exp() together

A spiral of seeds that shrink outward.

@scene {
  sphere.seed * 12;
}

.seed {
  --a: calc(sibling-index() * 30deg);
  --d: calc(log(sibling-index() + 1) * 0.8);
  radius: calc(exp(0 - sibling-index() / 8) * 0.25);
  translate: calc(-1 * cos(var(--a)) * var(--d)) calc(0.3 + hypot(cos(var(--a)), 1) * 0.2) calc(sin(var(--a)) * var(--d));
  color: hsl(calc(sibling-index() * 30) 80% 60%);
}

min(), max(), clamp()

The smallest or the largest of their values, or a value kept between two bounds, like CSS. The values must share a unit: max(10deg, 20deg), not max(10deg, 2).

scale: clamp(0.25, var(--size), 0.75);
Syntax
min(<value>, …) | max(<value>, …) | clamp(<min>, <value>, <max>)
Computed
at compile time, once per object

Functions

min(<value>, …)
The smallest.
max(<value>, …)
The largest.
clamp(<min>, <value>, <max>)
The value, kept between min and max.

min()

Bars that grow, up to 1.2.

@scene {
  cube.bar * 6;
}

.bar {
  --h: min(sibling-index() * 0.35, 1.2);
  size: 0.3 var(--h) 0.3;
  translate: calc(1.75 - sibling-index() * 0.5) calc(var(--h) / 2) 0;
  color: #ff5a36;
}

max()

Bars never shorter than 0.6.

@scene {
  cube.bar * 6;
}

.bar {
  --h: max(0.6, sibling-index() * 0.3);
  size: 0.3 var(--h) 0.3;
  translate: calc(1.75 - sibling-index() * 0.5) calc(var(--h) / 2) 0;
  color: #3a7bff;
}

clamp()

Bars kept between 0.5 and 1.4.

@scene {
  cube.bar * 6;
}

.bar {
  --h: clamp(0.5, sibling-index() * 0.35, 1.4);
  size: 0.3 var(--h) 0.3;
  translate: calc(1.75 - sibling-index() * 0.5) calc(var(--h) / 2) 0;
  color: #3ad16b;
}

min(), max() and clamp() together

Heights and widths, each one bounded.

@scene {
  cube.bar * 8;
}

.bar {
  --h: max(0.3, min(sibling-index() * 0.3, 1.5));
  --w: clamp(0.15, sibling-index() * 0.05, 0.35);
  size: var(--w) var(--h) var(--w);
  translate: calc(2.25 - sibling-index() * 0.5) calc(var(--h) / 2) 0;
  color: hsl(calc(sibling-index() * 40) 80% 60%);
}

progress()

Where a value sits between a start and an end, from 0 to 1, like the CSS function: progress(sibling-index(), 1, sibling-count()) goes from 0 for the first copy to 1 for the last. The result is kept between 0 and 1, and the three values must share a unit.

00.250.50.751
cube { opacity: progress(sibling-index(), 1, sibling-count()); }
Syntax
progress(<value>, <start>, <end>)
Computed
at compile time, once per object

a ramp of heights and hues

Each bar grows and changes hue with its progress.

@scene {
  cube.fade * 8;
}

.fade {
  --p: progress(sibling-index(), 1, sibling-count());
  size: 0.35 calc(0.2 + var(--p)) 0.35;
  translate: calc(2.25 - sibling-index() * 0.5) calc(0.1 + var(--p) / 2) 0;
  color: hsl(calc(200 + var(--p) * 160) 80% 60%);
}

random()

A random value between a minimum and a maximum, like CSS: random(0.2, 1.4), random(0deg, 360deg). The value is chosen once, when the scene compiles, and stays the same at every reload.

0.310.920.080.640.470.77
cube { translate: 0 random(0, 1) 0; }
Syntax
random([--<name> || element-shared | fixed <number>,]? <min>, <max>, <step>?)
Computed
at compile time, once per object

Forms

random(<min>, <max>)
Any value between them. Each object, property and call gets its own, so one rule scatters every copy.
random(<min>, <max>, <step>)
One of min, min + step… up to max: random(0deg, 180deg, 45deg).
random(--<name>, …)
One value shared by the calls of an object that use this name: the same number for x and z.
random(element-shared, …)
The same value for every object.
random(fixed <number>, …)
The random number set by hand, from 0 to just below 1.

The values must share a unit.

a sky of stars

Thirty stars, each with its own size, place and hue.

@scene {
  sphere.star * 30;
}

.star {
  radius: random(0.05, 0.2);
  translate: random(-2.5, 2.5) random(0.3, 2.2) random(-2, 1);
  color: hsl(random(180, 260) 80% 70%);
}

random() with a step and --name

--size gives each block one size for its width and its height, and its turn goes by steps of 15°.

@scene {
  cube.block * 16;
}

.block {
  --s: random(--size, 0.2, 0.5);
  size: var(--s);
  translate: calc(1.2 - mod(sibling-index() - 1, 4) * 0.8) calc(var(--s) / 2) calc(round(down, calc((sibling-index() - 1) / 4)) * 0.8 - 1.2);
  rotate-y: random(0deg, 90deg, 15deg);
  color: oklch(70% 0.15 random(0, 360, 60));
}

sibling-count()

How many siblings the object has, itself included, like the CSS function. Divided into a full turn, it spreads objects evenly, whatever their number: change * 8 into * 20, and the ring follows.

sphere { rotate-y: calc(360deg / sibling-count() * sibling-index()); }
Syntax
sibling-count()
Computed
at compile time, once per object

beads in a ring

@scene {
  sphere.bead * 8;
}

.bead {
  radius: 0.2;
  translate: calc(cos(sibling-index() * 1turn / sibling-count()) * -1.4) 0.5 calc(sin(sibling-index() * 1turn / sibling-count()) * 1.4);
  material: jelly(0.6);
  color: #ff5a36;
}

sibling-index()

The position of the object among its siblings, from 1, like the CSS function: cube * 12 makes 12 siblings numbered 1 to 12. With calc(), one rule gives each copy its own place, angle or size: no loop needed.

12345
cube { translate: 0 calc(sibling-index() * 0.2) 0; }
Syntax
sibling-index()
Computed
at compile time, once per object

Siblings are the objects of the same @scene block or group. A group is a sibling too, and the count starts again inside each group. It means nothing in scene { } or in @keyframes, shared by every object.

a ring of petals

Twelve copies, each placed and turned 30° further than the one before.

@scene {
  cube.petal * 12;
}

.petal {
  size: 0.25 0.6 0.25;
  translate: calc(cos(sibling-index() * 30deg) * -1.6) 0.5 calc(sin(sibling-index() * 30deg) * 1.6);
  rotate-y: calc(sibling-index() * 30deg);
  color: #ff5a36;
}

sign(), round(), mod(), rem()

The stepped functions of CSS. Their values must share a unit: round(37deg, 15deg) is 30deg.

rotate-y: round(var(--angle), 15deg);
Syntax
sign(<value>) | round([nearest | up | down | to-zero,]? <value>, <step>?) | mod(<value>, <value>) | rem(<value>, <value>)
Computed
at compile time, once per object

Functions

sign(<value>)
-1, 0 or 1.
round(<value>, <step>)
The value snapped to a multiple of its step (1 by default): nearest (halfway goes up), up, down or to-zero, written first.
mod(<value>, <value>)
The rest of a division, with the sign of the divisor: mod(-7, 3) is 2.
rem(<value>, <value>)
The rest, with the sign of the value: rem(-7, 3) is -1.

sign()

Above or below, by the sign of each position.

@scene {
  cube.side * 9;
}

.side {
  size: 0.3;
  translate: calc(2.25 - sibling-index() * 0.45) calc(0.6 + sign(sibling-index() - 5) * 0.4) 0;
  color: #7cb4ff;
}

round()

Stairs that climb by steps of 0.5.

@scene {
  cube.stair * 9;
}

.stair {
  --h: round(down, calc(sibling-index() * 0.3), 0.5);
  size: 0.4 calc(var(--h) + 0.1) 0.4;
  translate: calc(2.25 - sibling-index() * 0.45) calc(var(--h) / 2 + 0.05) 0;
  color: #ff5a36;
}

mod()

A grid: mod() gives the column of each copy.

@scene {
  cube.row * 12;
}

.row {
  size: 0.3;
  translate: calc(0.75 - mod(sibling-index() - 1, 4) * 0.5) 0.15 calc(round(down, calc((sibling-index() - 1) / 4)) * 0.5 - 0.5);
  color: #3ad16b;
}

rem()

A pattern that repeats every 3.

@scene {
  sphere.ball * 9;
}

.ball {
  radius: 0.18;
  translate: calc(2.25 - sibling-index() * 0.45) calc(0.3 + rem(sibling-index(), 3) * 0.4) 0;
  color: #ff5a36;
}

sign(), round(), mod() and rem() together

A grid of tiles, of three heights.

@scene {
  cube.tile * 16;
}

.tile {
  --col: mod(sibling-index() - 1, 4);
  --row: round(down, calc((sibling-index() - 1) / 4));
  size: 0.4 calc(0.2 + rem(sibling-index(), 3) * 0.2) 0.4;
  translate: calc(0.75 - var(--col) * 0.5) 0.2 calc(var(--row) * 0.5 - 0.75);
  rotate-y: calc(sign(var(--col) - 1.5) * -15deg);
  color: hsl(calc(sibling-index() * 22) 80% 60%);
}

sin(), cos(), tan()

The trigonometric functions of CSS: they take an angle (deg, rad, turn) or a number of radians, and return a number. cos() and sin() of the same angle give a point on a circle: the way to place objects in a ring, a spiral or a wave. pi is known too: cos(pi) is -1.

translate: cos(<angle>) sin(<angle>) 0;
Syntax
sin(<angle> | <number>)
Computed
at compile time, once per object

sin()

A wave: the height of each sphere follows sin().

@scene {
  sphere.wave * 9;
}

.wave {
  radius: 0.18;
  translate: calc(2 - sibling-index() * 0.4) calc(0.8 + sin(sibling-index() * 40deg) * 0.5) 0;
  color: #7cb4ff;
}

cos()

Columns whose heights follow cos().

@scene {
  cube.col * 9;
}

.col {
  --h: calc(0.9 + cos(sibling-index() * 40deg) * 0.6);
  size: 0.25 var(--h) 0.25;
  translate: calc(2 - sibling-index() * 0.4) calc(var(--h) / 2) 0;
  color: #ff5a36;
}

tan()

Steps that climb faster and faster with tan().

@scene {
  cube.step * 7;
}

.step {
  --h: calc(tan(sibling-index() * 10deg) * 1.2);
  size: 0.35 var(--h) 0.35;
  translate: calc(2 - sibling-index() * 0.5) calc(var(--h) / 2) 0;
  color: #3ad16b;
}

sin(), cos() and tan() together

A ring of beads that rises and falls.

@scene {
  sphere.bead * 12;
}

.bead {
  radius: calc(0.1 + tan(sibling-index() * 5deg) * 0.2);
  translate: calc(cos(sibling-index() * 30deg) * -1.5) calc(0.6 + sin(sibling-index() * 60deg) * 0.3) calc(sin(sibling-index() * 30deg) * 1.5);
  color: hsl(calc(sibling-index() * 30) 85% 60%);
}

Shapes and groups

capsule

A cylinder with round ends, standing on the y axis and centered on its origin: a radius of 0.25 and a height of 1 by default, round ends included.

Own properties
radius, height
Also
geometry properties

a capsule on the floor

@scene {
  capsule;
}

capsule {
  translate: 0 0.5 0;
}

cone

A cone standing on the y axis, centered on its origin, pointing up: a radius of 0.5 and a height of 1 by default. A second radius cuts its top.

Own properties
radius, height
Also
geometry properties

a cone on the floor

@scene {
  cone;
}

cone {
  translate: 0 0.5 0;
}

cube

A box with slightly rounded edges, centered on its origin: 1 × 1 × 1 by default. size stretches it into any box, and corner-radius rounds its edges.

Own properties
size, corner-radius
Also
geometry properties

a cube on the floor

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
}

cylinder

A cylinder standing on the y axis, centered on its origin: a radius of 0.5 and a height of 1 by default.

Own properties
radius, height
Also
geometry properties

a cylinder on the floor

@scene {
  cylinder;
}

cylinder {
  translate: 0 0.5 0;
}

group

Not a shape: it holds objects and other groups, like <g> in SVG, and draws nothing itself. Its translate, rotations and scale apply to everything inside it, and the positions of its children become relative to it.

Takes
display, visibility, cursor, mix-blend-mode, filter, translate, rotate-x, rotate-y, rotate-z, scale, transform-origin, animation, animation-duration, animation-delay, animation-iteration-count, animation-direction, animation-fill-mode, animation-timing-function, animation-timeline, animation-range, animation-range-start, animation-range-end, offset-path, offset-distance, offset-rotate

Its other properties (color, material, size…) are not passed down to its children: to style them, use a descendant selector, like #letters cube.

letters moved as one

The group places and turns its three cubes together.

@scene {
  group#letters {
    cube#l;
    cube#u;
    cube#c;
  }
}

#letters {
  translate: 1 0.5 0;
  rotate-y: -20deg;
}

#letters cube {
  size: 0.3 1 0.3;
  color: #ff5a36;
}

#u {
  translate: -1 0 0;
}

#c {
  translate: -2 0 0;
}

a group that turns

An animation on the group turns both spheres around its center.

@scene {
  group#spin {
    sphere#a;
    sphere#b;
  }
}

#spin {
  translate: 0 0.6 0;
  animation: turn 4s linear;
}

#a {
  translate: 0.8 0 0;
  radius: 0.4;
}

#b {
  translate: -0.8 0 0;
  radius: 0.4;
}

@keyframes turn {
  to {
    rotate-y: -1turn;
  }
}

lathe

A contour, filled, then turned around the vertical axis, like clay on a potter's wheel: a vase, a bowl, a bottle, a chess piece. A prism pushes its contour straight back into a flat plate; a lathe turns it into an object that is round from every side. The contour is a polygon() or a path() (see d), drawn right of the axis x = 0.

Own properties
d, view-box
Also
geometry properties

Draw half the outline: x is the distance from the axis, and y goes down, like SVG. A contour that does not touch the axis turns into a ring.

More details

A contour inside another one is a hole. The height is centered on the contours (or on the view-box), and y goes up in the scene, like a path. Seen from the front, a prism and a lathe of the same contour can look alike; turned, the prism is a cut-out as thick as its depth, and the lathe stays round.

a lathe and a prism of the same contour

Both turned by 60deg: the lathe, on the left, is a vase from every side; the prism is a flat plate.

@scene {
  lathe;
  prism;
}

lathe,
prism {
  d: path("M0 -1 H0.35 C0.35 -0.7 0.2 -0.6 0.2 -0.4 C0.2 0 0.75 0.3 0.6 0.7 C0.55 0.9 0.45 1 0 1 Z");
  rotate-y: 60deg;
  color: #c8643c;
}

lathe {
  translate: -0.9 1 0;
}

prism {
  translate: 0.9 1 0;
  depth: 0.3;
}

a vase

A path() of three curves, from the neck to the foot, closed along the axis.

@scene {
  lathe;
}

lathe {
  translate: 0 1 0;
  d: path("M0 -1 H0.35 C0.35 -0.7 0.2 -0.6 0.2 -0.4 C0.2 0 0.75 0.3 0.6 0.7 C0.55 0.9 0.45 1 0 1 Z");
  color: #c8643c;
}

a bowl

The contour goes up the outer wall and comes down the inner one: the bowl is hollow.

@scene {
  lathe;
}

lathe {
  translate: 0 0.4 0;
  d: path("M0 0.4 H0.35 C0.9 0.4 1 0 1 -0.4 H0.92 C0.92 0.05 0.75 0.3 0.3 0.3 H0 Z");
  color: #f2efe8;
}

a ring

A square away from the axis: once turned, a ring with a hole in its middle.

@scene {
  lathe;
}

lathe {
  translate: 0 0.15 0;
  d: polygon(0.6 -0.15, 1 -0.15, 1 0.15, 0.6 0.15);
  material: gold;
}

octahedron

Eight triangles, two pyramids joined by their bases, like a die with eight faces: a radius of 0.5 by default, from its center to each tip.

Own properties
radius
Also
geometry properties

a turning crystal

@scene {
  octahedron;
}

octahedron {
  translate: 0 1 0;
  radius: 0.7;
  material: glass(#bfe8ff, 1.5);
  animation: turn 8s linear infinite;
}

@keyframes turn {
  to {
    rotate-y: 360deg;
  }
}

path

A tube with round ends that follows an SVG path, given by d. stroke-width and view-box work like in SVG.

Own properties
d, stroke-width, view-box
Also
geometry properties

a curve

@scene {
  path;
}

path {
  translate: 0 1 0;
  d: path("M-1 0.5 C-1 -1 1 -1 1 0.5");
  stroke-width: 0.25;
  color: #ff5a36;
}

plane

A thin, flat rectangle lying in the xz plane, centered on its origin: 1 × 1 by default. size sets its width and its depth, and rotate-x: 90deg stands it up, like a wall.

Own properties
size
Also
geometry properties

a mat on the floor

@scene {
  plane;
}

plane {
  translate: 0 0.01 0;
  size: 3 2;
  color: #3ad16b;
}

prism

A contour, filled, then given a depth: a flat object, like a star, a letter, an arrow, a logo. The contour is a polygon() or a path() (see d), and a contour inside another one is a hole.

Own properties
d, depth, view-box
Also
geometry properties

It stands in the xy plane, facing the camera, centered on its contours (or on its view-box), like a path. To make a round object of a contour, like a vase or a bowl, turn it with a lathe instead.

a gold star

A star, from a polygon() of ten points.

@scene {
  prism;
}

prism {
  translate: 0 1 0;
  d: polygon(0 -1, -0.25 -0.34, -0.95 -0.31, -0.4 0.13, -0.59 0.81, 0 0.42, 0.59 0.81, 0.4 0.13, 0.95 -0.31, 0.25 -0.34);
  depth: 0.3;
  material: gold;
}

a square with a round hole

A path() with two contours: the circle inside the square is a hole.

@scene {
  prism;
}

prism {
  translate: 0 1 0;
  d: path("M-1 -1 H1 V1 H-1 Z M0 -0.6 A0.6 0.6 0 1 1 0 0.6 A0.6 0.6 0 1 1 0 -0.6 Z");
  depth: 0.4;
  color: #ff5a36;
}

pyramid

A pyramid on a square base, pointing up, centered on its origin: a base of 1 by 1 and a height of 1 by default. size sets the width and the depth of its base, and its height, like the box of a cube.

Own properties
size
Also
geometry properties

a pyramid on the floor

@scene {
  pyramid;
}

pyramid {
  translate: 0 0.5 0;
  rotate-y: 30deg;
  color: #e8c06a;
}

a tall pyramid

size: 1 2 1: the same base, twice as high.

@scene {
  pyramid;
}

pyramid {
  translate: 0 1 0;
  size: 1 2 1;
  rotate-y: 20deg;
}

sphere

A ball centered on its origin, with a radius of 0.5 by default.

Own properties
radius
Also
geometry properties

a sphere on the floor

@scene {
  sphere;
}

sphere {
  translate: 0 0.5 0;
}

torus

A ring lying flat around the y axis: a radius of 1 to the center of its tube, and a tube of 0.28 by default, set by thickness.

Own properties
radius, thickness
Also
geometry properties

a torus on the floor

@scene {
  torus;
}

torus {
  translate: 0 0.28 0;
}

tube

A hollow cylinder, a pipe, standing on the y axis and centered on its origin: an outer radius of 0.5, a height of 1 and a wall 0.1 thickness thick by default.

Own properties
radius, thickness, height
Also
geometry properties

Not a path: a path draws a tube along any line, while tube is a straight pipe, open at both ends, whose wall has a thickness.

More details

The wall goes inside the radius, so the tube keeps its outer size when the wall gets thicker.

a pipe

@scene {
  tube;
}

tube {
  translate: 0 0.5 0;
  rotate-x: 70deg;
  material: copper;
}

a ring, from a short tube

height: 0.2 and a thick wall make a flat ring.

@scene {
  tube;
}

tube {
  translate: 0 0.6 0;
  rotate-x: 80deg;
  radius: 0.6;
  height: 0.2;
  thickness: 0.25;
  color: #ff5a36;
}

Geometry

corner-radius

Rounds the edges of a cube; 0 keeps them sharp.

  1. corner-radius: 0;
  2. corner-radius: 0.08;
  3. corner-radius: 0.25;
Syntax
<number>
Initial value
0.08
Applies to
cube
Animatable
no

rounded edges

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
  corner-radius: 0.3;
}

d

The line a path follows, written like the d of an SVG path or CSS path(). On a prism, the contour to fill; on a lathe, the contour to turn around its axis: a polygon() or a path().

d: path("M 0 0 L 2 0 C 3.5 0 3 2 5 2 L 7 2");
Syntax
path("<svg path>") | polygon(<x> <y>, …) (prism, lathe)
Initial value
none (required)
Applies to
path, prism, lathe
Animatable
no

Commands

M
Moves, without drawing.
L, H, V
A line; a horizontal line; a vertical line.
C, S, Q, T
Curves, like SVG.
A
An arc of ellipse: rx ry rotation large-arc sweep x y, like SVG.
Z
Closes the path.
polygon()
On a prism or a lathe: one point per comma, x and y separated by a space, y going down, like CSS clip-path. It closes itself.

A path copied from an SVG keeps its way up. One path unit is one scene unit, so an icon drawn in a box of 24 usually needs a scale.

More details

Capitals are absolute, lowercase letters relative. y goes down in SVG and up in the scene: GSS flips a path. On a prism or a lathe, each subpath of a path() is a closed contour, filled with the even-odd rule: a contour inside another one is a hole, like the inside of an o. A lathe turns its contour around x = 0, so its x are never negative.

a curve

One cubic curve, C.

@scene {
  path;
}

path {
  translate: 0 1 0;
  d: path("M-1 0.5 C-1 -1 1 -1 1 0.5");
  stroke-width: 0.25;
  color: #ff5a36;
}

a zigzag, scaled down

Lines drawn 4 units wide, shown at half size by scale.

@scene {
  path;
}

path {
  translate: 0 1.2 0;
  d: path("M0 0 L1 1.5 L2 0 L3 1.5 L4 0");
  stroke-width: 0.3;
  scale: 0.5;
  material: gold;
}

a smiley

Two arcs draw the face, a third one the smile.

@scene {
  path;
}

path {
  translate: 0 1 0;
  d: path("M-1 0 A1 1 0 1 1 1 0 A1 1 0 1 1 -1 0 M-0.4 -0.3 A0.5 0.5 0 0 0 0.4 -0.3");
  stroke-width: 0.15;
  color: #ff5a36;
}

a thick line

One straight line, with a wide stroke-width.

@scene {
  path;
}

path {
  translate: 0 1 0;
  d: path("M-1 0 L1 0");
  stroke-width: 0.6;
  color: #3a7bff;
}

depth

The full thickness of a prism along the z axis, centered on its origin: depth: 1 goes from z = -0.5 to z = 0.5.

  1. prism
depth: …;
Syntax
<number>
Initial value
0.2
Applies to
prism
Animatable
no

a triangle, 1 deep

@scene {
  prism;
}

prism {
  translate: 0 1 0;
  d: polygon(0 -1, 1 1, -1 1);
  depth: 1;
  color: #ff5a36;
}

height

Sets the full height of a cylinder, a cone or a capsule, along the y axis. The shape is centered on its origin: to stand it on the floor, set its y to half its height.

  1. cylinder
  2. cone
  3. capsule
  4. tube
height: …;
Syntax
<number>
Initial value
1
Applies to
cylinder, cone, capsule, tube
Animatable
no

The height of a capsule counts its round ends, so it is at least twice its radius.

a tall cylinder

@scene {
  cylinder;
}

cylinder {
  translate: 0 1 0;
  radius: 0.4;
  height: 2;
}

a cone

@scene {
  cone;
}

cone {
  translate: 0 0.75 0;
  radius: 0.5 0.2;
  height: 1.5;
}

a capsule

1.5 high, round ends included.

@scene {
  capsule;
}

capsule {
  translate: 0 0.75 0;
  radius: 0.25;
  height: 1.5;
}

radius

Sets the radius of a sphere, a cylinder or a capsule, or that of a torus ring, measured to the center of its tube. On an octahedron, from its center to each tip; on a tube, its outer radius.

  1. sphere
  2. torus
  3. cylinder
  4. cone
  5. capsule
  6. octahedron
  7. tube
radius: …;
Syntax
<number> | <number> <number> (cone: bottom top)
Initial value
0.5 (sphere), 1 (torus), 0.5 (cylinder), 0.5 0 (cone), 0.25 (capsule), 0.5 (octahedron), 0.5 (tube)
Applies to
sphere, torus, cylinder, cone, capsule, octahedron, tube
Animatable
no

A cone takes a bottom and a top radius, like border-radius takes several values: the top one is 0 by default, a point, and a positive one cuts the top.

a big sphere

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  radius: 1;
}

a cut cone

radius: 0.5 0.2: a wide base, a narrow top.

@scene {
  cone;
}

cone {
  translate: 0 0.75 0;
  radius: 0.5 0.2;
  height: 1.5;
}

size

Sets the size of a cube along the x, y and z axes: one value makes a cube, three make a box. On a pyramid, the width and the depth of its base, and its height in the middle. On a plane, it sets the width and the depth: one value makes a square.

  1. cube
  2. plane
  3. pyramid
size: …;
Syntax
<number>{1,3} (cube, pyramid) | <number>{1,2} (plane: width depth)
Initial value
1 (cube), 1 (plane), 1 (pyramid)
Applies to
cube, plane, pyramid
Animatable
no

a box

Twice as wide as it is high.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
  size: 2 1 1;
}

a plane, stood up

A width and a depth, on a plane turned like a wall.

@scene {
  plane;
}

plane {
  translate: 0 1 0;
  rotate-x: 90deg;
  size: 2 1.5;
  color: #ff5a36;
}

stroke-width

The thickness of the tube along the path, in path units, like the stroke-width of SVG. Its ends are round.

  1. path
stroke-width: …;
Syntax
<number>
Initial value
1
Applies to
path
Animatable
no

a thick line

@scene {
  path;
}

path {
  translate: 0 1 0;
  d: path("M-1 0 L1 0");
  stroke-width: 0.6;
  color: #3a7bff;
}

thickness

Sets the radius of the tube of a torus, or the thickness of the wall of a tube, inside its radius.

  1. torus
  2. tube
thickness: …;
Syntax
<number>
Initial value
0.28 (torus), 0.1 (tube)
Applies to
torus, tube
Animatable
no

a thick ring

@scene {
  torus;
}

torus {
  translate: 0 0.5 0;
  thickness: 0.5;
}

view-box

The drawing area of a path: x, y, width and height, like the viewBox of SVG. Its center becomes the origin of the object.

view-box: 0 0 24 24;
Syntax
<number>{4}
Initial value
the box of the path itself
Applies to
path, prism, lathe
Animatable
no

Without it, each path is centered on itself; with the same view-box, the paths copied from one SVG keep their places relative to each other.

More details

On a lathe, only its y and its height count: the height is centered on them, and x = 0 stays the axis.

two braces of one icon

Both paths share the 32 × 32 box of their icon, and keep their places.

@scene {
  path#left;
  path#right;
}

path {
  translate: 0 1.6 0;
  view-box: 0 0 32 32;
  stroke-width: 2.4;
  scale: 0.1;
  color: #e6e6e6;
}

#left {
  d: path("M12.5 4.5C9.5 4.5 9 6 9 8.5v4c0 2-1 3.5-3.5 3.5C8 16 9 17.5 9 19.5v4c0 2.5.5 4 3.5 4");
}

#right {
  d: path("M19.5 4.5C22.5 4.5 23 6 23 8.5v4c0 2 1 3.5 3.5 3.5C24 16 23 17.5 23 19.5v4c0 2.5-.5 4-3.5 4");
}

Combinations

blend

Smooths the junction between the object and the objects declared before it, over the given distance. 0 keeps a sharp junction; it works with every operation.

blend: 0;blend: 0.4;
Syntax
<number>
Initial value
0
Applies to
objects
Animatable
no

two spheres that melt together

@scene {
  sphere#a;
  sphere#b;
}

#a {
  translate: 0.4 1 0;
}

#b {
  translate: -0.4 1 0;
  blend: 0.4;
}

operation

Sets how the object combines with the objects declared before it in @scene. The floor is never affected.

  1. operation: union;
  2. operation: subtract;
  3. operation: intersect;
Syntax
union | subtract | intersect
Initial value
union
Applies to
objects
Animatable
no

Values

union
Adds the object: the default.
subtract
Carves it out of them.
intersect
Keeps only their common part.

a sphere carved out of a cube

@scene {
  cube;
  sphere;
}

cube {
  translate: 0 1 0;
}

sphere {
  translate: 0 1 0;
  radius: 0.65;
  operation: subtract;
}

Colors

color

Sets the base color of the surface of the object: a hex color (#ff5a36), rgb(), hsl() or one of the 148 named colors of CSS (tomato). It can also be a gradient, a noise(), or one of them moved by displace().

color: #ff5a36;color: rgb(79 140 255);color: mediumseagreen;
Syntax
<color> | <gradient>
Initial value
#e6e6e6
Applies to
objects
Animatable
yes

Values

<color>
Any CSS color: the compiler turns it into a hex color.
<gradient>
linear-gradient(), radial-gradient() or conic-gradient(), painted on the object as seen from the front, and taken by every material.
noise()
A noise cut in the object's own space, like a block of stone.
transparent
A transparent color (#ff000080, rgb(255 0 0 / 50%)), or the transparent stops of a gradient, make the object transparent, like opacity.

A gradient can be animated and changed by :hover, into another gradient of the same kind with as many colors. A color cannot change into a gradient.

More details

Each number of an animated gradient moves on its own. Through a variable, one number is enough: linear-gradient(var(--angle), …) turns when @keyframes changes --angle. On a point light, color is the color of its light: a plain color only.

a hex color

@scene {
  sphere;
}

sphere {
  color: #ff5a36;
}

a named color and hsl()

tomato, and hsl(210 80% 60%).

@scene {
  sphere#a;
  sphere#b;
}

#a {
  translate: 0.8 0.5 0;
  color: tomato;
}

#b {
  translate: -0.8 0.5 0;
  color: hsl(210 80% 60%);
}

animated gradient

@keyframes turns the angle of the gradient through --angle.

@scene {
  plane;
}

plane {
  size: 4 3;
  translate: 0 0.05 0;
  --angle: 0deg;
  color: linear-gradient(var(--angle), #ff6540, #722cff);
  animation: spin 6s;
}

scene {
  camera-angle: 0deg 60deg;
}

@keyframes spin {
  to {
    --angle: 1turn;
  }
}

gradient on :hover

Under the mouse, the center of the gradient moves and its colors change.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
  color: radial-gradient(circle at 30% 70%, #ffd27a, #ff5a36);
  transition: 0.4s;
}

cube:hover {
  color: radial-gradient(circle at 70% 30%, #7ad2ff, #3a3aff);
}

a transparent color

A sphere at 40% alpha: the cube behind it shows through.

@scene {
  sphere;
  cube;
}

sphere {
  translate: 0 1 0;
  radius: 0.7;
  color: rgb(58 123 255 / 40%);
}

cube {
  translate: 0 0.4 -1.2;
  size: 0.8;
  color: #ff5a36;
}

a gradient that fades out

The cube fades out toward its bottom.

@scene {
  cube;
}

cube {
  translate: 0 0.7 0;
  size: 1.2;
  color: linear-gradient(#ff5a36, transparent);
}

background

Sets the background of the scene, seen wherever there is no object and no floor: a color, a gradient drawn over the canvas like a CSS background, a noise() or a displace().

background: noise(3, #fff0 40%, #fff), linear-gradient(#2f6bd8, #9fc4ff);
Syntax
[ <gradient> , ]* [ <gradient> | <color> ]
Initial value
#080808
Applies to
the scene
Animatable
yes

Like CSS, a background can have several layers, separated by commas, the first on top. Only the last layer can be a plain color.

More details

Two layers: noise(3, #ffffff00 40%, #ffffff), linear-gradient(#2f6bd8, #9fc4ff). Transparent colors let the layers below show through, and background-blend-mode blends them. With animation on the scene, a color changes into a color and a gradient into a gradient of the same kind, also through variables: without objects and floor, the scene is a moving image.

a color

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
}

scene {
  background: #42429f;
}

a gradient

A radial gradient, and no floor: the sphere floats in it.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  color: #ff5a36;
}

scene {
  floor: none;
  background: radial-gradient(circle at 50% 40%, #2a2a3a, #07070a);
}

layers: clouds over a sky

A transparent noise() over a blue gradient.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  material: chrome;
}

scene {
  floor: none;
  background: noise(2 4, #ffffff00 45%, #ffffff 80%), linear-gradient(#2f6bd8, #9fc4ff);
}

animated background

@keyframes moves the center of the gradient through --x.

@scene {
}

scene {
  floor: none;
  --x: 20%;
  background: radial-gradient(circle at var(--x) 40%, #ffb36b, #ff6540 30%, #722cff 70%, #171322);
  animation: drift 8s ease-in-out alternate;
  filter: grain(0.06);
}

@keyframes drift {
  to {
    --x: 80%;
  }
}

background-blend-mode

How each layer of background blends with what is below it, like CSS: one mode per layer, the first for the top layer. A shorter list repeats over the layers.

background-blend-mode: multiply;
Syntax
<blend-mode>#
Initial value
normal
Applies to
the scene
Animatable
no

Modes

normal
Covers what is below: the default.
multiply, screen
Darkens, where white changes nothing; lightens, where black changes nothing.
overlay, soft-light, hard-light
More contrast: multiplies the darks, screens the lights.
darken, lighten
The darker or the lighter of the two colors.
color-dodge, color-burn
Brightens or darkens what is below, by the color of the layer.
difference, exclusion
The difference of the two colors; softer with exclusion.
hue, saturation, color, luminosity
One part of the color of the layer, over the rest of the color below.

The transparency of a layer still applies: a transparent part blends nothing.

a grain over a gradient

A gray noise multiplied over a gradient, like a grain.

@scene {
}

scene {
  floor: none;
  background: noise(40 2, #808080, #ffffff), linear-gradient(135deg, #ff5a36, #3a7bff);
  background-blend-mode: multiply;
}

two noises, screened

Two noises over black, lightened together by screen.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  material: chrome;
}

scene {
  floor: none;
  background: noise(3 3, #000000 45%, #ff5a36), noise(2 4 seed 3, #000000 40%, #3a7bff), #000000;
  background-blend-mode: screen;
}

floor

Sets the color of the floor, an infinite plane at y = 0, lit like the objects. It can also be a gradient, a noise(), or one of them moved by displace(). none removes it: the objects float over the background, like the GSS logo.

floor: #e8e3db;floor: none;
Syntax
<color> | <gradient> | none
Initial value
#e8e3db
Applies to
the scene
Animatable
no

Values

<color>
Any opaque CSS color.
<gradient>
linear-gradient(), radial-gradient() or conic-gradient(), spread over a square of 40 units centered under the scene: everything the scene draws.
noise()
A noise read at each point of the floor, in the units of the scene: its scale is how many patterns fit in one unit.
none
No floor.

The floor takes one image, not layers, and opaque colors only. It is not animated, but the numbers of its image can be set from JavaScript.

More details

A gradient is seen from above, like on a plane: its top is away from the camera at rest, so linear-gradient(#0b1020, #273d62) goes from the horizon to the camera, and past the square it goes on with its first and last colors. A displace() moves the image by a share of that square: 0.02 moves it by up to 0.4 units. Reflections see it, and fog covers it like the rest of the scene.

a dark floor

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
}

scene {
  floor: #1a1a1f;
}

no floor

floor: none, over a dark background.

@scene {
  sphere;
}

sphere {
  color: #ff5a36;
  material: jelly(0.6);
}

scene {
  floor: none;
  background: #0a0a0c;
}

a noise() on the floor

Blue stone under a chrome sphere, which reflects it.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  material: chrome;
}

scene {
  floor: noise(2 3, #10182b, #273d62 55%, #0b1020);
  background: #0b1020;
}

a pool of light

A radial-gradient() that fades into the color of the background, around the scene.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
  color: #ff5a36;
}

scene {
  floor: radial-gradient(circle, #4a4f6a, #0b1020 12%);
  background: #0b1020;
}

mix-blend-mode

How the color of the object blends with what is behind it, like CSS: the objects behind it, the floor and the background. It takes the modes of background-blend-mode, and plus-lighter, which adds the colors. normal, the default, covers what is behind.

mix-blend-mode: screen;
Syntax
<blend-mode> | plus-lighter
Initial value
normal
Applies to
objects
Animatable
yes

Modes

normal
Covers what is behind: the default.
multiply, screen
Darkens, where white changes nothing; lightens, where black changes nothing.
overlay, soft-light, hard-light
More contrast: multiplies the darks, screens the lights.
darken, lighten
The darker or the lighter of the two colors.
color-dodge, color-burn
Brightens or darkens what is behind, by the color of the object.
difference, exclusion
The difference of the two colors; softer with exclusion.
hue, saturation, color, luminosity
One part of the color of the object, over the rest of the color behind it.
plus-lighter
Adds the two colors, up to white.

On a group, the mode goes into each object, so they blend with each other too: CSS blends the group as one picture. On :hover and in @keyframes, the mode switches halfway through.

More details

The eye sees through a blended object to what is behind it, like through a transparent one, and the color of the object, lit, blends with the color behind it. An object never blends with itself: it shows its nearest surface, over what is behind the whole object. Its opacity still applies: at 50%, half of the blend shows. On a group, an object with a mode of its own keeps it, and there is no isolation. The switch halfway through is the one of a discrete property of CSS. The reflections show a blended object with its own color, and the eye sees six surfaces behind one another at most.

three lights, screened

A red, a green and a blue sphere over black: where they meet, screen adds them up to white.

@scene {
  sphere#r;
  sphere#g;
  sphere#b;
}

scene {
  floor: none;
  background: #000000;
  ambient: 0.85;
}

sphere {
  radius: 0.75;
  mix-blend-mode: screen;
}

#r {
  translate: 0 1.45 0;
  color: #ff0000;
}

#g {
  translate: -0.45 0.75 0.2;
  color: #00ff00;
}

#b {
  translate: 0.45 0.75 0.4;
  color: #0000ff;
}

multiply over the floor

A sphere multiplied over a checkered floor and a cube: the squares and the cube show through it, tinted.

@scene {
  sphere;
  cube;
}

scene {
  floor: checker(0.5, #f4f1ea, #2a2a3a);
}

sphere {
  translate: 0 0.9 0.6;
  radius: 0.9;
  color: #ffcc33;
  mix-blend-mode: multiply;
}

cube {
  translate: 0.6 0.5 -0.8;
  size: 1;
  color: #3a7bff;
}

difference on :hover

The white cube turns into the negative of the gradient behind it under the mouse.

@scene {
  cube;
}

scene {
  floor: none;
  background: linear-gradient(90deg, #ff5a36, #3a7bff);
}

cube {
  translate: 0 0.5 0;
  size: 1.4;
  color: #ffffff;
  rotate-y: 30deg;
  transition: 0.3s;
}

cube:hover {
  mix-blend-mode: difference;
}

on a group

The mode of the group goes into each of its cubes: difference turns the stripes behind them inside out.

@scene {
  group#bars {
    cube * 3;
  }
}

scene {
  floor: none;
  ambient: 0.7;
  camera-distance: 4;
  background: repeating-linear-gradient(45deg, #ffcc33 0% 5%, #2a2a3a 5% 10%);
}

#bars {
  translate: 0 0.5 0;
  rotate-y: 20deg;
  mix-blend-mode: difference;
}

cube {
  size: 0.6 1.6 0.6;
  corner-radius: 0.08;
  translate: calc(sibling-index() * 0.9 - 1.8) 0 0;
  color: #ffffff;
}

Color functions

rgb()

A color from its red, green and blue channels, like CSS: numbers from 0 to 255, or percentages (100% is 255). Spaces or commas both work, and rgba() is the same function.

rgb(255 90 54)rgb(79 140 255)
Syntax
rgb(<red> <green> <blue>) | rgb(<red>, <green>, <blue>)
Computed
at compile time, once per object

An alpha (rgb(255 0 0 / 50%)) makes the color transparent, which only color, background and mask-image take: anywhere else, it is an error.

More details

Values outside the range are clamped. The math works inside, so one rule can give each copy its own color.

red, green and blue

@scene {
  sphere;
}

sphere {
  color: rgb(255 90 54);
}

more red for each copy

calc() and sibling-index() raise the red of each cube.

@scene {
  cube.step * 5;
}

.step {
  size: 0.4;
  translate: calc(1.8 - sibling-index() * 0.6) 0.3 0;
  color: rgb(calc(sibling-index() * 50) 90 200);
}

hsl()

A color from its hue, saturation and lightness, like CSS. The hue is an angle on the color wheel (0 red, 120 green, 240 blue), in degrees or in deg, rad or turn, and it goes round: -120 is 240.

hsl(<hue> 100% 60%)
Syntax
hsl(<hue> <saturation> <lightness>)
Computed
at compile time, once per object

Saturation and lightness are percentages, or numbers where 100 is 100%; a lightness of 50% gives the pure color. Commas, hsla() and the alpha work as in rgb().

More details

With sibling-index() on the hue, the copies of an object spread a rainbow.

an orange

@scene {
  sphere;
}

sphere {
  color: hsl(20 100% 60%);
}

a rainbow

Each dot turns the hue 45° further.

@scene {
  sphere.dot * 8;
}

.dot {
  radius: 0.25;
  translate: calc(2.7 - sibling-index() * 0.6) 0.5 0;
  color: hsl(calc(sibling-index() * 45) 90% 60%);
}

hwb()

A color from a hue, and how much white and black are mixed into it, like CSS: hwb(0 0% 0%) is pure red; more whiteness makes it paler, more blackness darker. When whiteness and blackness add up to 100% or more, the color is a gray. The hue works as in hsl().

  1. hwb(15 0% 0%)
  2. hwb(15 50% 0%)
  3. hwb(15 0% 50%)
Syntax
hwb(<hue> <whiteness> <blackness>)
Computed
at compile time, once per object

paler and paler

Each cube adds 15% of white.

@scene {
  cube.tint * 5;
}

.tint {
  size: 0.6;
  translate: calc(2.4 - sibling-index() * 0.8) 0.3 0;
  color: hwb(200 calc(sibling-index() * 15%) 10%);
}

lab(), lch()

The CIE Lab color space of CSS, built on how the eye sees: the lightness goes from 0 (black) to 100 (white), a from green to red, b from blue to yellow. lch() is the same space, written with a chroma (how colorful) and a hue.

lab(65 a b)lch(65 60 <hue>)
Syntax
lab(<lightness> <a> <b>) | lch(<lightness> <chroma> <hue>)
Computed
at compile time, once per object

Percentages work as in CSS: 100% is 100 for the lightness, 125 for a and b, 150 for the chroma. A color the screen cannot show is clipped to it.

lab()

An orange, in Lab.

@scene {
  sphere;
}

sphere {
  color: lab(62 52 48);
}

lch()

Six hues of the same lightness and chroma.

@scene {
  sphere.dot * 6;
}

.dot {
  radius: 0.3;
  translate: calc(2.6 - sibling-index() * 0.75) 0.5 0;
  color: lch(65 60 calc(sibling-index() * 60));
}

lab() and lch() together

A green in lab(), and one in lch().

@scene {
  cube#a;
  cube#b;
}

#a {
  translate: 0.7 0.5 0;
  color: lab(55 -40 30);
}

#b {
  translate: -0.7 0.5 0;
  color: lch(55 50 140);
}

oklab(), oklch()

The OKLab color space of CSS: its lightness matches the eye much better than hsl(), so colors of the same lightness look equally light, whatever their hue. With sibling-index() on its hue, oklch() gives a rainbow whose colors all look as light.

oklch(72% 0.15 <hue>)
Syntax
oklab(<lightness> <a> <b>) | oklch(<lightness> <chroma> <hue>)
Computed
at compile time, once per object

The lightness goes from 0 to 1, or 0% to 100%. oklch() adds a chroma, from about 0 to 0.4 (100% is 0.4), and a hue. A color the screen cannot show is clipped to it.

oklab()

A warm color, in OKLab.

@scene {
  sphere;
}

sphere {
  color: oklab(0.72 0.12 0.1);
}

oklch()

A rainbow whose colors look equally light.

@scene {
  sphere.dot * 8;
}

.dot {
  radius: 0.25;
  translate: calc(2.7 - sibling-index() * 0.6) 0.5 0;
  color: oklch(72% 0.15 calc(sibling-index() * 45));
}

oklab() and oklch() together

A blue in oklab(), and a red in oklch().

@scene {
  cube#a;
  cube#b;
}

#a {
  translate: 0.7 0.5 0;
  color: oklab(60% -0.1 -0.1);
}

#b {
  translate: -0.7 0.5 0;
  color: oklch(60% 0.14 30deg);
}

color()

A color in a named color space, like CSS, with three channels from 0 to 1, or percentages.

color(<space> 0.95 0.35 0.2)
Syntax
color(<space> <r> <g> <b>)
Computed
at compile time, once per object

Color spaces

srgb
The space of hex colors.
srgb-linear
The same colors, without the gamma curve: the channels are amounts of light.
display-p3
A wider gamut. What the sRGB render cannot show is clipped.
xyz, xyz-d65, xyz-d50
The CIE XYZ space, with a D65 white (xyz is xyz-d65) or a D50 white.

display-p3 and srgb-linear

@scene {
  sphere#a;
  sphere#b;
}

#a {
  translate: 0.8 0.6 0;
  color: color(display-p3 0.95 0.35 0.2);
}

#b {
  translate: -0.8 0.6 0;
  color: color(srgb-linear 0.1 0.3 0.8);
}

color-mix()

Mixes two colors in a color space, like CSS. Without percentages, half of each; with one, the other color takes the rest; with both, they are scaled to add up to 100%.

color-mix(in oklab, #ff5a36 50%, #4f8cff)
Syntax
color-mix(in <space> [shorter | longer hue]?, <color> <percentage>?, <color> <percentage>?)
Computed
at compile time, once per object

Color spaces

oklab
The most even mixes.
srgb
The mix of hex colors: duller middle colors.
srgb-linear
A mix of the light itself: brighter middle colors.
lab
Like oklab, in CIE Lab.
oklch, lch, hsl, hwb
Spaces with a hue: it goes the shorter way round the wheel, or the longer one with longer hue. A gray takes the hue of the other color.

Like CSS, when the percentages add up to less than 100%, the color becomes transparent, which only color, background and mask-image take: anywhere else, it is an error.

More details

A percentage set from JavaScript is kept between 0% and 100%, and percentages that add up to less are scaled up to 100%.

from orange to blue

Each cube takes 20% more blue, mixed in oklab.

@scene {
  cube.step * 6;
}

.step {
  size: 0.6;
  translate: calc(2.6 - sibling-index() * 0.75) 0.3 0;
  color: color-mix(in oklab, #ff5a36, #3a7bff calc(sibling-index() * 20% - 20%));
}

light-dark()

The first color in light mode, the second in dark mode, like CSS. The scene follows the system as it changes: it works like @media (prefers-color-scheme: dark), and counts as one of the queries of the scene.

  1. light mode
  2. dark mode
color: light-dark(#9fc4ff, #ffb86b);
Syntax
light-dark(<light color>, <dark color>)
Computed
at compile time, once per object

light and dark mode

@scene {
  sphere;
}

scene {
  background: light-dark(#f4f1ea, #0b0b10);
  floor: light-dark(#e8e3db, #16161d);
}

sphere {
  color: light-dark(#ff5a36, #7cb4ff);
}

contrast-color()

White or black, whichever contrasts most with the color, like CSS: the way to keep an object readable against a background held in a variable.

  1. contrast-color(#f2efe9)
  2. contrast-color(#1b2a4a)
Syntax
contrast-color(<color>)
Computed
at compile time, once per object

readable on any background

The sphere is white or black, whatever --bg holds.

@scene {
  cube;
  sphere;
}

scene {
  --bg: #3a7bff;
  background: var(--bg);
}

cube {
  translate: 0 0.5 0;
  color: var(--bg);
}

sphere {
  radius: 0.3;
  translate: 0 1.3 0;
  color: contrast-color(var(--bg));
}

currentColor

A keyword, like CSS: the object's own color, wherever a color is expected. color-mix(in oklab, currentColor 60%, white) is a lighter version of whatever color the object has, so one rule can tint many objects.

outline-color: currentColor;
Syntax
currentColor
Computed
at compile time, once per object

It works in a material, in the stops of a gradient, in light-dark() and in a variable. color: currentColor is an error: a group passes no color down.

More details

As the color of a material, metal(currentColor, 0.2) is metal(0.2): the material follows the color, animated, on :hover, or a gradient. In a variable, it is read with the color of the object that uses the variable. An object without color uses the initial #e6e6e6. It can be written currentcolor too.

a lighter shell for every color

Three spheres, each in a jelly lighter than its own color.

@scene {
  sphere * 3;
}

sphere {
  radius: 0.4;
  translate: calc((sibling-index() - 2) * -1.1) 0.5 0;
  material: jelly(color-mix(in oklab, currentColor 55%, white), 0.4);
}

sphere:nth-child(1) {
  color: #ff5a36;
}

sphere:nth-child(2) {
  color: #3ad16b;
}

sphere:nth-child(3) {
  color: #3a7bff;
}

in a variable

One gradient in a variable, read with the color of each cube.

@scene {
  cube#a;
  cube#b;
}

scene {
  --shade: linear-gradient(currentColor, color-mix(in srgb, currentColor, black 60%));
}

cube {
  size: 0.8;
  material: metal(var(--shade), 0.3);
}

#a {
  translate: 0.7 0.4 0;
  color: #ff5a36;
}

#b {
  translate: -0.7 0.4 0;
  color: #3a7bff;
}

Gradients and noise

<gradient>

The gradients of CSS, for the background of the scene, the color of an object, the color of a material and the floor. Each color can have one or two positions; the missing ones are spread like CSS, and two colors at the same place make a hard edge.

  1. linear-gradient()
  2. radial-gradient()
  3. conic-gradient()
Syntax
linear-gradient([<angle> | to <side> <side>?]?, <color> <percentage>{0,2}, …) | radial-gradient([circle | ellipse]? [closest-side | farthest-side | closest-corner | farthest-corner]? [at <position>]?, <color> <percentage>{0,2}, …) | conic-gradient([from <angle>]? [at <position>]?, <color> [<angle> | <percentage>]{0,2}, …)
Computed
on the GPU, at each pixel

Functions

linear-gradient()
Along a line, to bottom by default. It takes an angle (0deg up, 90deg right), or to a side or a corner.
radial-gradient()
An ellipse that reaches the farthest corner by default. It takes circle or ellipse, a size keyword and a position: at 30% 40%, at top.
conic-gradient()
Around a center, clockwise from the top: from 90deg starts a quarter turn later, and at 30% 40% moves the center. Its colors take angles or percentages: a color wheel, a pie chart, the sweep of a hand.
repeating-linear-gradient()
Repeats the stops; also repeating-radial-gradient() and repeating-conic-gradient().

On an object, the gradient covers it as seen from the front, whatever its size, and moves and turns with it. In the background, it covers the canvas.

More details

In the background, the gradient follows the size of the canvas; reflections and glass see it in the direction they look. On an object, it goes from left to right and from bottom to top (a plane is seen from above): to top goes from its bottom to its top. On the floor, it covers a square of 40 units centered under the scene, seen from above. Colors are mixed in sRGB, like CSS with hex colors.

linear-gradient()

A diagonal background, to top right.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
  color: #f4f1ea;
}

scene {
  floor: none;
  background: linear-gradient(to top right, #ff5a36, #3a7bff);
}

radial-gradient()

A circle closest-side behind a glass sphere.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  material: glass;
}

scene {
  floor: none;
  background: radial-gradient(circle closest-side, #ffd27a, #ff5a36 60%, #1a0f2e);
}

linear-gradient() on objects

A gradient on a cylinder and on a cube, and one in the material of a sphere.

@scene {
  cylinder;
  sphere;
  cube;
}

cylinder {
  radius: 0.4;
  height: 2;
  translate: 1.4 1 0;
  color: linear-gradient(#ff5a36, #ffd27a);
}

sphere {
  radius: 0.7;
  translate: 0 0.7 0;
  material: metal(radial-gradient(circle at 35% 65%, #ffffff, #3a7bff 40%, #10183a), 0.15);
}

cube {
  size: 1;
  translate: -1.4 0.5 0;
  rotate-y: -30deg;
  color: repeating-linear-gradient(45deg, #3ad16b 0% 10%, #f4f1ea 10% 20%);
}

conic-gradient()

A color wheel on a dial, stripes in a metal, and a conic background.

@scene {
  cube#dial;
  sphere;
}

#dial {
  size: 2 2 0.1;
  corner-radius: 0.05;
  translate: 0.4 1.2 0;
  color: conic-gradient(#ff5a36, #ffd27a, #3ad16b, #3a7bff, #b15aff, #ff5a36);
}

sphere {
  radius: 0.5;
  translate: -1.4 0.5 0.6;
  material: metal(repeating-conic-gradient(from 45deg, #f4f1ea 0deg 30deg, #111111 30deg 60deg), 0.3);
}

scene {
  floor: none;
  background: conic-gradient(from 180deg at 50% 0%, #1c1c24, #2a2a3a, #1c1c24);
}

repeating-linear-gradient() and repeating-radial-gradient()

Stripes behind two chrome spheres.

@scene {
  sphere#a;
  sphere#b;
}

sphere {
  radius: 0.6;
  translate: 0 1 0;
  material: chrome;
}

#a {
  translate: 0.8 1 0;
}

#b {
  translate: -0.8 1 0;
}

scene {
  floor: none;
  background: repeating-linear-gradient(45deg, #111 0% 5%, #2a2a3a 5% 10%);
}

checker()

A checkerboard, wherever a gradient goes: the color of an object, the color of a material, the background, the floor. Like noise(), it is cut in the object's own space: a grid of cubes, one color and then the other.

  1. checker()
  2. color
color: checker(4, #111, #eee);
Syntax
checker(<number> [at <number> <number> <number>]?, <color>, <color>)
Computed
on the GPU, at each pixel

Arguments

<number>
The scale: how many cells fit in one unit.
at <x> <y> <z>
Moves the grid.
<color>, <color>
The two colors of the cells.

On an object, the cells keep their size whatever the shape, and move and turn with it. The edges of the cells are sharp: far away, a fine checkerboard shimmers.

More details

On an object, the grid is cut in its own space, like a block of stone carved out of a checkered material. On the floor, it is read in the units of the scene; in the background, it follows the direction of the view. Like noise(), it can be animated into another checker(), its numbers can be set from JavaScript, and it can be the map of displace().

a checkered floor

One cell per unit of the scene.

@scene {
  sphere;
}

scene {
  floor: checker(1, #1a1d2b, #e8e6e1);
}

sphere {
  translate: 0 0.6 0;
  radius: 0.6;
  material: chrome;
}

a checkered cube

@scene {
  cube;
}

cube {
  translate: 0 0.6 0;
  size: 1.2;
  rotate-y: 30deg;
  color: checker(2.5, #ff5a36, #f4f1ea);
}

displace()

Moves an image by another one, like SVG feDisplacementMap, wherever a gradient goes: color, a material, background and its layers, mask-image, floor. Stripes moved by a turbulence make marble, rings make wood, a mask gets ragged edges.

  1. an image
  2. a map
  3. displace()
color: displace(stripes(4, #111, #eee), noise(2, black, white), 0.3);
Syntax
displace(<gradient> | <noise()>, <gradient> | <noise()>, <number> | <percentage>)
Computed
on the GPU, at each pixel

Arguments

<image>
The gradient or the noise() to move.
<map>
Usually a noise(): its colors move each point where the image is read. On a gradient, red moves it right and green down, like SVG; on a noise(), red, green and blue move it in 3D. A channel at 50% moves nothing, 0% and 100% the most.
<amount>
A share of the size of the image: 0.3 or 30% moves it by up to 15% of its size.

It can be animated, and changed by :hover, into another displace() whose image and map are of the same kinds. Animating the at of a noise map makes the image flow.

More details

A noise() map gives each channel a noise of its own, like feTurbulence, so even a gray noise moves the image in every direction; a gradient map is read once, by its colors, which are opaque. Its numbers can be set from JavaScript.

marble

Stripes moved by a turbulence.

@scene {
  cube;
}

cube {
  translate: 0 0.6 0;
  size: 1.2;
  corner-radius: 0.06;
  color: displace(repeating-linear-gradient(45deg, #f4f1ea 0% 9%, #9a9385 10%, #f4f1ea 11%), noise(turbulence 1 4, black, white), 0.35);
}

a ragged hole, in mask-image

A round hole with ragged edges.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  radius: 0.8;
  color: #ff5a36;
  mask-image: displace(radial-gradient(circle, transparent 30%, black 31%), noise(4 3, black, white), 0.15);
}

water that flows

Stripes that flow as @keyframes moves the at of their map.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  material: chrome;
}

scene {
  floor: none;
  background: displace(repeating-linear-gradient(#123a6b 0% 3%, #3a7bff 5% 8%), noise(2 3, black, white), 0.1);
  animation: flow 10s linear infinite;
}

@keyframes flow {
  to {
    background: displace(repeating-linear-gradient(#123a6b 0% 3%, #3a7bff 5% 8%), noise(2 3 at 0 1 0, black, white), 0.1);
  }
}

noise()

A noise image, wherever a gradient goes: the background, the color of an object, the color of a material, the floor. Its colors are placed like the stops of a gradient, along the value of a smooth 3D noise, like SVG feTurbulence.

  1. noise()
  2. color
color: noise(3, #1b2a4a, #9fc4ff);
Syntax
noise([turbulence]? <number> <integer>? [seed <integer>]? [at <number> <number> <number>]?, <color> <percentage>{0,2}, …)
Computed
on the GPU, at each pixel

Arguments

<number>
The scale: how many patterns fit in one unit.
<integer>
The octaves, from 1 to 8, if any: each one adds detail twice as fine, like numOctaves.
turbulence
Sharp creases instead of soft clouds: marble, fire, lightning.
seed <integer>
Another pattern.
at <x> <y> <z>
Moves the pattern.

On an object, the noise has no seam, and moves and turns with it. Animating at makes it drift, like clouds or smoke.

More details

On an object, the noise is cut in the object's own space, like a block of stone. On the floor, it is read at each point of the floor, in the units of the scene. In the background, it follows the direction of the view. Like a gradient, it can be animated and changed by :hover, into another noise() of the same kind with as many colors. Its numbers can be set from JavaScript.

noise() on an object

Blue clouds on a sphere.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  radius: 0.8;
  color: noise(3 4, #1a1d2b, #3a7bff 55%, #ffffff);
}

turbulence: marble

Sharp creases, like the veins of marble.

@scene {
  cube;
}

cube {
  translate: 0 0.6 0;
  size: 1.2;
  corner-radius: 0.06;
  color: noise(turbulence 1.2 5, #4a4f5a, #f4f1ea 30%);
}

a sky that drifts

A background noise that drifts as @keyframes moves its at.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  material: chrome;
}

scene {
  floor: none;
  background: noise(2 4, #2f6bd8 40%, #ffffff 75%);
  animation: wind 20s linear infinite;
}

@keyframes wind {
  to {
    background: noise(2 4 at 1 0 0, #2f6bd8 40%, #ffffff 75%);
  }
}

noise() in a material

A turbulence in the color of a metal.

@scene {
  torus;
}

torus {
  translate: 0 0.6 0;
  rotate-x: 70deg;
  material: metal(noise(turbulence 6 3, #6b4a2b, #d4af37), 0.25);
}

stripes()

Bands of two colors across an axis, wherever a gradient goes. Like checker(), they are cut in the object's own space: across y by default, like the rings of a column.

  1. stripes()
  2. color
color: stripes(4, #111, #eee);
Syntax
stripes(<number> [x | y | z]? [at <number> <number> <number>]?, <color>, <color>)
Computed
on the GPU, at each pixel

Arguments

<number>
The scale: how many pairs of bands fit in one unit.
x, y, z
The axis the bands follow one another along: y by default, horizontal bands.
at <x> <y> <z>
Moves the bands: animated, they scroll.
<color>, <color>
The two colors, each half of a pair.

stripes() goes all around the object, in its own space. For bands seen from the front only, at any angle, use repeating-linear-gradient().

More details

Like noise(), it can be animated into another stripes() of the same axis, its numbers can be set from JavaScript, and it can be the map of displace(). The edges are sharp.

a striped column

@scene {
  cylinder;
}

cylinder {
  translate: 0 0.8 0;
  radius: 0.4;
  height: 1.6;
  color: stripes(3, #ff5a36, #f4f1ea);
}

bands that scroll

at animated: the bands move up the capsule.

@scene {
  capsule;
}

capsule {
  translate: 0 0.8 0;
  radius: 0.4;
  height: 1.6;
  color: stripes(4, #3a7bff, #e8e6e1);
  animation: rise 2s linear infinite;
}

@keyframes rise {
  to {
    color: stripes(4 at 0 -0.25 0, #3a7bff, #e8e6e1);
  }
}

Materials

material

Sets how the surface of the object reacts to light. Without a color, a material uses the color property, like currentColor in CSS, so the color stays animatable; with its own, like brass or metal(#3a7bff), it keeps it, even when color is a gradient. The color can also be a gradient: metal(linear-gradient(#ffd27a, #ff5a36), 0.2).

  1. matte()
  2. metal()
  3. glass()
  4. jelly()
  5. emissive()
  6. iridescent()
Syntax
matte([<color>]) | metal([<color>,] [<roughness>]) | jelly([<color>,] [<density>]) | emissive([<color>,] [<strength>]) | iridescent([<color>,] [<strength>]) | gold | chrome | copper | silver | brass | aluminum | jelly | glass([<color>,] [<refraction-index>] [, [frosted | wavy | hammered | blurred] <frost>]) | glass | ice | emissive | iridescent
Initial value
matte()
Applies to
objects
Animatable
no

Values

matte()
Scatters the light only, like chalk: the default.
metal()
Reflects the scene. Its roughness goes from 0, a mirror, to 1, brushed metal (0.2 by default).
jelly()
Lets the light through its thin parts, like a gummy candy. Its density goes from 0, clear, to 1, deep (0.5 by default).
glass()
See-through, bent by its refraction index, from 1 to 3 (1.5 by default: water is 1.33, diamond 2.4), then a frost from 0 to 1.
emissive()
Gives its own light: a lamp, a screen, a neon. It is never darker than its color times its strength, from 0, none, to 4 (1 by default: its full color, even in the shade). It does not light the objects around it; with bloom() in the filter of the scene, it glows.
iridescent()
A thin film, like a soap bubble or the back of a CD: its colors turn with the angle you see it at, most at the edges. Its strength goes from 0, none, to 1 (0.7 by default).
frosted, wavy, hammered, blurred
The style of the frost: white patches (the default), big waves, small bumps, a soft blur.
gold, chrome
metal(#d4af37, 0.2) and metal(#ffffff, 0.05).
copper, silver, brass, aluminum
metal(#c8784a, 0.25), metal(#e3e4e6, 0.1), metal(#c9a24d, 0.2) and metal(#c4c8cc, 0.35): the metals of everyday objects.
jelly, glass, ice
jelly(), glass(), and glass(#cfeaff, 1.31, frosted 0.25).
emissive, iridescent
emissive() and iridescent(), with the color of color.

matte()

The default: a soft, even surface.

@scene {
  sphere;
}

sphere {
  color: #ff5a36;
  material: matte();
}

metal()

gold, chrome, and a rougher metal(0.7) that takes the color.

@scene {
  sphere#a;
  sphere#b;
  sphere#c;
}

#a {
  translate: 1.3 0.6 0;
  radius: 0.6;
  material: gold;
}

#b {
  translate: 0 0.6 0;
  radius: 0.6;
  material: chrome;
}

#c {
  translate: -1.3 0.6 0;
  radius: 0.6;
  color: #d4af37;
  material: metal(0.7);
}

everyday metals

copper, silver, brass and aluminum, from left to right.

@scene {
  sphere#a;
  sphere#b;
  sphere#c;
  sphere#d;
}

sphere {
  radius: 0.45;
}

#a {
  translate: -1.5 0.5 0;
  material: copper;
}

#b {
  translate: -0.5 0.5 0;
  material: silver;
}

#c {
  translate: 0.5 0.5 0;
  material: brass;
}

#d {
  translate: 1.5 0.5 0;
  material: aluminum;
}

its own color

brass keeps its color over the stripes of color; metal(0.2), without a color of its own, takes them.

@scene {
  sphere#a;
  sphere#b;
}

sphere {
  radius: 0.6;
  color: stripes(3, #ff5a36, #f4f1ea);
}

#a {
  translate: -0.8 0.6 0;
  material: brass;
}

#b {
  translate: 0.8 0.6 0;
  material: metal(0.2);
}

jelly()

jelly, and a clearer jelly(0.3).

@scene {
  sphere;
  cube;
}

sphere {
  translate: 0.8 0.6 0;
  radius: 0.6;
  color: #ff5a36;
  material: jelly;
}

cube {
  translate: -0.8 0.5 0;
  rotate-y: -30deg;
  color: #3ad16b;
  material: jelly(0.3);
}

emissive()

A glowing sphere and a dim one, with bloom() on the scene.

@scene {
  sphere#lamp;
  sphere#dim;
}

scene {
  background: #07080c;
  ambient: 0.25;
  filter: bloom(0.7, 10px);
}

#lamp {
  translate: -0.8 0.6 0;
  radius: 0.55;
  material: emissive(#ff5a36, 2);
}

#dim {
  translate: 0.8 0.6 0;
  radius: 0.55;
  material: emissive(#3a7bff, 0.5);
}

iridescent()

A dark sphere and a pale torus: their colors turn as they spin.

@scene {
  sphere;
  torus;
}

sphere {
  translate: -0.8 0.7 0;
  radius: 0.6;
  color: #15161c;
  material: iridescent(0.9);
}

torus {
  translate: 0.9 0.7 0;
  radius: 0.45;
  thickness: 0.18;
  rotate-x: 70deg;
  color: #f4f1ea;
  material: iridescent;
  animation: spin 6s linear infinite;
}

@keyframes spin {
  to {
    rotate-y: 360deg;
  }
}

glass()

Clear glass.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  radius: 0.6;
  material: glass;
}

glass(), with its arguments

A white glass, of index 1.5 and frost 0.3.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  radius: 0.6;
  material: glass(#ffffff, 1.5, 0.3);
}

ice

glass(#cfeaff, 1.31, frosted 0.25).

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  radius: 0.6;
  material: ice;
}

glass, with a cube behind

The sphere bends the view of the cube behind it.

@scene {
  sphere;
  cube;
}

sphere {
  translate: 0 0.7 0;
  radius: 0.6;
  material: glass;
}

cube {
  translate: -0.3 0.5 -1.5;
  color: #ff5a36;
}

glass() with frost and style

A wavy and a blurred frost, side by side.

@scene {
  sphere#a;
  sphere#b;
  cube;
}

#a {
  translate: 0.7 0.7 0;
  radius: 0.6;
  material: glass(1.5, wavy 0.6);
}

#b {
  translate: -0.7 0.7 0;
  radius: 0.6;
  material: glass(1.5, blurred 0.6);
}

cube {
  translate: 0 0.5 -1.8;
  color: #ff5a36;
}

Textures

element()

A live image of an HTML element of the page, for texture, like CSS element(): a card, a form, a chart, any interface on a 3D surface. The browser draws the element with the CSS and the fonts of the page, and the object shows it again each time it changes.

  1. <div id="card">
  2. element(#card)
texture: element(#card);
Syntax
element(<id>)
Computed
by the browser, each time the element changes

Put the element inside <gss-scene>, next to its script, or inside the canvas given to mount(). Screen readers still read it, but it is seen only on the object.

More details

One image covers each face, like any texture: give the element the proportions of the face, and use ::face(front) for one face only. A <canvas> inside the element is captured with it, frame after frame while the scene draws: a chart, a game, another shader. A scene with an element is drawn with WebGL2.

an HTML card

A card of HTML that sways on the front of a thin cube.

@scene {
  cube;
}

scene {
  floor: none;
  camera-angle: -20deg 10deg;
  camera-target: 0 0.8 0;
  camera-distance: 3.2;
  ambient: 0.55;
}

cube {
  translate: 0 0.8 0;
  size: 1.6 1 0.06;
  corner-radius: 0.03;
  animation: sway 6s ease-in-out infinite alternate;
}

cube::face(front) {
  texture: element(#card);
}

@keyframes sway {
  to {
    rotate-y: 25deg;
  }
}
<article id="card" style="
  width: 320px; height: 200px; padding: 28px;
  box-sizing: border-box; border-radius: 18px;
  background: #f4f1ea; color: #1a1d2b;
  font: 600 30px/1.2 system-ui, sans-serif;">
  Hello from <em style="color: #ff5a36;">HTML</em>, on a 3D card
</article>

a canvas, live

A <canvas> and its one-line WebGL shader inside the element: the face follows it, frame by frame. The script rides an onerror attribute, so it runs even where a page inserts the HTML with innerHTML, as the playground does; in a page of yours, a plain <script> works too.

@scene {
  cube;
}

scene {
  floor: none;
  background: radial-gradient(#1b1f3a, #0a0c18);
  camera-angle: -18deg 8deg;
  camera-target: 0 0.8 0;
  camera-distance: 3.4;
  ambient: 0.6;
}

cube {
  translate: 0 0.8 0;
  size: 1.92 1.2 0.06;
  corner-radius: 0.03;
  animation: sway 7s ease-in-out infinite alternate;
}

cube::face(front) {
  texture: element(#screen);
}

@keyframes sway {
  from {
    rotate-y: -18deg;
  }
  to {
    rotate-y: 18deg;
  }
}
<div id="screen" style="position: relative; width: 480px; height: 300px; border-radius: 20px; overflow: hidden; background: #0a0c18; font: 600 13px/1 system-ui, sans-serif;">
<canvas width="480" height="300" style="position: absolute; inset: 0;"></canvas>
<span style="position: absolute; left: 16px; top: 14px; color: #fff; letter-spacing: .12em; text-transform: uppercase;">a shader, in a shader</span>
<img src="data:," hidden onerror="this.remove();var c=this.parentNode.firstElementChild;var g=c.getContext('webgl',{preserveDrawingBuffer:true});function s(t,x){var h=g.createShader(t);g.shaderSource(h,x);g.compileShader(h);return h}var p=g.createProgram();g.attachShader(p,s(g.VERTEX_SHADER,'attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}'));g.attachShader(p,s(g.FRAGMENT_SHADER,'precision highp float;uniform float t;uniform vec2 r;void main(){vec2 u=(gl_FragCoord.xy-.5*r)/r.y;gl_FragColor=vec4(.5+.5*cos(t+10.*length(u)-vec3(0.,2.,4.)),1.);}'));g.linkProgram(p);g.useProgram(p);var b=g.createBuffer();g.bindBuffer(g.ARRAY_BUFFER,b);g.bufferData(g.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),g.STATIC_DRAW);var a=g.getAttribLocation(p,'p');g.enableVertexAttribArray(a);g.vertexAttribPointer(a,2,g.FLOAT,false,0,0);var T=g.getUniformLocation(p,'t'),R=g.getUniformLocation(p,'r');cancelAnimationFrame(window.__gssShader);(function f(ms){window.__gssShader=requestAnimationFrame(f);g.uniform1f(T,ms/1000);g.uniform2f(R,c.width,c.height);g.drawArrays(g.TRIANGLES,0,3)})(0)">
</div>

image-rendering

How the image of texture is drawn, like CSS.

  1. image-rendering: smooth;
  2. image-rendering: pixelated;
Syntax
auto | smooth | pixelated | crisp-edges
Initial value
auto
Applies to
objects
Animatable
no

Values

auto, smooth
Blends the pixels, for photos and painted textures. auto is the default.
pixelated
Reads the nearest pixel: each pixel stays a sharp square, the look of pixel art.
crisp-edges
The same as pixelated.

a pixel-art block

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
  texture: url("/textures/dirt.png");
  image-rendering: pixelated;
}

paint()

The texture a @paint draws, for texture, like CSS paint(), whose image comes from code: texture: paint(rings);. WebGL2 only, for now.

  1. @paint rings { … }
  2. 512 × 512 px
  3. texture: paint(rings);
Syntax
paint(<name>)
Computed
on the GPU: once, or at every frame when its shader reads time

The name is that of a @paint of the scene. Several objects can show the same one: it is drawn once for all of them.

More details

A scene with paint() is drawn with WebGL2, which <gss-scene>, mountAsync() and the playground pick by themselves; with WebGPU chosen, the object shows its color instead.

two objects, one shader

The cube and the sphere show the same stripes, drawn once per frame.

@paint stripes {
  uniform float time;
  out vec4 color;

  void main() {
    float band = step(0.5, fract(gl_FragCoord.y / 64.0 - time * 0.5));
    color = vec4(mix(vec3(1.0, 0.35, 0.21), vec3(0.98, 0.95, 0.9), band), 1.0);
  }
}

@scene {
  cube;
  sphere;
}

cube {
  translate: -0.9 0.6 0;
  size: 1.1;
  texture: paint(stripes);
}

sphere {
  translate: 0.9 0.7 0;
  radius: 0.7;
  texture: paint(stripes);
}

texture

Projects an image onto the surface of the object: one image per face, the top, the bottom and the sides, whatever the size of the object. The image replaces the base color, and moves, turns and scales with the object.

  1. url("photo.jpg")
  2. texture
texture: url("photo.jpg");
Syntax
url("<file>") | element(<id>) | paint(<name>)
Initial value
none
Applies to
objects
Animatable
no

Values

url("<file>")
An image file, read next to the .gss file, like url() in a stylesheet.
element(<id>)
A live image of an HTML element of the page: see element().
paint(<name>)
A texture drawn by a fragment shader: see @paint.
none
No image: the default.

a block of dirt

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
  texture: url("/textures/dirt.png");
}

an HTML element, with element()

A card of HTML on the front face of a thin cube.

@scene {
  cube;
}

scene {
  floor: none;
  camera-angle: -20deg 10deg;
  camera-target: 0 0.8 0;
  camera-distance: 3.2;
  ambient: 0.55;
}

cube {
  translate: 0 0.8 0;
  size: 1.6 1 0.06;
  corner-radius: 0.03;
}

cube::face(front) {
  texture: element(#card);
}
<article id="card" style="
  width: 320px; height: 200px; padding: 28px;
  box-sizing: border-box; border-radius: 18px;
  background: #f4f1ea; color: #1a1d2b;
  font: 600 30px/1.2 system-ui, sans-serif;">
  Hello from <em style="color: #ff5a36;">HTML</em>, on a 3D card
</article>

texture-mapping

How the image of texture is laid on the object. box, the default, projects one image on each face of the box around it; sphere wraps one image around the whole object, like a map around a globe, for the Earth, the Moon or a planet.

  1. url("earth.jpg")
  2. texture-mapping: box;
  3. texture-mapping: sphere;
Syntax
box | sphere
Initial value
box
Applies to
objects
Animatable
no

Values

box
One image on each face of the box around the object: the default.
sphere
One image around the whole object: the longitude across, the latitude up. The middle of the image faces the front, its top is the top of the object, and its left and right edges meet at the back.

sphere expects a map twice as wide as it is tall (an equirectangular map, like most maps of the Earth and the Moon). It covers the object once: texture-size has no effect.

More details

The image is seen from the center of the object: a sphere shows it in one piece, without the stretched corners of box, and only the poles squeeze its top and bottom rows, like on any globe. A map whose left and right edges match shows no seam at the back. Another shape shows the image too, projected from its center. The images of ::face() are wrapped the same way, each one on its own face, and image-rendering applies as with box. A large map can shimmer on a small sphere: 1024 pixels across is enough.

the Earth

NASA's Blue Marble around a sphere tilted like the Earth, turning once in 12 seconds.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  radius: 0.8;
  texture: url("/textures/earth.jpg");
  texture-mapping: sphere;
  rotate-x: -23deg;
  animation: spin 12s linear infinite;
}

@keyframes spin {
  to {
    rotate-y: 360deg;
  }
}

the Moon

NASA's map of the Moon, its near side to the front, lit from the right.

@scene {
  sphere;
}

scene {
  floor: none;
  background: #000000;
  light: 60deg 10deg 1.4;
  ambient: 0.05;
}

sphere {
  translate: 0 1 0;
  radius: 0.8;
  texture: url("/textures/moon.jpg");
  texture-mapping: sphere;
}

texture-size

The size of one image on the surface, in the units of the object, like background-size: the image repeats to cover each face. auto, the default, fits one image to each face.

  1. texture-size: auto;
  2. texture-size: 0.5;
Syntax
<number>
Initial value
auto
Applies to
objects
Animatable
no

a tiled slab

Images of 0.5 on a slab of 3: 6 × 6 per face.

@scene {
  cube;
}

cube {
  size: 3 0.2 3;
  translate: 0 0.1 0;
  texture: url('/textures/dirt.png');
  texture-size: 0.5;
  image-rendering: pixelated;
}

Text

content

Writes a text on the surface of the object, the way content gives a text to a ::before in CSS. The text is painted over the color, the gradient or the texture of the object: it moves, turns and takes the light with it, and goes behind what stands in front of it.

  1. "GSS"
  2. content
content: "GSS";
Syntax
<string>+ | none
Initial value
none
Applies to
objects
Animatable
yes

Values

<string>
The text, between quotes. Several strings side by side are joined: "3D" " text". \A starts a new line, like CSS: "one\A two".
var(--name)
A variable that holds a string.
none
No text: the default. normal is the same.

The text is as large as each face lets it be, or as font-size says. Unlike CSS, content goes on the object itself, not on a ::before.

More details

The text sits in the middle of each face, and keeps its proportions. Without ::face(), every face of the object shows it, read from left to right on each; ::face() gives a face a text of its own, or none, by its name or its number (::face(3), ::face(odd)). A cube and a plane write on their flat faces. A round shape (a sphere, a cylinder, a tube, a cone, a capsule, a torus) wraps the text around itself like a label on a can, on four sides around its axis, and lays it flat on its top and its bottom. A pyramid writes on its four slopes and its base, an octahedron on its eight triangles, upright. Any other shape takes the text of each side like a sticker, fading where its surface turns away. The font comes from font-family, font-weight and font-style, the size from font-size, the color from text-color, the side its lines lean to from text-align. An emoji keeps its own colors. On :hover and in @keyframes the text changes halfway, like a value of CSS that does not interpolate.

a sign

The word fits the front of the board, and each of its other faces.

@scene {
  cube;
}

scene {
  camera-angle: -20deg 12deg;
}

cube {
  translate: 0 0.8 0;
  size: 2.4 1 0.2;
  color: #1a1d2b;
  content: "hello";
  font-weight: bold;
}

a text per face

Each face of the die takes its own content.

@scene {
  cube;
}

scene {
  camera-angle: 35deg 30deg;
}

cube {
  translate: 0 0.6 0;
  color: #f4f1ea;
  corner-radius: 0.08;
  font-weight: bold;
}

cube::face(top) {
  content: "1";
}

cube::face(front) {
  content: "2";
}

cube::face(right) {
  content: "3";
}

cube::face(left) {
  content: "4";
}

cube::face(back) {
  content: "5";
}

cube::face(bottom) {
  content: "6";
}

around a can

On a cylinder, the text wraps around the side like a label.

@scene {
  cylinder;
}

scene {
  camera-angle: 25deg 15deg;
}

cylinder {
  translate: 0 0.9 0;
  radius: 0.6;
  height: 1.6;
  color: #e8443a;
  content: "SODA";
  font-weight: bold;
  animation: turn 6s linear infinite;
}

@keyframes turn {
  to {
    rotate-y: 360deg;
  }
}

a variable

The text is a variable of the scene, read with var().

@scene {
  sphere;
}

scene {
  --word: "GSS";
}

sphere {
  translate: 0 0.8 0;
  radius: 0.8;
  color: #3a7bff;
  content: var(--word);
  font-family: Georgia, serif;
  font-style: italic;
}

font-family

The font of the text of content, like CSS: a list of fonts, and the first one the browser has draws the text.

  1. font-family: sans-serif;
  2. font-family: serif;
  3. font-family: monospace;
Syntax
[<family-name> | <generic-family>]#
Initial value
sans-serif
Applies to
objects
Animatable
yes

A font the page loads, with @font-face or a stylesheet of fonts, can be named: the text is drawn again when it arrives. Unlike CSS, the font is not inherited: set it on the object that has the text.

a serif and a monospace

@scene {
  cube#a;
  cube#b;
}

scene {
  camera-distance: 5;
}

cube {
  size: 1.8 0.9 0.2;
  color: #f4f1ea;
  content: "Hello";
}

#a {
  translate: -1 0.8 0;
  font-family: Georgia, "Times New Roman", serif;
}

#b {
  translate: 1 0.8 0;
  font-family: monospace;
}

font-size

The height of the letters of content, in the units of the scene, like the other sizes of GSS. A line is as wide as the face: a longer text goes on to the next line, between its words.

  1. font-size: auto;
  2. font-size: 0.2;
Syntax
auto | <number>
Initial value
auto
Applies to
objects
Animatable
yes

Values

auto
The text is as large as each face lets it be, its lines broken only where it says \A: the default.
<number>
The height of the letters: 0.3 on a cube of 1 gives about three lines.

Unlike CSS, the size is in the units of the scene, and auto fits the text to the face. A taller text is cut at the edges of the face; its lines are centered on its height.

a paragraph on a board

@scene {
  cube;
}

scene {
  camera-angle: -15deg 10deg;
}

cube {
  translate: 0 0.9 0;
  size: 2.4 1.4 0.1;
  color: #f4f1ea;
  content: "Selectors, the cascade and keyframes, compiled into one shader.";
  font-size: 0.2;
}

font-style

Slants the letters of content, like CSS.

  1. font-style: normal;
  2. font-style: italic;
Syntax
normal | italic | oblique
Initial value
normal
Applies to
objects
Animatable
yes

italic

@scene {
  cube;
}

cube {
  translate: 0 0.8 0;
  size: 2.4 1 0.2;
  color: #f4f1ea;
  content: "Hello";
  font-family: Georgia, serif;
  font-style: italic;
}

font-weight

How thick the letters of content are, like CSS: normal is 400, bold is 700, and a number goes from 1 to 1000 when the font has that weight.

  1. font-weight: 300;
  2. font-weight: normal;
  3. font-weight: bold;
Syntax
normal | bold | <number>
Initial value
normal
Applies to
objects
Animatable
yes

normal and bold

@scene {
  cube#a;
  cube#b;
}

scene {
  camera-distance: 5;
}

cube {
  size: 1.8 0.9 0.2;
  color: #1a1d2b;
  content: "Hello";
}

#a {
  translate: -1 0.8 0;
}

#b {
  translate: 1 0.8 0;
  font-weight: bold;
}

text-align

The side the lines of content lean to, like CSS. start is the left and end the right: GSS reads from left to right.

  1. text-align: start;
  2. text-align: center;
  3. text-align: end;
Syntax
left | center | right | start | end
Initial value
start
Applies to
objects
Animatable
yes

centered lines

@scene {
  cube;
}

cube {
  translate: 0 0.9 0;
  size: 2.4 1.2 0.1;
  color: #1a1d2b;
  content: "GPU\A Style Sheets";
  text-align: center;
  font-weight: bold;
}

text-color

The color of the text of content. Without one, the text is black on a light object and white on a dark one, so it stays readable when the color of the object changes.

  1. black or white
  2. text-color: #ff5a36;
Syntax
<color>
Initial value
black or white
Applies to
objects
Animatable
yes

In CSS, color colors the text. In GSS, color is the color of the object, so its text has a property of its own.

orange on white

@scene {
  cube;
}

cube {
  translate: 0 0.8 0;
  size: 2.4 1 0.2;
  color: #f4f1ea;
  content: "hello";
  font-weight: bold;
  text-color: #ff5a36;
}

Filters

filter

Post-processing, like CSS filter: a list of functions applied in order, on the whole image (scene { filter }), on an object, or on a group and everything in it.

  1. filter: blur(4px);
  2. filter: bloom();
  3. filter: hue-rotate(160deg);
  4. filter: grain();
Syntax
none | <filter-function>+
Initial value
none
Applies to
the scene, objects and groups
Animatable
no

Functions

brightness(), contrast(), saturate()
A number or a percentage: 1 or 100% changes nothing.
grayscale(), sepia(), invert()
From 0 to 1: how far it goes.
hue-rotate()
An angle around the color wheel.
grain()
A film grain that moves at every frame (0.1 by default).
vignette()
Darker towards the corners, like an old lens: from 0 to 1 (0.5 by default). On the scene only.
chromatic-aberration()
Red and blue pulled apart towards the corners, like a cheap lens: a length in px at the corners (2px by default). On the scene only.
blur()
A blur, by a length in px, like CSS.
bloom()
Makes the bright parts glow: an amount (0.6 by default) and a radius in px (16px by default).
opacity()
Makes an object or a group transparent, multiplied with opacity. On the scene, it is an error: the scene stays opaque.

On an object or a group, the filters change only its own pixels. blur() and bloom() cost more as their radius grows; the other filters cost almost nothing.

More details

The reflections see the filters of an object too, and a blur() spreads the object over what is around it. Like CSS, an object's filter comes before its group's, and the scene's comes last; on an object, the filters that read one pixel come before blur() and bloom(), and an object and its group cannot both have one of them. blur() and bloom() cost more because they read the pixels around each pixel.

brightness()

The whole image, 1.4 times brighter.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  color: #ff5a36;
}

scene {
  filter: brightness(1.4);
}

contrast()

Darker darks, lighter lights.

@scene {
  sphere;
  cube;
}

sphere {
  translate: 0.8 0.6 0;
  color: #ff5a36;
}

cube {
  translate: -0.8 0.5 0;
  color: #3a7bff;
}

scene {
  filter: contrast(1.6);
}

saturate()

Twice the saturation.

@scene {
  sphere;
  cube;
}

sphere {
  translate: 0.8 0.6 0;
  color: #ff5a36;
}

cube {
  translate: -0.8 0.5 0;
  color: #3a7bff;
}

scene {
  filter: saturate(2);
}

grayscale()

No color left.

@scene {
  sphere;
  cube;
}

sphere {
  translate: 0.8 0.6 0;
  color: #ff5a36;
}

cube {
  translate: -0.8 0.5 0;
  color: #3a7bff;
}

scene {
  filter: grayscale(1);
}

sepia()

The tones of an old photograph.

@scene {
  sphere;
  cube;
}

sphere {
  translate: 0.8 0.6 0;
  color: #ff5a36;
}

cube {
  translate: -0.8 0.5 0;
  color: #3a7bff;
}

scene {
  filter: sepia(0.8);
}

hue-rotate()

Every hue turned by 120°.

@scene {
  sphere;
  cube;
}

sphere {
  translate: 0.8 0.6 0;
  color: #ff5a36;
}

cube {
  translate: -0.8 0.5 0;
  color: #3a7bff;
}

scene {
  filter: hue-rotate(120deg);
}

invert()

The negative of the image.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  color: #ff5a36;
}

scene {
  filter: invert(1);
}

grain()

A film grain over the image.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  color: #ff5a36;
}

scene {
  background: #1a1a22;
  filter: grain(0.15);
}

blur()

The whole image, blurred by 3px.

@scene {
  sphere;
  cube;
}

sphere {
  translate: 0.8 0.6 0;
  color: #ff5a36;
}

cube {
  translate: -0.8 0.5 0;
  color: #3a7bff;
}

scene {
  filter: blur(3px);
}

vignette() and chromatic-aberration()

The effects of a lens, on the whole image: darker corners, colors that split at the edges.

@scene {
  torus;
  sphere;
}

scene {
  filter: vignette(0.6) chromatic-aberration(4px);
}

torus {
  translate: 0 0.8 0;
  radius: 0.8;
  thickness: 0.25;
  rotate-x: 60deg;
  color: #f4f1ea;
}

sphere {
  translate: 0 0.8 0;
  radius: 0.3;
  color: #ff5a36;
}

bloom()

Bright spheres that glow in the dark.

@scene {
  sphere.light * 5;
}

.light {
  radius: 0.25;
  translate: calc(2.4 - sibling-index() * 0.8) 0.8 0;
  color: hsl(calc(sibling-index() * 40) 100% 70%);
}

scene {
  floor: none;
  background: #07070a;
  ambient: 1;
  filter: bloom(0.9, 20px);
}

filter on objects

Each sphere has a filter of its own.

@scene {
  sphere#a;
  sphere#b;
  sphere#c;
}

sphere {
  radius: 0.5;
  color: #ff5a36;
}

#a {
  translate: 1.3 0.6 0;
  filter: grayscale(1);
}

#b {
  translate: 0 0.6 0;
  filter: blur(4px);
}

#c {
  translate: -1.3 0.6 0;
  filter: hue-rotate(180deg) brightness(1.3);
}

filter on a group

Only the spheres of the group glow.

@scene {
  group#lights {
    sphere * 4
  }
  cube;
}

#lights {
  filter: bloom(0.9, 18px);
}

#lights sphere {
  radius: 0.2;
  translate: calc(1.75 - sibling-index() * 0.7) 1.4 0;
  color: #ffd27a;
}

cube {
  translate: 0 0.5 0;
  color: #3a7bff;
}

scene {
  floor: none;
  background: #07070a;
}

opacity() on a group

The group fades: its objects show through each other.

@scene {
  group#g {
    cube;
    sphere;
  }
}

#g {
  filter: opacity(0.5);
}

cube {
  translate: -0.6 0.5 0;
  color: #3a7bff;
}

sphere {
  translate: 0.6 0.5 0;
  radius: 0.5;
  color: #ff5a36;
}

filters together

Four filters, applied in the order written.

@scene {
  torus;
  sphere;
}

torus {
  translate: 0 1 0;
  rotate-x: 70deg;
  color: #ffd27a;
  material: gold;
}

sphere {
  radius: 0.3;
  translate: 0 1 0;
  color: #ff5a36;
}

scene {
  floor: none;
  background: radial-gradient(#2a2a3a, #07070a);
  filter: contrast(1.1) saturate(1.3) bloom(0.7, 18px) grain(0.06);
}

Visibility, opacity, outlines and masks

display

none leaves the object out of the scene, like CSS: it is not drawn, casts no shadow and cannot be pointed at. On a group, every object inside it is left out; on a light, the light is off.

none
sphere { display: none; }
Syntax
none | block
Initial value
block
Applies to
objects
Animatable
no

It changes with @media, not on :hover or in @keyframes: to hide an object for a moment, use visibility.

More details

The object still counts among its siblings, like an element of the page with display: none: :nth-child() and sibling-index() see it. It is set when the scene compiles. Like CSS, @media (max-width: …) reads the width of the window on a page; in the playground and in Try it, the width of the render.

a whole group left out

The group of small spheres says display: none: only the torus is drawn. Remove the rule to see them.

@scene {
  torus;
  group#moons {
    sphere * 3;
  }
}

torus {
  translate: 0 0.8 0;
  rotate-x: 70deg;
  radius: 0.7;
  thickness: 0.2;
}

#moons {
  display: none;
}

#moons sphere {
  radius: 0.18;
  translate: calc(sibling-index() * 0.6 - 1.2) 1.8 0;
  color: #ff5a36;
}

with @media

The small spheres show only when the scene is wider than 400px: Try it is narrower, so they are left out; open it in the playground and drag the divider to see them come and go.

@scene {
  torus;
  sphere.moon * 3;
}

torus {
  translate: 0 0.8 0;
  rotate-x: 70deg;
  radius: 0.7;
  thickness: 0.2;
}

.moon {
  radius: 0.18;
  translate: calc(sibling-index() * 0.6 - 1.2) 1.8 0;
  color: #ff5a36;
}

@media (max-width: 400px) {
  .moon {
    display: none;
  }
}

every other one

display: none on every other cube: the others keep their places.

@scene {
  cube * 7;
}

cube {
  size: 0.4;
  translate: calc(sibling-index() * 0.6 - 2.4) 0.3 0;
  color: #3a7bff;
}

cube:nth-child(even) {
  display: none;
}

mask-image

Cuts holes in the object, like a CSS mask: where the image covers the surface, the object is there; where it is transparent, the surface is not drawn, and the eye sees inside the object, then what is behind it. The image is a gradient, a noise(), or one of them moved by displace().

  1. mask-image
  2. the object
mask-image: repeating-linear-gradient(black 0 20%, transparent 20% 40%);
Syntax
none | <gradient>
Initial value
none
Applies to
objects
Animatable
yes

Unlike CSS, a mask does not fade the object: a part covered less than half is a hole, with a sharp edge. The mouse goes through the holes.

More details

It is read like a gradient in color: seen from the front in the object's own space, and in 3D for a noise(), so the holes move and turn with the object. mask-mode says what counts, the alpha by default. The holes show in the reflections too, and :hover reaches the object behind a hole.

holes from a noise

A noise() whose black covers and whose transparent cuts.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  radius: 0.8;
  color: #ff5a36;
  mask-image: noise(4 3, black 48%, transparent 52%);
}

a sphere seen through a cube

A round hole in each face shows the sphere inside the cube.

@scene {
  cube;
  sphere;
}

scene {
  camera-angle: 20deg 15deg;
}

cube {
  translate: 0 0.7 0;
  size: 1.4;
  color: #3a7bff;
  mask-image: radial-gradient(circle, transparent 35%, black 36%);
}

sphere {
  translate: 0 0.7 0;
  radius: 0.35;
  color: #ff5a36;
}

a dissolve

Moving the stops of the noise() dissolves the sphere.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  radius: 0.8;
  color: #ff5a36;
  mask-image: noise(4 3, black 20%, transparent 20%);
  animation: dissolve 3s ease-in-out alternate;
}

@keyframes dissolve {
  to {
    mask-image: noise(4 3, black 80%, transparent 80%);
  }
}

mask-mode

Which part of mask-image counts, like CSS.

  1. mask-mode: alpha;
  2. mask-mode: luminance;
Syntax
alpha | luminance | match-source
Initial value
match-source
Applies to
objects
Animatable
no

Values

match-source
The default: a gradient counts by its alpha, like CSS.
alpha
The transparency of its colors: black and white are both there.
luminance
Their brightness, times their alpha, like an SVG mask: white is there, black is a hole.

a cage, with luminance

White stripes stay, black ones are holes: the sphere shows between the bars.

@scene {
  cube;
  sphere;
}

cube {
  translate: 0 0.7 0;
  size: 1.4;
  mask-image: repeating-linear-gradient(90deg, white 0% 10%, black 10% 20%);
  mask-mode: luminance;
}

sphere {
  translate: 0 0.7 0;
  radius: 0.45;
  color: #ff5a36;
}

opacity

How much the object covers what is behind it, like CSS: from 0, invisible, to 1, opaque (the default), as a number or a percentage; a value outside is kept between them. The alpha of its color and opacity() in filter multiply with it.

opacity: 1;opacity: 0.4;
Syntax
<number> | <percentage>
Initial value
1
Applies to
objects
Animatable
yes

Unlike CSS, a group does not fade as one picture: each of its objects fades, so they show through each other.

More details

Through a transparent object, the eye sees its back face from the inside, then what is behind it: a sphere at 50% looks like a bubble. With shadows, the light goes through a transparent object like stained glass: a red glass at 50% casts a pink light. The mouse still points at a transparent object, like CSS, and the reflections show it opaque.

a bubble

A sphere at 35%, with another one inside.

@scene {
  sphere#bubble;
  sphere#core;
}

#bubble {
  translate: 0 1 0;
  radius: 0.8;
  color: #9fd8ff;
  opacity: 0.35;
}

#core {
  translate: 0 1 0;
  radius: 0.3;
  color: #ff5a36;
}

fading on :hover

The cube fades to 25% under the mouse.

@scene {
  cube;
}

cube {
  translate: 0 0.6 0;
  size: 1.2;
  color: #3a7bff;
  transition: 0.4s;
}

cube:hover {
  opacity: 0.25;
}

a colored shadow

A red pane at 50% casts a pink light.

@scene {
  cube;
  sphere;
}

scene {
  shadows: soft;
  light: 30deg 55deg;
}

cube {
  translate: 0 0.9 0;
  size: 1.2 2.2 0.05;
  color: #ff2a2a;
  opacity: 0.5;
  animation: move 5s ease alternate;
}

sphere {
  translate: 0.9 0.4 -1;
  radius: 0.4;
}

@keyframes move {
  to {
    translate: 1 0.9 0;
  }
}

outline

A line around the silhouette of the object, like CSS outline: a width, a style and a color, in any order. Like CSS, it needs a style: outline: 0.03 solid #111;, or dashed, dotted, double…

outline: 0.03 solid #ff5a36;
Syntax
<outline-width> || <outline-style> || <outline-color>
Initial value
medium none currentColor
Applies to
objects
Animatable
yes

Unlike CSS, its width is in the units of the scene, not in pixels: it gets thinner far from the camera, like the object. It changes on :hover and in @keyframes.

More details

The line is drawn outside the object, where the eye passes close to its edge, and over what is behind it: another object, the floor or the background. It takes no room, is never lit and cannot be pointed at, like CSS, but the fog covers it. outline-width, outline-style, outline-color and outline-offset set one part each, and win over outline wherever they are written, like the other longhands of GSS.

a drawn look

Dark lines around matte shapes, like a drawing.

@scene {
  sphere;
  cube;
  torus;
}

* {
  outline: 0.03 solid #1a1a1a;
}

sphere {
  translate: -1.3 0.6 0;
  radius: 0.6;
  color: #ffd166;
}

cube {
  translate: 0 0.5 0;
  color: #ef476f;
  rotate-y: 30deg;
}

torus {
  translate: 1.4 0.6 0;
  rotate-x: 70deg;
  radius: 0.5;
  thickness: 0.18;
  color: #06d6a0;
}

outlined under the mouse

Hovering the cube draws its outline, which glides in.

@scene {
  cube;
}

cube {
  translate: 0 0.6 0;
  color: #3a7bff;
  outline: 0 solid #ff5a36;
  transition: 0.25s;
  cursor: pointer;
}

cube:hover {
  outline-width: 0.06;
}

outline-color

The color of the line of outline. Like CSS, it is the object's own color by default; a gradient gives the mean of its colors.

outline-color: #ff5a36;outline-color: rgb(79 140 255);outline-color: mediumseagreen;
Syntax
<color>
Initial value
currentColor
Applies to
objects
Animatable
yes

a line that changes color

The outline goes from orange to blue and back.

@scene {
  torus;
}

torus {
  translate: 0 0.8 0;
  rotate-x: 70deg;
  radius: 0.6;
  thickness: 0.2;
  color: #f4f4f6;
  outline: 0.04 solid #ff5a36;
  animation: hue 3s ease-in-out alternate;
}

@keyframes hue {
  to {
    outline-color: #3a7bff;
  }
}

outline-offset

The gap between the object and the line of outline, in the units of the scene, like CSS: the line starts this far from the surface.

outline-offset: 0;outline-offset: 0.15;
Syntax
<number>
Initial value
0
Applies to
objects
Animatable
yes

a halo

The line floats away from the sphere and back.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  radius: 0.5;
  color: #ffd166;
  outline: 0.02 solid #ffd166;
  animation: halo 2s ease-in-out alternate;
}

@keyframes halo {
  to {
    outline-offset: 0.25;
  }
}

outline-style

How the line of outline is drawn, like CSS: none, the default, draws none; the others draw a line, plain, broken or shaded.

  1. outline-style: solid;
  2. outline-style: dashed;
  3. outline-style: dotted;
  4. outline-style: double;
Syntax
none | auto | solid | dashed | dotted | double | groove | ridge | inset | outset
Initial value
none
Applies to
objects
Animatable
no

Values

none
No line, the default: an outline needs a style.
solid
One plain line.
auto
In CSS, the browser draws its own line, the one of a focused field. GSS has no browser look of its own: auto draws solid.
dashed
Dashes three widths long, with gaps as long, all around the object.
dotted
Round dots, as wide as the line, one width apart.
double
Two thin lines, each a third of the width, with the third between them empty.
groove
Carved into the scene: the outer half darker at the top left, the inner half at the bottom right.
ridge
Raised out of the scene: groove the other way.
inset
The line darker at the top left, as if the object sat in a hollow.
outset
The line darker at the bottom right, as if the object stood out.

A :hover or :active rule can change the style; @keyframes cannot.

More details

Dashes and dots are placed around the object, by the angle seen from the camera, and as many as fit around its size: they close without a broken one. The shaded styles take the light from the top left of the screen, like the borders of CSS, at half the color on their dark side. When the object has no style at rest, the one of :hover, then of :active, is drawn.

every style

Eight spheres, one style each, from left to right: solid, dashed, dotted, double, groove, ridge, inset and outset.

@scene {
  sphere * 8;
}

scene {
  background: #14141c;
  camera-distance: 9;
}

sphere {
  radius: 0.4;
  translate: calc(sibling-index() * 1.05 - 4.7) 0.5 0;
  color: #f4f4f6;
  outline: 0.08 solid #ff5a36;
}

sphere:nth-child(2) {
  outline-style: dashed;
}

sphere:nth-child(3) {
  outline-style: dotted;
}

sphere:nth-child(4) {
  outline-style: double;
}

sphere:nth-child(5) {
  outline-style: groove;
}

sphere:nth-child(6) {
  outline-style: ridge;
}

sphere:nth-child(7) {
  outline-style: inset;
}

sphere:nth-child(8) {
  outline-style: outset;
}

dashes on hover

Hovering the cube draws a dashed line around it.

@scene {
  cube;
}

cube {
  translate: 0 0.6 0;
  color: #3a7bff;
  outline-style: dashed;
  outline-width: 0;
  outline-color: #ff5a36;
  transition: 0.25s;
  cursor: pointer;
}

cube:hover {
  outline-width: 0.05;
  outline-offset: 0.05;
}

solid

The style alone draws a medium line of the object's own color.

@scene {
  cube;
}

cube {
  translate: 0 0.6 0;
  color: #3a7bff;
  outline-style: solid;
  outline-offset: 0.06;
}

outline-width

How wide the line of outline is, in the units of the scene, unlike CSS pixels: thin is 0.01, medium 0.02 and thick 0.04.

  1. outline-width: thin;
  2. outline-width: medium;
  3. outline-width: thick;
Syntax
<number> | thin | medium | thick
Initial value
medium
Applies to
objects
Animatable
yes

a thick line

A wide white line around a sphere.

@scene {
  sphere;
}

scene {
  background: #14141c;
}

sphere {
  translate: 0 1 0;
  radius: 0.6;
  color: #ff5a36;
  outline-style: solid;
  outline-color: white;
  outline-width: 0.08;
}

visibility

hidden hides the object, like CSS: it is not drawn, casts no shadow, and the mouse goes through it. collapse is hidden, as in CSS outside tables.

sphere { visibility: hidden; }sphere { visibility: visible; }
Syntax
visible | hidden | collapse
Initial value
visible
Applies to
objects
Animatable
yes

Like CSS, it is inherited: a hidden group hides its objects, and an object inside it can be visible again. A hidden object cannot be hovered: hide another object, like #button:hover #label.

More details

It changes on :hover and in @keyframes, at once, like CSS: between visible and hidden, the object is there for the whole way, and gone only at hidden. A group's own visibility is read when the scene compiles.

a light that blinks

The sphere is gone for the second half of each second.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  radius: 0.5;
  material: emissive(#ff5a36);
  animation: blink 1s step-end;
}

@keyframes blink {
  50% {
    visibility: hidden;
  }
}

shown under the mouse

Hovering the cube shows the sphere above it.

@scene {
  group#g {
    cube;
    sphere;
  }
}

#g {
  translate: 0 0.5 0;
}

sphere {
  translate: 0 1.2 0;
  radius: 0.3;
  color: #ff5a36;
  visibility: hidden;
}

#g:hover sphere {
  visibility: visible;
}

one visible in a hidden group

The group is hidden, but its sphere says visible.

@scene {
  group#g {
    cube * 3;
    sphere;
  }
}

#g {
  visibility: hidden;
  translate: 0 0.6 0;
}

cube {
  size: 0.5;
  translate: calc(sibling-index() - 2) 0 0;
}

sphere {
  visibility: visible;
  radius: 0.4;
  color: #ff5a36;
}

Transforms

offset-distance

How far along its offset-path the object is, like CSS: a number, in the units of the path, or a percentage of its length. Animate it with @keyframes, a transition on :hover, or the scroll.

offset-distance: 50%;
Syntax
<number> | <percentage>
Initial value
0
Applies to
objects
Animatable
yes

On an open path, the object stops at its ends; on a closed one (with Z), it goes round, so 150% is halfway through the second lap. A ray() has no length: give it a number.

three places on one path

Three spheres at 0%, 50% and 100% of one path.

@scene {
  sphere * 3;
}

sphere {
  translate: 0 1 0;
  radius: 0.25;
  color: #e6e6e6;
  offset-path: path("M-2 0 Q0 2 2 0");
  offset-distance: calc((sibling-index() - 1) * 50%);
}

sphere:nth-child(2) {
  color: #ff5a36;
}

on :hover, along a ray

Under the mouse, the cube slides 1.5 along a ray.

@scene {
  cube;
}

cube {
  translate: 1 0.4 0;
  size: 0.5;
  color: #ff5a36;
  offset-path: ray(45deg);
  offset-rotate: 0deg;
  transition: 0.5s ease-out;
}

cube:hover {
  offset-distance: 1.5;
}

offset-path

A motion path, like CSS: the line the object travels along, placed by offset-distance and turned by offset-rotate. It works on groups too.

offset-distance: 0%;offset-distance: 100%;
Syntax
none | path(<string>) | ray(<angle>)
Initial value
none
Applies to
objects
Animatable
no

Values

path(<string>)
The d of an SVG path, in the object's own xy plane, facing the camera like the path shape: centered on itself, y up, one path unit per scene unit. A path shape with the same d draws the track; a path that ends with Z is closed, and the object goes round it.
ray(<angle>)
A straight line from the place of the object, at an angle: 0deg up, 90deg right, like CSS.
none
No motion path: the default.

Like CSS, the motion path comes after translate, the rotations and scale: translate moves the whole path, scale scales it. For a path on the floor, turn a group with rotate-x: 90deg.

along a track

A ball that follows the curve of a path drawn under it.

@scene {
  path#track;
  sphere#ball;
}

#track {
  translate: 0 1.2 0;
  d: path("M-2 0 C-1 2 1 -2 2 0");
  stroke-width: 0.04;
  color: #555555;
}

#ball {
  translate: 0 1.2 0;
  radius: 0.2;
  color: #ff5a36;
  offset-path: path("M-2 0 C-1 2 1 -2 2 0");
  animation: go 3s ease-in-out alternate;
}

@keyframes go {
  to {
    offset-distance: 100%;
  }
}

round a closed path, turned along it

A cube that laps a closed ellipse, turned along it.

@scene {
  cube;
}

cube {
  translate: 0 1.2 0;
  size: 0.6 0.25 0.25;
  color: #3a7bff;
  offset-path: path("M-1.5 0 A1.5 1 0 1 1 1.5 0 A1.5 1 0 1 1 -1.5 0 Z");
  animation: lap 4s linear;
}

@keyframes lap {
  to {
    offset-distance: 100%;
  }
}

offset-rotate

How the object turns along its offset-path, like CSS.

  1. offset-rotate: auto;
  2. offset-rotate: 0deg;
Syntax
[auto | reverse] || <angle>
Initial value
auto
Applies to
objects
Animatable
no

Values

auto
The default: its x axis follows the path, like a car on a road.
reverse
The same, turned the other way.
<angle>
A fixed turn, like rotate-z: 0deg keeps the object upright.
auto 90deg
With auto or reverse, the angle is added to the turn along the path.

along the path, or upright

The upper cube turns along the path; the lower one stays upright.

@scene {
  cube#along;
  cube#upright;
}

cube {
  size: 0.5 0.2 0.2;
  color: #ff5a36;
  offset-path: path("M-2 0 C-1 2 1 -2 2 0");
  animation: go 3s ease-in-out alternate;
}

#along {
  translate: 0 1.6 0;
}

#upright {
  translate: 0 0.6 0;
  color: #3a7bff;
  offset-rotate: 0deg;
}

@keyframes go {
  to {
    offset-distance: 100%;
  }
}

rotate-x

Rotates the object around the x axis. Like CSS rotateX(), a positive angle turns its top away from the viewer. On a group, it turns everything inside it around the origin of the group.

rotate-x: 0deg → 360deg;
Syntax
<angle>
Initial value
0deg
Applies to
objects
Animatable
yes

45 degrees

@scene {
  cube;
}

cube {
  rotate-x: 45deg;
}

rotate-y

Rotates the object around the y axis. Like CSS rotateY(), a positive angle turns its right side away from the viewer. On a group, it turns everything inside it around the origin of the group.

rotate-y: 0deg → 360deg;
Syntax
<angle>
Initial value
0deg
Applies to
objects
Animatable
yes

-45 degrees

@scene {
  cube;
}

cube {
  rotate-y: -45deg;
}

rotate-z

Rotates the object around the z axis. Like CSS rotate(), a positive angle turns it clockwise. On a group, it turns everything inside it around the origin of the group.

rotate-z: 0deg → 360deg;
Syntax
<angle>
Initial value
0deg
Applies to
objects
Animatable
yes

-45 degrees

@scene {
  cube;
}

cube {
  rotate-z: -45deg;
}

scale

Scales the object by one number, on its three axes. On a group, it scales everything inside it, the positions of its children included.

scale: 0.55;scale: 1.35;
Syntax
<number>
Initial value
1.0
Applies to
objects
Animatable
yes

twice as big

@scene {
  cube;
}

cube {
  scale: 2.0;
}

transform-origin

The point the object turns and scales around, like CSS: rotate-x, rotate-y, rotate-z and scale keep it in place, and a motion path carries it along the path.

transform-origin: left bottom;
Syntax
[ left | center | right | top | bottom | <percentage> | <number> ]{1,2} <number>?
Initial value
center
Applies to
objects
Animatable
yes

Values

left, right, top, bottom, center
The sides of the box of the object, like CSS. Two keywords go in any order: top left.
<percentage>
Also on the box: 0% is the left or the top side, 100% the right or the bottom one.
<number>
A point of the object's own space, from its center, y up, like translate: 0 0.5 0 is 0.5 above the center. The third value, z, is always a number.

Unlike CSS, where a length is measured from the top left corner, a number is measured from the center, like everything in GSS. A group has no box: it takes numbers and center.

a door on its hinge

A door that turns on its left side.

@scene {
  cube#door;
  cube#frame;
}

#frame {
  size: 0.1 1.6 0.1;
  translate: -0.6 0.8 0;
  color: #3a7bff;
}

#door {
  size: 1.1 1.5 0.08;
  translate: 0 0.8 0;
  transform-origin: left;
  animation: open 3s ease-in-out infinite alternate;
  color: #ff5a36;
}

@keyframes open {
  to {
    rotate-y: -80deg;
  }
}

grow from the floor

Columns that grow from their base.

@scene {
  cylinder * 5;
}

cylinder {
  radius: 0.18;
  height: 1.2;
  translate: calc((sibling-index() - 3) * -0.5) 0 0;
  transform-origin: bottom;
  animation: grow 1.5s calc(sibling-index() * 0.15s) ease-out infinite alternate;
  color: #ff5a36;
}

@keyframes grow {
  from {
    scale: 0.2;
  }
}

translate

Moves the object along the x, y and z axes. Like CSS, x points right and z toward the viewer; y points up, and the floor is at y = 0. On a group, it moves everything inside it, and the positions of its children become relative to the group.

translate: 1.5 0 0;translate: 0 1.1 0;translate: 0 0 1.5;
Syntax
<number>{3}
Initial value
0 0 0
Applies to
objects
Animatable
yes

on the floor

A cube of 1, half a unit up: it sits on the floor.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
}

Animation and transitions

animation

Plays a @keyframes animation on the object, like CSS. After its name come the duration, then if needed an easing, a delay, a number of iterations, a direction and a fill mode, in any order.

animation: float 2s ease-in-out alternate;
Syntax
<keyframes-name> <time> [<easing>] [<time>] [<number> | infinite] [normal | reverse | alternate | alternate-reverse] [none | forwards | backwards | both]
Initial value
none
Applies to
the scene, objects and groups
Animatable
no

Values

<time>
The first time is the duration of one iteration, in s or ms; the second one, the delay: the animation starts after it, or partway with a negative delay.
<easing>
linear (the default), ease, ease-in, ease-out, ease-in-out, cubic-bezier(), linear() or steps().
<number> | infinite
How many times it plays: 1.5 stops halfway through the second time. infinite is the default.
normal, reverse, alternate, alternate-reverse
The direction: forward, backward, forward then backward, or backward first.
none, forwards, backwards, both
The fill mode: what the object shows outside the animation.

Unlike CSS, an animation loops forever unless it says how many times it plays.

More details

Each part also has a property of its own, like animation-duration, which wins over what animation says. On an object, it animates translate, the rotations, scale, color (a gradient too), opacity, mask-image and offset-distance; on a group, its transforms. On the scene, it animates the properties of the scene, like background, light or fog, and the variables they use: the objects do not follow the variables a scene animates. In a :hover or :active rule, the animation starts with the state and plays to its end, once unless it says how many times, like a button that fires: see :active.

up and down

alternate plays the frames forward, then backward.

@scene {
  sphere;
}

sphere {
  animation: float 2s ease-in-out alternate;
}

@keyframes float {
  from {
    translate: 0 1 0;
  }
  to {
    translate: 0 2 0;
  }
}

a bounce

Up at 50%, and back down, in a loop of 1 s.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
  animation: bounce 1s;
}

@keyframes bounce {
  0%, 100% {
    translate: 0 0.5 0;
  }
  50% {
    translate: 0 1.5 0;
    scale: 1.2;
  }
}

a full turn

linear keeps a constant speed while the cube turns and changes color.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
  color: #ff5a36;
  animation: turn 4s linear;
}

@keyframes turn {
  to {
    rotate-y: -1turn;
    color: #3a7bff;
  }
}

an overshoot

A cubic-bezier() that goes past the top before it settles.

@scene {
  sphere;
}

sphere {
  translate: 0 0.5 0;
  radius: 0.5;
  color: #ff5a36;
  animation: jump 1.2s cubic-bezier(0.3, -0.4, 0.7, 1.4) alternate;
}

@keyframes jump {
  to {
    translate: 0 2 0;
  }
}

a drop, once

After 0.5 s, one iteration; both shows the first frame before it and the last one after.

@scene {
  cube;
}

cube {
  translate: 0 3 0;
  color: #ff5a36;
  animation: drop 1s ease-in 0.5s 1 both;
}

@keyframes drop {
  to {
    translate: 0 0.5 0;
    rotate-y: -90deg;
  }
}

animation on the scene

The background of the scene darkens to dusk.

@scene {
  sphere;
}

sphere {
  translate: 0 1 0;
  color: #ff5a36;
}

scene {
  floor: none;
  background: #101018;
  animation: dusk 4s ease-in-out alternate;
}

@keyframes dusk {
  to {
    background: #3a1f4a;
  }
}

animation-delay

How long the animation waits before it starts, in s or ms; a negative delay starts it partway, as if it had begun earlier. It wins over the delay written in animation.

animation-delay: 1s;
Syntax
<time>
Initial value
0s
Applies to
the scene, objects and groups
Animatable
no

a second of wait

@scene {
  sphere;
}

sphere {
  radius: 0.5;
  color: #3a7bff;
  animation: rise 1.5s ease-in-out;
  animation-delay: 1s;
}

@keyframes rise {
  from {
    translate: 0 0.5 0;
  }
  to {
    translate: 0 2 0;
  }
}

animation-direction

The way the animation plays, like CSS. It wins over the direction written in animation.

animation-direction: alternate;
Syntax
normal | reverse | alternate | alternate-reverse
Initial value
normal
Applies to
the scene, objects and groups
Animatable
no

Values

normal
Forward: the default.
reverse
Backward.
alternate
Forward, then backward.
alternate-reverse
Backward, then forward.

back and forth

@scene {
  sphere;
}

sphere {
  radius: 0.5;
  color: #3a7bff;
  animation: rise 1.5s ease-in-out;
  animation-direction: alternate;
}

@keyframes rise {
  from {
    translate: 0 0.5 0;
  }
  to {
    translate: 0 2 0;
  }
}

animation-duration

The duration of one iteration of the animation, in s or ms. It wins over the duration written in animation.

animation-duration: 2s;
Syntax
<time>
Initial value
0s
Applies to
the scene, objects and groups
Animatable
no

3 s instead of 1.5 s

@scene {
  sphere;
}

sphere {
  radius: 0.5;
  color: #3a7bff;
  animation: rise 1.5s ease-in-out;
  animation-duration: 3s;
}

@keyframes rise {
  from {
    translate: 0 0.5 0;
  }
  to {
    translate: 0 2 0;
  }
}

animation-fill-mode

What the object shows outside the animation, like CSS. It matters only with a delay or a number of iterations.

animation-fill-mode: both;
Syntax
none | forwards | backwards | both
Initial value
none
Applies to
the scene, objects and groups
Animatable
no

Values

none
Its own value: the default.
backwards
The first frame, during the delay.
forwards
The last frame, once the animation is over.
both
The two.

staying at the top

One iteration, and forwards keeps the last frame.

@scene {
  sphere;
}

sphere {
  radius: 0.5;
  color: #3a7bff;
  animation: rise 1.5s ease-in-out;
  animation-iteration-count: 1;
  animation-fill-mode: forwards;
}

@keyframes rise {
  from {
    translate: 0 0.5 0;
  }
  to {
    translate: 0 2 0;
  }
}

animation-iteration-count

How many times the animation plays: a number, where 1.5 stops halfway through the second time, or infinite. Unlike CSS, where an animation plays once, a GSS animation loops forever by default. It wins over the count written in animation.

animation-iteration-count: 2;
Syntax
<number> | infinite
Initial value
infinite
Applies to
the scene, objects and groups
Animatable
no

twice

@scene {
  sphere;
}

sphere {
  radius: 0.5;
  color: #3a7bff;
  animation: rise 1.5s ease-in-out;
  animation-iteration-count: 2;
}

@keyframes rise {
  from {
    translate: 0 0.5 0;
  }
  to {
    translate: 0 2 0;
  }
}

animation-range

The part of a scroll timeline the animation plays on, like CSS: entry while the scene comes into view, exit while it leaves, or percentages of the whole scroll. It goes with animation-timeline.

animation-range: entry;
Syntax
[ normal | <range-name> <percentage>? | <percentage> | <length> ]{1,2}
Initial value
normal
Applies to
the scene, objects and groups
Animatable
no

Values

normal
The whole timeline, the default.
cover
From the moment the scene starts to enter its scroll container to the moment it has left: the whole of view().
contain
While the scene is all inside its scroll container, or, when it is larger, while it fills it.
entry
While the scene comes in, from its first pixel to all of it. entry-crossing: while its start edge crosses its whole height.
exit
While the scene goes out, from its first pixel out to all of it. exit-crossing: from its end edge leaving to its start edge leaving.
<percentage>
A share of the named range before it, or of the whole timeline alone: entry 50%, 20% 80%.
<length>
Pixels from the start of the named range before it, or of the whole timeline: 100px.

It takes a start, then an end: entry 10% exit 90%. Before the range the animation shows its first frame, past it its last.

More details

A name alone is the whole of that range, animation-range: entry; a name alone at the end is its end, contain exit. animation-range-start and animation-range-end set one side each, and win over animation-range wherever they are written, like the other longhands of animation. The named ranges belong to view(): scroll() takes percentages and pixels. Unlike CSS, which ignores it there, a range on an animation that plays in time is an error. In the playground, which does not scroll, the slider over the render stands for one screen of scroll: 100px is a hundred pixels of the height of the window.

rise while it comes in

The spheres rise while the scene enters the page, then stay up.

@scene {
  sphere * 3;
}

sphere {
  --x: calc(2 - sibling-index());
  radius: 0.35;
  translate: var(--x) 0.35 0;
  color: #3a7bff;
  animation: rise 1s ease-out;
  animation-timeline: view();
  animation-range: entry;
}

@keyframes rise {
  to {
    translate: var(--x) 1.6 0;
    color: #3ad16b;
  }
}

the middle of the scroll

The cube turns between 25% and 75% of the scroll of the page, and holds before and after.

@scene {
  cube;
}

cube {
  translate: 0 0.6 0;
  corner-radius: 0.1;
  color: #ff5a36;
  animation: turn 1s linear;
  animation-timeline: scroll();
  animation-range: 25% 75%;
}

@keyframes turn {
  to {
    rotate-y: -360deg;
  }
}

animation-range-end

Where on the timeline the animation ends, like CSS: the second half of animation-range. A name alone is the end of that range.

animation-range-end: contain;
Syntax
normal | <range-name> <percentage>? | <percentage> | <length>
Initial value
normal
Applies to
the scene, objects and groups
Animatable
no

done once it is in

The cube grows until the scene is all in view.

@scene {
  cube;
}

cube {
  translate: 0 0.6 0;
  color: #3a7bff;
  scale: 0.3;
  animation: grow 1s ease-out;
  animation-timeline: view();
  animation-range-end: entry;
}

@keyframes grow {
  to {
    scale: 1;
  }
}

animation-range-start

Where on the timeline the animation starts, like CSS: the first half of animation-range. A name alone is the start of that range.

animation-range-start: contain;
Syntax
normal | <range-name> <percentage>? | <percentage> | <length>
Initial value
normal
Applies to
the scene, objects and groups
Animatable
no

from the middle of the entry

The torus starts turning when half of the scene is in view.

@scene {
  torus;
}

torus {
  translate: 0 0.8 0;
  radius: 0.6;
  thickness: 0.2;
  color: #ff5a36;
  animation: tilt 1s;
  animation-timeline: view();
  animation-range-start: entry 50%;
}

@keyframes tilt {
  to {
    rotate-x: 90deg;
  }
}

animation-timeline

Drives the animation with the scroll of the page instead of time, like CSS scroll-driven animations. The whole timeline is the whole animation, so the duration and the delay do not count.

animation-timeline: scroll();
Syntax
auto | scroll([root | nearest] || [block | inline | x | y]) | view([block | inline | x | y])
Initial value
auto
Applies to
the scene, objects and groups
Animatable
no

Values

auto
The default: the animation plays in time.
scroll()
A scroll container, from its start (0%) to its end (100%): nearest (the default) is the closest one around the scene, root the page. The axis is block (the default), inline, y or x.
view()
The scene crossing its scroll container: 0% when it enters at the bottom, 100% when it leaves at the top.

Write it after animation, which still needs a name and a duration: animation: spin 1s linear; animation-timeline: scroll();. Scrolling back plays the animation backward.

More details

The number of iterations and the direction still count, an animation without a count plays once along the scroll. A scene takes up to 4 different timelines. In the playground, which does not scroll, a slider stands in for the scroll.

turn with the page

The cube turns and rises as the page scrolls.

@scene {
  cube;
}

cube {
  translate: 0 0.6 0;
  corner-radius: 0.1;
  color: #ff5a36;
  animation: turn 1s linear;
  animation-timeline: scroll();
}

@keyframes turn {
  from {
    rotate-y: 0deg;
  }
  to {
    rotate-y: -360deg;
    translate: 0 1.6 0;
  }
}

rise into view

The spheres rise as the scene comes into view; the second one goes up and back.

@scene {
  sphere * 3;
}

sphere {
  --x: calc(2 - sibling-index());
  radius: 0.35;
  translate: var(--x) 0.35 0;
  color: #3a7bff;
  animation: rise 1s ease-out;
  animation-timeline: view();
}

sphere:nth-child(2) {
  animation-iteration-count: 2;
  animation-direction: alternate;
}

@keyframes rise {
  to {
    translate: var(--x) 1.6 0;
    color: #3ad16b;
  }
}

animation-timing-function

The easing of each step of the animation, like CSS. It wins over the easing written in animation.

animation-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
Syntax
<easing>
Initial value
linear
Applies to
the scene, objects and groups
Animatable
no

Values

linear
A constant speed: the default.
ease
Speeds up quickly, then slows down.
ease-in, ease-out, ease-in-out
Starts slowly, ends slowly, or both.
step-start, step-end
One jump, at the start or at the end.
cubic-bezier(), linear()
A curve of your own.
steps()
Moves by equal jumps.

slowing down at the end

@scene {
  sphere;
}

sphere {
  radius: 0.5;
  color: #3a7bff;
  animation: rise 1.5s ease-in-out;
  animation-timing-function: ease-out;
}

@keyframes rise {
  from {
    translate: 0 0.5 0;
  }
  to {
    translate: 0 2 0;
  }
}

transition

Glides the object to its :hover state and back, instead of jumping, like CSS. One transition covers every property that :hover changes on the object.

cube { transition: 0.4s ease-out; }
Syntax
none | [all] <time> [<easing>] [<time>]
Initial value
none
Applies to
objects
Animatable
no

Values

<time>
The first time is the duration, the second one the delay.
<easing>
ease by default, or any easing: a keyword, cubic-bezier(), linear() or steps().

Like CSS, the object enters :hover with the transition of its :hover rule, if it has one, and leaves it with the transition written at rest. Leaving halfway goes back from where it is, in the time already spent.

a hover that glides

Scale, turn and color glide in 0.4 s.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
  color: #ff5a36;
  transition: 0.4s ease-out;
}

cube:hover {
  scale: 1.3;
  rotate-y: -45deg;
  color: #3a7bff;
}

one way in, another out

The spheres rise quickly with ease-out, and fall back with an overshoot.

@scene {
  sphere * 5;
}

sphere {
  radius: 0.35;
  translate: calc(2.7 - sibling-index() * 0.9) 0.4 0;
  color: #3ad16b;
  transition: 0.8s cubic-bezier(0.3, -0.4, 0.7, 1.4);
}

sphere:hover {
  translate: calc(2.7 - sibling-index() * 0.9) 1.4 0;
  transition: 0.4s ease-out;
}

Easings

cubic-bezier()

An easing curve, like CSS, for animation and transition. The curve goes from (0, 0) to (1, 1), pulled by two handles: x is the time, from 0 to 1, and y the progress, which can go below 0 or above 1 to overshoot.

animation-timing-function: cubic-bezier(0.3, -0.4, 0.7, 1.4);
Syntax
cubic-bezier(<x1>, <y1>, <x2>, <y2>)
Computed
at compile time, once per object

Keywords

ease
cubic-bezier(0.25, 0.1, 0.25, 1)
ease-in
cubic-bezier(0.42, 0, 1, 1)
ease-out
cubic-bezier(0, 0, 0.58, 1)
ease-in-out
cubic-bezier(0.42, 0, 0.58, 1)

an overshoot

y goes below 0, then above 1: the sphere crouches, then jumps past the top.

@scene {
  sphere;
}

sphere {
  translate: 0 0.5 0;
  radius: 0.5;
  color: #ff5a36;
  animation: jump 1.2s cubic-bezier(0.3, -0.4, 0.7, 1.4) alternate;
}

@keyframes jump {
  to {
    translate: 0 2 0;
  }
}

linear()

An easing drawn as straight segments, like CSS: each number is the progress at one moment, a percentage of the duration. With enough points, it draws bounces and springs.

animation-timing-function: linear(0, 1 40%, 0.75 55%, 1 70%, 0.95 80%, 1);
Syntax
linear(<number> [<percentage>{0,2}], …)
Computed
at compile time, once per object

A missing moment is spread evenly between its neighbours; the first point is at 0% and the last one at 100%. The keyword linear is linear(0, 1): a constant speed.

More details

Two percentages on one number hold it still between them, and two points at the same moment jump.

a bounce

The sphere falls and bounces twice before it rests.

@scene {
  sphere;
}

sphere {
  radius: 0.4;
  color: #3a7bff;
  animation: drop 2s linear(0, 1 40%, 0.75 55%, 1 70%, 0.95 80%, 1);
}

@keyframes drop {
  from {
    translate: 0 3 0;
  }
  to {
    translate: 0 0.4 0;
  }
}

steps()

An easing that moves by equal jumps instead of gliding, like CSS: steps(4) holds still, then jumps, four times. Good for ticking hands, sprites and anything mechanical.

animation-timing-function: steps(4);
Syntax
steps(<integer>, [jump-start | jump-end | jump-none | jump-both | start | end]?) | step-start | step-end
Computed
at compile time, once per object

Positions

jump-end, end
The default: a jump at the end of each step, so the last value comes only at the very end.
jump-start, start
A jump at the start of each step.
jump-both
A jump at both ends.
jump-none
The first and the last values each hold for a whole step.
step-start, step-end
steps(1, jump-start) and steps(1, jump-end).

a ticking hand

Twelve jumps per turn, like the hand of a clock.

@scene {
  cube;
}

cube {
  size: 1.2 0.15 0.15;
  translate: 0 0.6 0;
  color: #ff5a36;
  animation: tick 6s steps(12);
}

@keyframes tick {
  from {
    rotate-y: 0deg;
  }
  to {
    rotate-y: -360deg;
  }
}

step-start and step-end

The same blink, jumping at the start or at the end.

@scene {
  sphere#a;
  sphere#b;
}

sphere {
  radius: 0.4;
}

#a {
  translate: 0.8 0.5 0;
  color: #3a7bff;
  animation: blink 1s step-start;
}

#b {
  translate: -0.8 0.5 0;
  color: #3ad16b;
  animation: blink 1s step-end;
}

@keyframes blink {
  50% {
    scale: 1.6;
  }
}

Camera

camera-angle

Sets where the camera starts around its target: first the angle around the vertical axis (0deg in front, on +z; 90deg on the right, on +x), then the height above the horizon, from 0deg to 80deg. Dragging with the mouse changes it.

camera-angle: 40deg 25deg;
Syntax
<angle> <angle>
Initial value
0deg 22.9deg
Applies to
the scene
Animatable
no

from the front left, high above

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
}

scene {
  camera-angle: -45deg 60deg;
}

camera-distance

Sets the distance between the camera and its target at the start, from 3 to 15. The mouse wheel changes it.

camera-distance: 4;camera-distance: 12;
Syntax
<number>
Initial value
8
Applies to
the scene
Animatable
no

closer

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
}

scene {
  camera-distance: 5;
}

camera-spin

Sets how long the camera takes to turn once around its target, in s or ms. none, the default, keeps it still.

camera-spin: 12s;
Syntax
<time> | none
Initial value
none
Applies to
the scene
Animatable
no

one turn in 40 s

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
}

scene {
  camera-spin: 40s;
}

camera-target

Sets the point the camera looks at and turns around.

camera-target: 0 0.5 0;camera-target: 0 1.5 0;
Syntax
<number>{3}
Initial value
0 0.5 0
Applies to
the scene
Animatable
no

looking higher

The cube is 2 units up, and so is the target.

@scene {
  cube;
}

cube {
  translate: 0 2 0;
}

scene {
  camera-target: 0 2 0;
}

controls

The gestures that move the camera: orbit, a drag turns it around its target, and zoom, the wheel brings it closer. none keeps the camera where the GSS puts it, and the page scrolls over the scene. On an object, the gestures that cannot start over it.

  1. controls: orbit;
  2. controls: zoom;
Syntax
auto | none | [ orbit || zoom ]
Initial value
auto
Applies to
the scene, objects and groups
Animatable
no

Values

auto
Both gestures, the default: a drag turns the camera, the wheel zooms.
none
Neither: the camera stays where the GSS puts it, and the wheel scrolls the page.
orbit
A drag turns the camera around its target.
zoom
The wheel brings the camera closer or takes it farther, from 3 to 15.

On scene, it says which gestures move the camera. On an object or a group, it says which gestures can start over it: a button with controls: none is pressed without turning the camera.

More details

CSS has no camera, so GSS has a property of its own for it. Around a button with controls: none, a drag from the floor still turns the camera. A gesture moves the camera when the scene and the object under the pointer both take it. Like cursor, it is inherited from the groups; it follows @media, not :hover. On a page, <gss-scene controls="none"> and mount(canvas, scene, { controls: false }) turn every gesture off, whatever the GSS says. camera-spin still turns the camera.

a still camera

The camera stays where the GSS puts it: a drag does nothing, and the wheel scrolls the page.

@scene {
  torus;
}

scene {
  controls: none;
  camera-angle: 30deg 30deg;
}

torus {
  translate: 0 0.8 0;
  rotate-x: 60deg;
  radius: 0.6;
  thickness: 0.2;
  color: #ff5a36;
}

turn, but no zoom

A drag turns the camera; the wheel scrolls the page.

@scene {
  cube;
}

scene {
  controls: orbit;
}

cube {
  translate: 0 0.5 0;
  rotate-y: 30deg;
  color: #3a7bff;
}

a button that holds still

Press the orange button and drag: it sinks, and the camera stays. From the floor, a drag still turns it.

@scene {
  cube#base;
  cube#button;
}

#base {
  size: 1.6 0.2 1.6;
  translate: 0 0.1 0;
  color: #2a2a33;
}

#button {
  size: 1 0.3 1;
  translate: 0 0.35 0;
  color: #ff5a36;
  cursor: pointer;
  controls: none;
  transition: 0.15s;
}

#button:active {
  translate: 0 0.25 0;
}

Lighting and fog

light (sun)

Sets the sun of the scene: its direction, then if needed its color and its intensity, in any order. The default lights the scene from the upper left, in front.

light: -45deg 54.7deg;
Syntax
none | <angle> <angle> [ <color> || <number> ]?
Initial value
-45deg 54.7deg
Applies to
the scene
Animatable
yes

Values

<angle> <angle>
The direction: the azimuth around the vertical axis (0deg points to +z, 90deg to +x), then the elevation above the horizon (90deg is straight overhead).
<color>
The color of the sun, white by default.
<number>
Its intensity, 1 by default.
none
No sun: the scene is lit by its point lights and its ambient light.

With animation on the scene, the sun can turn, change color, or rise from none.

a low sun from behind

-120deg 30deg: from behind, on the left, low over the horizon.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
}

scene {
  light: -120deg 30deg;
}

a warm sun

An orange sun, low, with bluish shadows from ambient.

@scene {
  sphere;
}

sphere {
  translate: 0 0.6 0;
  radius: 0.6;
}

scene {
  light: -60deg 25deg #ffb36b 1.2;
  ambient: 0.2 #6b8cff;
}

no sun

none, and a point light instead.

@scene {
  sphere;
  light#lamp;
}

sphere {
  translate: 0 0.6 0;
  radius: 0.6;
}

#lamp {
  translate: 1 1.5 1;
  intensity: 2.5;
}

scene {
  light: none;
  ambient: 0.05;
}

a day

@keyframes on the scene move the sun from dawn to noon.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
}

scene {
  animation: day 6s ease-in-out infinite alternate;
}

@keyframes day {
  from {
    light: -80deg 5deg #ff8a5c 0.6;
  }
  to {
    light: 60deg 70deg #ffffff 1;
  }
}

light (point)

Not a shape: a point of light, which draws nothing. Its color is the color of its light (white by default), and intensity how much light it gives, on top of the sun and the ambient light of the scene.

translate: -0.8 1.35 0;translate: 0.8 1.35 0;
Takes
display, color, intensity, translate, rotate-x, rotate-y, rotate-z, transform-origin, transition, animation, animation-duration, animation-delay, animation-iteration-count, animation-direction, animation-fill-mode, animation-timing-function, animation-timeline, animation-range, animation-range-start, animation-range-end, offset-path, offset-distance, offset-rotate

A light moves like an object, and is never drawn: to see the bulb, put a shape at its place. A scene takes 8 lights at most.

More details

What moves a light: translate, the groups it is in, animations, :hover through its group, a motion path, and rotations around a transform-origin, in numbers, since a light has no size. Lights add up. A light cannot be hovered or pressed: hover the shape at its place, like #lamp:hover light. Without shadows, the light goes through the objects.

a lamp

A warm bulb, with a faint ambient light and no sun.

@scene {
  light#bulb;
  sphere;
}

scene {
  light: none;
  ambient: 0.05;
}

#bulb {
  translate: 0.9 1.6 0.9;
  color: #ffd27a;
  intensity: 2;
}

sphere {
  translate: 0 0.6 0;
  radius: 0.6;
}

colored lights

A red and a blue light, one on each side of a white cube.

@scene {
  light#warm;
  light#cold;
  cube;
}

scene {
  light: none;
  ambient: 0.05;
}

#warm {
  translate: -1.4 1.4 1;
  color: #ff5a36;
  intensity: 3;
}

#cold {
  translate: 1.4 1.4 1;
  color: #3a7bff;
  intensity: 3;
}

cube {
  translate: 0 0.5 0;
  color: #ffffff;
}

a light that moves

A firefly that circles the sphere, around its transform-origin.

@scene {
  light#firefly;
  sphere;
}

scene {
  light: none;
  ambient: 0.05;
}

#firefly {
  translate: 1.2 0.8 0;
  transform-origin: -1.2 0 0;
  color: #b6ff6b;
  intensity: 1.5;
  animation: circle 4s linear;
}

sphere {
  translate: 0 0.6 0;
  radius: 0.5;
}

@keyframes circle {
  to {
    rotate-y: 1turn;
  }
}

a lamp to hover

Hovering the lamp turns its light up.

@scene {
  light#bulb;
  group#lamp {
    sphere#shade;
    cube#stand;
  }
}

scene {
  light: none;
  ambient: 0.15;
}

#shade {
  translate: 0 1.5 0;
  radius: 0.3;
  color: #ffd27a;
}

#stand {
  translate: 0 0.6 0;
  size: 0.1 1.2 0.1;
}

#bulb {
  translate: 0 1.1 0.5;
  intensity: 0.2;
  transition: 0.4s;
}

#bulb:has(+ #lamp:hover) {
  intensity: 3;
}

intensity

How much light a point light gives: what a surface facing it receives at 1 unit, then less with the square of the distance, a quarter at 2 units and a ninth at 3. 0 turns it off.

  1. intensity: 0.5;
  2. intensity: 1;
  3. intensity: 4;
Syntax
<number>
Initial value
1
Applies to
light
Animatable
yes

a strong lamp

An intensity of 4, two units above the sphere.

@scene {
  light;
  sphere;
}

scene {
  light: none;
  ambient: 0.05;
}

light {
  translate: 0 2 1;
  intensity: 4;
}

sphere {
  translate: 0 0.6 0;
  radius: 0.6;
}

ambient

Sets the light received by the surfaces the sun does not reach, from 0 (black shadows) to 1 (no shadows). A color can follow, for the light of the sky in the shadows: ambient: 0.2 #9db4ff gives bluish shadows under a warm sun.

ambient: 0.1;ambient: 0.6;
Syntax
<number> <color>?
Initial value
0.1
Applies to
the scene
Animatable
no

lighter shadows

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
}

scene {
  ambient: 0.4;
}

shadows

Lets the objects cast shadows, on the floor and on each other: a point in the shadow of an object keeps only the ambient light.

  1. shadows: none;
  2. shadows: hard;
  3. shadows: soft;
Syntax
none | hard | soft
Initial value
none
Applies to
the scene
Animatable
no

Values

none
No shadow: the default.
soft
A penumbra that grows with the distance to the object that casts it, like the shadow of the sun.
hard
A sharp edge.

The sun and every point light cast shadows, and the holes of mask-image let the light through. Each light costs one more ray per pixel. The reflections and the refractions show the objects without shadows.

soft shadows

The shadows of the sun blur as they get farther from the objects.

@scene {
  sphere;
  cube;
}

scene {
  shadows: soft;
  light: -30deg 50deg;
}

sphere {
  translate: -0.6 0.9 0;
  radius: 0.5;
  color: #ff5a36;
}

cube {
  translate: 0.7 0.4 0.3;
  size: 0.8;
  color: #3a7bff;
}

hard shadows from a lamp

A lamp and no sun: sharp shadows.

@scene {
  light;
  sphere;
}

scene {
  shadows: hard;
  light: none;
  ambient: 0.15;
}

light {
  translate: 0.8 2.4 0.6;
  intensity: 4;
  color: #ffd27a;
}

sphere {
  translate: 0 0.7 0;
  radius: 0.6;
}

light through holes

The holes of a mask let the sun through.

@scene {
  sphere;
}

scene {
  shadows: soft;
  light: 20deg 70deg;
}

sphere {
  translate: 0 1.2 0;
  radius: 0.9;
  mask-image: noise(4 3, black 48%, transparent 52%);
}

fog

Fills the scene with fog, measured from the camera: none before the first distance, only fog after the second one, and more and more of it between. Without a color, each object fades into the background behind it.

fog: none;fog: 6 14;
Syntax
none | [ <color> ]? <number> <number> [ <color> ]?
Initial value
none
Applies to
the scene
Animatable
yes

Values

<number> <number>
Where the fog starts, and where it hides everything.
<color>
Its color, before or after the distances. The background takes it too: past the fog, only the fog is seen.
none
No fog: the default.

The scene draws nothing farther than 20 units from the camera: a fog that ends before that hides the far edge of the floor. The scene can animate its fog, from none too.

More details

The reflections and the refractions show the objects without fog.

into the background

The cubes melt into the background as they go away.

@scene {
  cube * 7;
}

cube {
  size: 0.6;
  translate: calc((sibling-index() - 4) * -1.1) 0.3 calc(sibling-index() * -1.6);
  color: #ff5a36;
}

scene {
  background: #c9d6e3;
  fog: 3 13;
}

a colored fog

A warm fog, whose color the background takes.

@scene {
  sphere * 5;
}

sphere {
  radius: 0.4;
  translate: 0 0.4 calc(sibling-index() * -2);
  color: #3a7bff;
}

scene {
  fog: #e8e0d4 2 11;
  camera-angle: 20deg 12deg;
}

the fog rolls in

The fog rolls in, from none.

@scene {
  cylinder * 9;
}

cylinder {
  radius: 0.15;
  height: 1.6;
  translate: calc((sibling-index() - 5) * -0.9) 0.8 calc(sibling-index() * -1.2);
  color: #e6e6e6;
}

scene {
  background: #1a1d2b;
  floor: #2a2e40;
  fog: none;
  animation: roll 6s ease-in-out infinite alternate;
}

@keyframes roll {
  to {
    fog: 1 7;
  }
}

Rendering

dpr

Sets the pixel density of the render, like the device pixel ratio of the screen.

  1. dpr: 1;
  2. dpr: 2;
Syntax
auto | max | <number>
Initial value
auto
Applies to
the scene
Animatable
no

Values

auto
The density of the screen, up to 2: the default.
max
The density of the screen, however high: the sharpest render, and the slowest.
<number>
From 0.25 to 4, never above the screen's. Below 1, the render is coarser and faster.

as sharp as the screen

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
}

scene {
  dpr: max;
}

half the density

Coarser, and faster to draw.

@scene {
  cube;
}

cube {
  translate: 0 0.5 0;
}

scene {
  dpr: 0.5;
}

shape-rendering

Smooths the silhouettes of the objects, without raising the pixel density of the render.

  1. shape-rendering: auto;
  2. shape-rendering: geometricPrecision;
Syntax
auto | geometricPrecision
Initial value
auto
Applies to
the scene
Animatable
no

Values

auto
The default: each pixel shows one surface, and silhouettes keep their steps.
geometricPrecision
A pixel that the edge of an object only partly covers blends the object with what is behind it: the silhouettes come out smooth.

Like shape-rendering in SVG, but on the scene only, with two values. Only silhouettes are smoothed: the edges of a texture, of a shadow or of the floor keep their steps.

More details

auto keeps the sharp edges GSS has always drawn. The smoothing comes from the ray each pixel already casts, not from more rays or a bigger render.

smooth silhouettes

The sphere keeps one ray per pixel; only its grazing edge gets partial coverage.

@scene {
  sphere;
}

scene {
  shape-rendering: geometricPrecision;
  floor: none;
}

sphere {
  radius: 1;
}

view

Shows the scene as it is lit, or with the isolines of its distance field over it: the distance to the nearest object, which the shader measures at every step of every ray.

  1. view: shaded;
  2. view: distance;
Syntax
shaded | distance
Initial value
shaded
Applies to
the scene
Animatable
no

Values

shaded
The scene, lit: the default.
distance
The same scene, with a line every 0.25 units from the objects, fainter as it goes away. The lines lie on the plane that faces the camera through camera-target, and show where that plane is in front of the objects.

The floor is left out of the distance: the lines go around the objects only.

More details

Far from a path, a prism or a lathe, the distance is the one to its box, which the shader uses to skip it quickly, and the lines show that box. The lines are light on a dark scene and dark on a light one.

the distance around a sphere

Rings around the sphere, one every 0.25.

@scene {
  sphere;
}

sphere {
  translate: 0 0.5 0;
}

scene {
  view: distance;
}

on a dark scene

Without a floor, on a dark background: the lines of the field around two objects meet halfway.

@scene {
  sphere;
  cube;
}

sphere {
  translate: -0.8 0.5 0;
  color: #ff5a36;
}

cube {
  translate: 0.8 0.5 0;
  color: #e8e6e1;
}

scene {
  view: distance;
  floor: none;
  background: #0a0a0c;
}