Small, friendly 3D for the web. A WebGL 2 renderer with PBR materials, shadows, instancing, points, lines, labels, picking, glTF models and particles, behind an API small enough to learn in an afternoon - aimed at business apps: charts, labelled scenes, diagrams, dashboards, floor plans and product viewers. The demo gallery.
- Small: about 70 KB gzipped for a lit, interactive scene, and about 116 KB for everything; charts, particles, glTF, motion and editing are separate imports, and the heavier parts (shadows, HDR environments, GPU picking) load only when used.
- Interaction built in: tooltips, hover highlights, click and drag handlers, and HTML labels that hide when they overlap or go behind something - each an option or a line of code, not a layer to build.
- Idle when nothing changes: a view draws only when something in it changed, so a dashboard of many views only updates when needed.
- Any number of views on a page: browsers allow about 16 WebGL contexts per page, so most libraries need one big canvas and scissoring for a dashboard of tiles. phoria2 gives the first four views their own context and draws the rest with one shared context, each still an ordinary canvas in your layout. The chart lab demo runs over 100 live chart views.
- Tells you what to write: every option is checked, and a mistake throws an error that names
the fix - pass
colourand you getMaterial: use 'color'. Written to be easy for coding agents too. - Accessible:
keyboard: truelets people step through bars, points and parts with the arrow keys while a screen reader says what each one is; motion follows the system's reduced-motion setting. - Integrates well: physics, solid modelling and path finding can be added using libraries made for them - the gallery shows phoria2 rendering with Rapier, Manifold and recast-navigation.
- No runtime dependencies. ES modules, with TypeScript types.
New to phoria2? The getting started tutorial builds a first 3D scene and then a live dashboard of 3D charts, step by step - each step running beside its code, ready to copy as one HTML file.
Using a coding agent? Point it at llms-full.txt - the whole API
in one page (llms.txt is the short index). Both ship in the
package too, at node_modules/phoria2/llms-full.txt.
Claude Code: install the phoria2 plugin, and Claude reaches for phoria2 when asked for 3D charts, diagrams, explainers or dashboards - with the reference to hand and a check that loads the page in Chrome and reports what's wrong:
/plugin marketplace add kevinroast/phoria2
/plugin install phoria2@phoria2
The skill inside it (claude-plugin/skills/phoria2) follows the Agent Skills standard, so other agents that read skills can use the folder as it is.
npm install phoria2Or straight from a CDN, with no build step:
<script type="module">
import { View, Mesh, box } from 'https://cdn.jsdelivr.net/npm/phoria2/+esm'
</script>To use more than the core - charts, particles, glTF, motion or editing - map each import to the package's own
files, so they share one copy of the core (the CDN's ready-made +esm builds each bundle their
own). Pin the version so a new release can't change your page:
<script type="importmap">
{
"imports": {
"phoria2": "https://cdn.jsdelivr.net/npm/phoria2@1.2.0/dist/phoria2.js",
"phoria2/charts": "https://cdn.jsdelivr.net/npm/phoria2@1.2.0/dist/charts.js",
"phoria2/particles": "https://cdn.jsdelivr.net/npm/phoria2@1.2.0/dist/particles.js",
"phoria2/gltf": "https://cdn.jsdelivr.net/npm/phoria2@1.2.0/dist/gltf.js",
"phoria2/motion": "https://cdn.jsdelivr.net/npm/phoria2@1.2.0/dist/motion.js",
"phoria2/edit": "https://cdn.jsdelivr.net/npm/phoria2@1.2.0/dist/edit.js"
}
}
</script>
<script type="module">
import { View } from 'phoria2'
import { chart } from 'phoria2/charts'
</script><canvas id="c" style="width: 100%; height: 100vh; display: block"></canvas>
<script type="module" src="./main.js"></script>import { View, Mesh, box, DirectionalLight } from 'phoria2'
const view = new View({ canvas: '#c', orbit: true, background: '#1c1c20' })
const cube = view.scene.add(new Mesh({ geometry: box(2), material: { color: 'orange' } }))
view.scene.add(new DirectionalLight({ direction: [-1, -2, -1] }))
view.start((dt) => cube.rotateY(0.6 * dt))The canvas takes its size from CSS; the view matches its drawing buffer to it and follows resizes.
The camera starts fitted to the scene, and orbit: true lets the user turn, pan and zoom it. The
tutorial takes it on from here: materials, shadows,
labels, hover and click.
const view = new View({ canvas: '#c', orbit: true, highlight: '#ffffff',
tooltip: (hit) => ({ title: hit.datum.name, rows: { Sales: hit.datum.sales } }) })
view.on('click', (hit) => hit && select(hit.datum)) // hit is null for empty space
chair.onDrag = (drag) => drag.point && chair.position.copy(drag.point)A hit carries the node, the instance and its row of data, so a chart of 10,000 bars is one mesh
with one handler. Labels are HTML, styled with your CSS.
import { View } from 'phoria2'
import { chart } from 'phoria2/charts'
const view = new View({ canvas: '#c', orbit: true })
view.scene.add(chart({
mark: 'bar',
data: rows, // [{ month: 'Jan', region: 'North', sales: 41 }, ...]
x: 'month', z: 'region', y: 'sales',
color: 'region',
tooltip: true,
}))Marks: bar, point, line, area, surface and network, with axes, legends, labels, tooltips
and animated updates (chart.update(newRows)). A JSON schema for specs ships as
phoria2/charts/schema.json.
import { loadGLTF } from 'phoria2/gltf'
const model = await loadGLTF('models/chair.glb') // .glb or .gltf
view.scene.add(model)
view.frame(model)
model.play() // its first animationphoria2 draws; a library built for the job does the rest. Physics engines hand back positions and
{ x, y, z, w } rotations, which instances take as they are:
// each frame: Rapier moves the bricks, phoria2 draws them - one mesh, one draw call
world.step()
bodies.forEach((body, i) => bricks.setInstance(i, { position: body.translation(), quaternion: body.rotation() }))The gallery's Integrations section has four, each with its source: a brick tower knocked down with Rapier, a tablecloth simulated by Rapier's soft bodies, a block cut and joined with Manifold, and people finding their way round an office with recast-navigation.
const view = new View({ canvas: '#c', keyboard: true })The canvas joins the page's tab order. The arrow keys step through whatever can be hovered or
clicked - each bar, point, link or part - showing its highlight and tooltip, and Enter clicks it. A
screen reader hears each one ("North, Sales: 26. 1 of 48") and a description of the view, which
charts write for themselves. Glides, auto-rotation and chart transitions follow the system's
reduced-motion setting (reducedMotion to choose).
React owns the canvas; phoria2 draws on it. Make the view once, dispose of it on unmount, and change the scene in place when props change:
import { useEffect, useRef } from 'react'
import { View, Mesh, box } from 'phoria2'
export function SpinningBox({ color }) {
const canvas = useRef(null)
const mesh = useRef(null)
useEffect(() => {
const view = new View({ canvas: canvas.current, orbit: true })
mesh.current = view.scene.add(new Mesh({ geometry: box(2) }))
view.start((dt) => mesh.current.rotateY(0.6 * dt))
return () => view.dispose()
}, [])
useEffect(() => {
mesh.current.material.color.set(color)
}, [color])
return <canvas ref={canvas} style={{ width: '100%', height: 320 }} />
}A full example, with a chart that follows its data, is in
examples/react.
| Import | |
|---|---|
phoria2 |
the view, scene graph, cameras, lights, materials, geometry, curves, textures, picking, labels, bloom, create() from JSON |
phoria2/charts |
chart() - charts from a spec of mark, data and channels, and the scales they use |
phoria2/particles |
Emitter and Body |
phoria2/gltf |
loadGLTF() - glTF 2.0 models with their materials and animations |
phoria2/motion |
Timeline - named steps played in order, follow() along a curve, reveal() a path as it's travelled - for explainers and guided tours |
phoria2/edit |
TransformHandle - a move and turn handle with grid and angle snapping - and History for undo, for planners and layout editors |
phoria2/schema.json, phoria2/charts/schema.json |
JSON Schemas for scene descriptions (create()) and chart specs |
phoria2 follows semver from 1.0: no 1.x release breaks code written against what is documented.
- Covered: what the entry points above export, as the reference and the
published types describe it - names, options and their defaults, and what they do. The formats
create()reads andtoJSON()writes, and chart specs, as their JSON Schemas describe them. The conventions: +Y up, metres, radians, sRGB colour in, and light intensities - a scene keeps its colours and its brightness from one 1.x release to the next. - Not covered: exact pixels, since drawing can get better (smoother edges, softer shadows); the
wording of errors and warnings (catch
PhoriaError, not its message); modules imported fromphoria2/src/...directly, members left out of the types, and the shape ofNullRenderer.lastFrame; the demos. - Minor releases add: options, methods, node types, chart marks. A new option's default keeps what happened before.
- Nothing is removed or renamed in 1.x. What 2.0 will drop is deprecated first: a 1.x release keeps it working and warns once in the console, saying exactly what to write instead.
Any browser with WebGL 2: current Chrome, Edge, Firefox and Safari (desktop and iOS 15+).
The getting started tutorial walks through a first
scene and a chart dashboard. The demo gallery has charts and
diagrams, a demo for each feature, showcase scenes and integrations with other libraries; its
source, and the tutorial's, is in demos/.
MIT - see LICENSE. phoria2 is a rewrite of phoria, a 2013 library that drew 3D on the canvas 2D context.