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>
Download both files, put them next to your page, and link them. This works offline and with strict content security policies.
Download icons.js · Download cubes.js
<script src="icons.js"></script>
<script src="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" }} />
<script setup>
import { onMounted, onBeforeUnmount, ref } from "vue";
import { loadCubeIcons } from "./cube-icons";
const props = defineProps({ icon: { type: String, default: "rocket" }, options: Object, label: String });
const el = ref(null);
let stage;
onMounted(async () => {
const CubeIcons = await loadCubeIcons();
stage = CubeIcons.stage(el.value, props.options);
stage.show(props.icon);
});
onBeforeUnmount(() => stage?.destroy());
</script>
<template>
<canvas ref="el" role="img" :aria-label="label ?? icon" style="width:100%;height:280px" />
</template>
In Nuxt, wrap it in <ClientOnly>.
<script>
import { onMount } from "svelte";
import { loadCubeIcons } from "./cube-icons";
export let icon = "rocket", options = {}, label = icon;
let el;
onMount(() => {
let stage;
loadCubeIcons().then((CubeIcons) => { stage = CubeIcons.stage(el, options); stage.show(icon); });
return () => stage?.destroy();
});
</script>
<canvas bind:this={el} role="img" aria-label={label} style="width:100%;height:280px"></canvas>
Add an Embed (Insert → Utility → Embed), set it to HTML, and paste a complete snippet. The editor's "Drop-in snippet" gives you one for any icon, including your own.
<canvas id="cube" style="width:100%;height:100%" aria-label="A rocket"></canvas>
<script src="{{cdn}}icons.js"></script>
<script src="{{cdn}}cubes.js"></script>
<script>
CubeIcons.stage(document.getElementById("cube"), { look: "soft" }).show("rocket");
</script>
For a reusable Framer code component, use the React tab: Framer code components are React.
Add the two script tags under Page settings → Custom code → Before </body> tag (or Site settings, for every page). Then drop an Embed element where the icon goes:
<canvas id="cube" style="width:100%;height:280px" aria-label="A rocket"></canvas>
<script>
addEventListener("load", () => CubeIcons.stage(document.getElementById("cube")).show("rocket"));
</script>
Add a Custom HTML block and paste a complete snippet (canvas, scripts and the call). Some security plugins strip scripts from posts; if so, add the two script tags through your theme's footer settings or a header-and-footer plugin instead.
<canvas id="cube" style="width:100%;height:280px" aria-label="A rocket"></canvas>
<script src="{{cdn}}icons.js"></script>
<script src="{{cdn}}cubes.js"></script>
<script>CubeIcons.stage(document.getElementById("cube")).show("rocket");</script>
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....",
],
| Key | Colour | Key | Colour |
|---|
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:
| Look | What it is | Best on |
|---|---|---|
flat | Three clean shades per cube. The default. | Anything |
soft | Bevelled edges, warm light, cool shade and a shadow underneath. | Light pages |
clay | Pastel and matte, with rounded faces and a shadow. | Light pages |
candy | Saturated and glossy, with a highlight on every cube. | Light pages |
paper | Flat cut-outs with a drop shadow, like layered card. | Light pages |
outline | A dark edge round every face, like a comic. | Light pages |
pixel | Flat 2D pixel art: the front layer only. | Anything |
glass | See-through faces with bright edges. | Dark pages |
neon | Dark bodies with glowing edges. | Dark pages |
wire | Edges only, like a blueprint. Pair it with tint. | Dark or coloured pages |
holo | A 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.
| Option | Default | What 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". |
depth | 2 | Cubes deep, 1–5. |
gap | 0 | Space between cubes, 0–0.4 of a cube. |
tint | null | One ink: every colour redrawn in shades of this one, e.g. "#2f3cff". |
palette | PALETTE | Colours by key, as rgb() or hex. |
shadow | on for soft, clay and candy | A soft contact shadow under the icon. true or false to force it. |
duration | 700 | Ms each cube takes to reach its spot. |
ease | "expo" | One of CubeIcons.EASES: expo, cubic, back, elastic, linear. |
stagger | 260 | Up to this many ms of random delay per cube. |
spin | 0.25 | How far cubes turn while flying, in turns. 0 for none. |
from | "auto" | Where hidden cubes fly in from: below, above, left, right or center. |
magnet | 0 | How far cubes near the pointer pop toward you, in cubes (try 1.5). |
glow | 0 | Bloom: soft light around the cubes, 0–1. Lovely with tint on a dark page. Neon glows by default. |
float | 0 | Idle motion, 0–1: the built icon sways and bobs; loose cubes from scatter() drift. |
tilt | true | The depth leans with the mouse. |
size | "auto" | Pixels per cube, or fit the canvas. |
x, y | 0.5 | Where the icon sits in the canvas, 0–1. |
max | 900 | How many cubes the stage holds. |
API
Stage
| Call | What 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.options | The current options. |
stage.destroy() | Stops it and removes its listeners. Call it when you remove the canvas. |
Helpers
| Call | What 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, version | The 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 anaria-labelto the canvas. Mark decorative ones witharia-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.jsloads before you callshow()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.