Documentation

Get started

Cube Icons turns small pixel drawings into 3D cubes that fly in, settle and fly apart. It's one script with no dependencies: add it to a page, give it a canvas, and name an icon.

It comes with 61 icons, eleven looks, twenty-eight recipes, a scroll mode, an editor that also turns images into icons, and a Claude skill. It works in any website: plain HTML, React, Vue, Framer, Webflow or WordPress.

Requirements

  • A web page you can add a script to. No build step, no npm, no framework required.
  • A current browser: Chrome, Edge, Firefox or Safari from the last few years. It uses a plain 2D canvas, not WebGL.
  • Nothing else. No account, no API key, and nothing is sent anywhere: everything runs in the visitor's browser.

The engine is about 9 KB gzipped, and the icon library about 3 KB.

Installation

Pick one. Both give you the same two files: icons.js (the icon library) and cubes.js (the engine).

Add these before the closing </body> tag. The version in the URL is pinned, so an update never changes your site by surprise.

<script src="{{cdn}}icons.js"></script>
<script src="{{cdn}}cubes.js"></script>

Only drawing your own icons, or using CubeIcons.text()? You can skip icons.js and pass your rows straight to show().

Quick start

Add the scripts

From Installation, above.

Add a canvas with a size

The canvas needs a width and height in CSS. The icon scales to fit it.

<canvas id="hero-icon" style="width:360px;height:280px" aria-label="A rocket"></canvas>

Build an icon

Make a stage on the canvas and show an icon by name.

<script>
  const stage = CubeIcons.stage(document.getElementById("hero-icon"), { look: "soft" });
  stage.show("rocket");
</script>

That's all. Here is the whole page in one block, ready to save as an .html file and open:

<!doctype html>
<html lang="en">
<body style="margin:0;display:grid;place-items:center;min-height:100vh;background:#efe9df">
  <canvas id="hero-icon" style="width:360px;height:280px" aria-label="A rocket"></canvas>

  <script src="{{cdn}}icons.js"></script>
  <script src="{{cdn}}cubes.js"></script>
  <script>
    const stage = CubeIcons.stage(document.getElementById("hero-icon"), { look: "soft" });
    stage.show("rocket");
  </script>
</body>
</html>

Next, shape it in the playground: every setting is there, and its code box gives you the matching line to paste. The recipes show twenty-eight common uses.

Frameworks

The engine is a plain script that adds window.CubeIcons. In a framework, load it once in the browser, then make a stage when your component mounts and destroy it when it unmounts. This small loader does the first part and works everywhere:

// cube-icons.js: loads the two scripts once, in order, and resolves with window.CubeIcons
const BASE = "{{cdn}}";
let loading;
export function loadCubeIcons() {
  if (window.CubeIcons) return Promise.resolve(window.CubeIcons);
  loading ??= Promise.all(["icons.js", "cubes.js"].map((file) => new Promise((resolve, reject) => {
    const s = document.createElement("script");
    s.src = BASE + file; s.async = false; s.onload = resolve; s.onerror = reject;
    document.head.append(s);
  }))).then(() => window.CubeIcons);
  return loading;
}
"use client"; // Next.js App Router: this runs in the browser only
import { useEffect, useRef } from "react";
import { loadCubeIcons } from "./cube-icons";

export function CubeIcon({ icon = "rocket", options = {}, label, style }) {
  const ref = useRef(null);
  const opts = JSON.stringify(options);
  useEffect(() => {
    let stage, cancelled = false;
    loadCubeIcons().then((CubeIcons) => {
      if (cancelled) return;
      stage = CubeIcons.stage(ref.current, JSON.parse(opts));
      stage.show(icon);
    });
    return () => { cancelled = true; stage?.destroy(); };
  }, [icon, opts]);
  return <canvas ref={ref} role="img" aria-label={label ?? icon} style={{ width: "100%", height: 280, ...style }} />;
}

// <CubeIcon icon="trophy" options={{ look: "soft" }} />

Icons

Show any of these by name. They're all in icons.js; see them built in the playground.

An icon is just rows of text, read top to bottom as you'd draw it. Each character is a colour from the palette and . is empty. Every coloured cell becomes a short stack of cubes, depth deep.

heart: [
  ".RR...RR.",
  "RRRR.RRRR",
  "RRRRRRRRR",
  ".RRRRRRR.",
  "..RRRRR..",
  "...RRR...",
  "....R....",
],
KeyColourKeyColour

Your own icons

Draw one, or start from an image

Open the editor. Paint cell by cell, or drop a PNG, JPG or SVG on the grid (or paste one). The background is removed, the image's own main colours are found (or snapped to the built-in palette, if you prefer), and each cell takes the colour most of its pixels share. Width (up to 48 cells), number of colours and detail are adjustable. Logos, emoji, stickers and flat illustrations convert best. The image never leaves your browser.

Then keep it: Save to My icons (stored in your browser and shown in the playground), download a PNG in the preview's look, a .js file to load after icons.js, or JSON; or copy an icons.js entry or a complete drop-in snippet.

Use it

Pass the rows straight to show(), or add them to the library and use them by name:

// straight in
stage.show(["..BB..", ".BBBB.", "BBBBBB", ".W..W."]);

// or by name, anywhere after icons.js
Object.assign(window.CUBE_ICONS, { badge: ["..BB..", ".BBBB.", "BBBBBB", ".W..W."] });
stage.show("badge");

Icons with their own colours

An icon can bring colours of its own: give it colors with extra keys (lowercase letters by convention), and use those keys in its rows. Images imported in the editor come out this way.

stage.show({
  colors: { a: "#eb234f", b: "#79a93e" },
  rows: ["..aa..", ".aaaa.", "bbbbbb"],
});

Words

CubeIcons.text() turns a word into rows with a built-in 5-row pixel font (A–Z, 0–9 and . , : ! ? - + ' & /; \n starts a new line).

stage.show(CubeIcons.text("Hello", { color: "R" }));

Drawing icons that read well

  • Keep them 8–16 cells wide. Bigger loses the chunky look, and each stage holds 900 cubes (cells × depth) unless you raise max.
  • Outline objects in one colour and fill with another; one accent detail (a gold clasp, a red flag) makes them read.
  • The front view suits detailed icons; isometric suits simple solid shapes.

Looks and motion

Change the look with one option. All eleven work with every icon, view and colour:

LookWhat it isBest on
flatThree clean shades per cube. The default.Anything
softBevelled edges, warm light, cool shade and a shadow underneath.Light pages
clayPastel and matte, with rounded faces and a shadow.Light pages
candySaturated and glossy, with a highlight on every cube.Light pages
paperFlat cut-outs with a drop shadow, like layered card.Light pages
outlineA dark edge round every face, like a comic.Light pages
pixelFlat 2D pixel art: the front layer only.Anything
glassSee-through faces with bright edges.Dark pages
neonDark bodies with glowing edges.Dark pages
wireEdges only, like a blueprint. Pair it with tint.Dark or coloured pages
holoA rainbow that shifts across the icon, and while it flies.Dark pages

Motion is timed by the clock, so it looks the same on every screen. Each cube flies for duration ms along an ease curve, after a random delay of up to stagger ms, turning by up to spin on the way and always landing square. The default curve, expo, starts fast and lands softly; back and elastic overshoot for a playful feel.

CubeIcons.stage(canvas, { look: "clay", ease: "back", duration: 900, stagger: 400, spin: 0.5 });

One-ink brand colour: tint redraws every icon in shades of your colour, keeping light parts light. To change single colours instead, pass a palette: { palette: { ...CubeIcons.PALETTE, B: "#ff4d8d" } }.

Scroll and interaction

// build as the canvas scrolls to the middle of the screen, come apart as it leaves
CubeIcons.scroll(stage, "rocket");
// or fly the whole icon in once half of it is in view
CubeIcons.scroll(stage, "rocket", { mode: "trigger" });

// one icon after another
CubeIcons.sequence(stage, ["home", "bag", "gift"], { every: 2000 });

// glowing cubes that drift across the section, then gather into an icon
stage.set({ tint: "#3cff3c", glow: 0.8, float: 0.7 });
stage.scatter(140, { icon: "bolt" });
button.addEventListener("click", () => stage.show("bolt"));

// a transition: the icon bursts toward you
stage.burst();

// knock the cubes loose from the click point; they settle back
canvas.addEventListener("click", (e) => stage.explode(e.clientX, e.clientY));

// a menu: build on hover, fly apart on leave
link.addEventListener("pointerenter", () => stage.show("briefcase"));
menu.addEventListener("pointerleave", () => stage.clear());

Options

Pass any of these to CubeIcons.stage(canvas, options) or CubeIcons.still(), or change them later with stage.set(). CubeIcons.DEFAULTS holds the defaults.

OptionDefaultWhat it does
look"flat"One of CubeIcons.LOOKS: flat, soft, clay, candy, paper, outline, pixel, glass, neon, wire, holo.
view"front""front" (square-on, depth as a slanted side) or "iso".
depth2Cubes deep, 1–5.
gap0Space between cubes, 0–0.4 of a cube.
tintnullOne ink: every colour redrawn in shades of this one, e.g. "#2f3cff".
palettePALETTEColours by key, as rgb() or hex.
shadowon for soft, clay and candyA soft contact shadow under the icon. true or false to force it.
duration700Ms each cube takes to reach its spot.
ease"expo"One of CubeIcons.EASES: expo, cubic, back, elastic, linear.
stagger260Up to this many ms of random delay per cube.
spin0.25How far cubes turn while flying, in turns. 0 for none.
from"auto"Where hidden cubes fly in from: below, above, left, right or center.
magnet0How far cubes near the pointer pop toward you, in cubes (try 1.5).
glow0Bloom: soft light around the cubes, 0–1. Lovely with tint on a dark page. Neon glows by default.
float0Idle motion, 0–1: the built icon sways and bobs; loose cubes from scatter() drift.
tilttrueThe depth leans with the mouse.
size"auto"Pixels per cube, or fit the canvas.
x, y0.5Where the icon sits in the canvas, 0–1.
max900How many cubes the stage holds.

API

Stage

CallWhat it does
CubeIcons.stage(canvas, options)Makes a stage on a canvas and returns it.
stage.show(icon)Flies the cubes in to build an icon: a name, or an array of rows. Showing another icon morphs into it.
stage.clear()Flies the cubes apart and fades them out.
stage.build(icon, progress)Builds part of an icon, 0 (scattered) to 1 (whole), bottom row first. Drive it from anything: a scroll, a timer, an upload.
stage.explode(x?, y?, force?)Knocks the cubes away from a page point (or the middle); they settle back.
stage.burst({ force, dur })The cubes rush toward you, growing and fading. A page or menu transition; nothing is left showing.
stage.scatter(count, { icon, color, alpha, size })Loose cubes spread across the canvas (they drift with float). A following show() gathers them into the icon.
stage.set(options)Changes options live.
stage.optionsThe current options.
stage.destroy()Stops it and removes its listeners. Call it when you remove the canvas.

Helpers

CallWhat it does
CubeIcons.scroll(stage, icon, { mode, target, hold, range })Ties a stage to the scroll. mode is "scrub" (default) or "trigger". Returns { destroy }.
CubeIcons.sequence(stage, icons, { every })Shows icons one after another, every every ms (2000). Returns { destroy }.
CubeIcons.text(str, { color, spacing, line })Returns rows for a word in the built-in pixel font.
CubeIcons.still(canvas, icon, options)Draws an icon once, settled, with no animation. Good for thumbnails.
CubeIcons.voxels(icon, depth, palette)The cube positions and colours for an icon, if you want to render them yourself.
PALETTE, DEFAULTS, LOOKS, EASES, versionThe built-in values, for building your own controls.

Accessibility

  • Reduced motion is respected. When a visitor's system asks for less motion, icons appear in place with no flight, spin or stagger.
  • Label meaningful icons: add role="img" and an aria-label to the canvas. Mark decorative ones with aria-hidden="true".
  • Don't rely on the canvas alone for anything a visitor needs: keep the real text (a menu link, a button label) in HTML next to it.
  • Hover effects should also run on keyboard focus; the recipes show how.

Performance

  • A stage only draws while cubes move, when the pointer moves (for tilt and magnet) and when the canvas resizes. A settled icon costs nothing.
  • It pauses while the canvas is scrolled out of view, and carries on when it returns.
  • Sharpness is capped at 2× for high-density screens, which keeps big canvases fast.
  • Dozens of stages on one page are fine (the recipes page runs over twenty). Call destroy() on stages you remove.

Use with Claude

Cube Icons comes with a free Claude skill. With it, Claude knows the engine, the icon format and the recipes, so you can ask for things like "add a cube icon menu to my landing page" or "draw a coffee cup icon and put it in the empty state", and Claude writes the rows and the code.

Get the skill

Download the skill/cube-icons folder from the GitHub repo (it includes SKILL.md and the engine).

Add it

Claude Code: copy the folder to ~/.claude/skills/cube-icons (every project) or .claude/skills/cube-icons in one project. Claude apps: zip the folder and upload it under Settings → Capabilities → Skills.

Ask

Describe what you want. Claude uses the skill when the request fits.

Troubleshooting

Nothing shows

  • Give the canvas a size in CSS (a width and a height). A canvas with no height is 0 pixels tall.
  • Check the browser console. A wrong icon name logs there's no icon called "…".
  • Make sure icons.js loads before you call show() with a name.

CubeIcons is not defined

Your code ran before cubes.js loaded. Put your script after the two script tags, or run it on load, or use the loader from Frameworks.

Part of the icon is missing

It needs more cubes than the stage holds. The console says so; raise max, lower depth, or draw it smaller. The editor shows the count as you draw.

Blocked by a content security policy

Allow https://cdn.jsdelivr.net in script-src, or use the download install and serve the files yourself.

Server-side rendering errors (window is not defined)

The engine needs a browser. Load it inside an effect or mount hook, as in Frameworks, never at the top of a server-rendered module.

Licence and credits

Cube Icons is free and open source under the MIT licence: use it in personal and commercial projects, change it, and share it. Keep the licence notice in the files.

The idea of a cube swarm that builds an object per menu item was inspired by the menu on sparkk.fr, an agency site built with WebGL and Blender-made shapes. Cube Icons is an independent take: a dependency-free 2D canvas engine that builds its shapes from pixel art.

Related work worth a look: heerich.js (voxel scenes as SVG), claude-pxl (pixel art sprites with Claude), and the Three.js voxel tutorials on Codrops.

Want more icons, ready-made sections and framework components? Cube Icons Pro is coming.