A CSS 3D engine for the DOM. Renders polygon meshes in HTML by leveraging matrix3d transforms.
See the codeA CSS polygon mesh library. A 3D engine for the DOM. Renders OBJ/MTL, STL, glTF/GLB, and VOX as real HTML elements transformed with CSS matrix3d(...). Supports colors, textures, lighting, shadows, shapes and animations. Works with React, Vue or plain JavaScript.
Visit polycss.com for docs and model examples.
Join chat.polycss.com for support and community discussions.
# Vanilla
npm install @layoutit/polycss
# React
npm install @layoutit/polycss-react
# Vue
npm install @layoutit/polycss-vue
You can also load PolyCSS directly from a CDN. Here is a minimal custom-element scene:
<script type="module" src="https://esm.sh/@layoutit/polycss/elements"></script>
<poly-camera rot-x="65" rot-y="45">
<poly-scene>
<poly-orbit-controls drag wheel></poly-orbit-controls>
<poly-box size="100" color="#ffd166"></poly-box>
</poly-scene>
</poly-camera>
React and Vue expose the same component model. <PolyCamera> owns the viewpoint, <PolyScene> owns lighting and options, and <PolyMesh> loads or receives polygon data.
import { PolyCamera, PolyScene, PolyOrbitControls, PolyMesh } from "@layoutit/polycss-react";
export default function App() {
return (
<PolyCamera rotX={65} rotY={45}>
<PolyScene textureLighting="dynamic">
<PolyOrbitControls drag wheel />
<PolyMesh src="/gallery/obj/cottage.obj" mtl="/gallery/obj/cottage.mtl" />
</PolyScene>
</PolyCamera>
);
}
rotX, rotY control the orbit angle in degrees.zoom scales the projected scene.target pans the camera target in world coordinates.distance adds dolly pull-back.PolyCamera is the orthographic default. Use PolyPerspectiveCamera when you want perspective depth.polygons renders a static Polygon[] directly.directionalLight, pointLights (direction-only, baked mode; optional per-light castShadow), and ambientLight control scene lighting.textureLighting chooses "baked" or "dynamic".textureQuality controls atlas raster budget.strategies can disable selected render strategies for diagnostics.autoCenter rotates around the rendered mesh bounds instead of world origin.src loads .obj, .gltf, .glb, or .vox files.mtl loads companion OBJ materials.polygons accepts pre-parsed geometry.position, scale, and rotation transform the mesh wrapper.autoCenter shifts the mesh bbox center to local origin.meshResolution chooses "lossy" (default) or "lossless" optimization. STL imports use the conservative lossless path in both modes.castShadow emits CSS-projected shadows in dynamic lighting mode.<PolyOrbitControls> adds drag orbit, shift-drag pan, wheel zoom, and optional auto-rotate.<PolyMapControls> uses pan-first map-style input.<PolyFirstPersonControls> provides keyboard and pointer-look navigation.<PolyTransformControls> adds translate/rotate gizmos for selected mesh handles.The vanilla package exports exportPolySceneSnapshot(target). It clones the current rendered .polycss-camera / .polycss-scene DOM, injects only the PolyCSS CSS needed by that snapshot, inlines CSS url(...) image assets as data:image/...;base64,..., strips scripts and inline event handlers, and returns a standalone HTML document string with no PolyCSS runtime import. It works with rendered React/Vue scenes too; import it from @layoutit/polycss and pass the rendered camera or scene element.
import { exportPolySceneSnapshot } from "@layoutit/polycss";
const html = await exportPolySceneSnapshot(scene.host);
If any referenced asset cannot be inlined, the function throws PolySceneSnapshotError with code: "ASSET_INLINE_FAILED".
Use @layoutit/polycss-morph for models built ahead of time and updated through
a stable PolyCSS DOM graph.
Preparation runs in Node:
import { preparePolyMorphModel } from "@layoutit/polycss-morph/prepare";
await preparePolyMorphModel({
configPath: "./source/prepare.json",
outputRoot: "./public/model/package",
});
The browser entry loads, mounts, samples, and applies updates:
import {
createPolyMorphDeformationRuntime,
loadPolyMorphPackage,
mountPolyMorphModel,
} from "@layoutit/polycss-morph";
const loaded = await loadPolyMorphPackage("/model/");
const mounted = mountPolyMorphModel(host, loaded.model, {
resources: loaded.resources,
});
const deformation = createPolyMorphDeformationRuntime(loaded.model);
const frame = deformation.sample({
tick: 0,
morphWeights: { "corner-lift": 0.5 },
});
mounted.apply({ leaves: frame.leafUpdates });
See the PolyCSS Morph guide, including the interactive cube-to-sphere example.
Each polygon describes one renderable face:
const polygons = [
{
vertices: [[0, 0, 0], [60, 0, 0], [0, 60, 0]],
color: "#f97316",
},
{
vertices: [[0, 0, 0], [60, 0, 0], [60, 60, 0], [0, 60, 0]],
texture: "/texture.png",
uvs: [[0, 0], [1, 0], [1, 1], [0, 1]],
},
];
Render polygons directly when you need per-face DOM events or custom styling:
<PolyCamera>
<PolyScene>
{polygons.map((polygon, index) => (
<Poly
key={index}
{...polygon}
onClick={() => console.log("clicked polygon", index)}
className="my-polygon"
/>
))}
</PolyScene>
</PolyCamera>
Use loadMesh() to parse supported model formats:
import { createPolyCamera, createPolyScene, loadMesh } from "@layoutit/polycss";
const host = document.getElementById("polycss")!;
const camera = createPolyCamera({ rotX: 65, rotY: 45 });
const scene = createPolyScene(host, { camera });
const mesh = await loadMesh("https://polycss.com/gallery/obj/cottage.obj", {
mtlUrl: "https://polycss.com/gallery/obj/cottage.mtl",
});
scene.add(mesh);
Supported formats:
map_Kd textures and UV coordinates.TEXCOORD_0..vox, with direct voxel fast paths when eligible.PolyCSS renders through the DOM, so performance is mostly shaped by two things: the number of mounted leaves, and the amount of texture atlas area the browser has to paint. The renderer tries to keep the common cases cheap. Simple surfaces stay as solid CSS elements, while textured, irregular, or high-detail geometry falls back to atlas-backed slices only when needed.
Each visible polygon is emitted as one leaf element; the renderer chooses the least expensive CSS primitive that can represent the polygon, then uses matrix3d(...) to place that primitive in 3D space.
<b> uses background: currentColor on a fixed box for solid rectangles and stable quads.<u> uses corner-shape for stable triangles and beveled-corner solids, with a border-width triangle fallback when needed.<i> clips solid polygons with border-shape: polygon(...) when the browser supports it.<s> maps a packed texture-atlas slice with background-image, and is the fallback for textured or unsupported shapes.PolyCSS ships an agent skill: an entry-point SKILL.md plus a folder of
reference docs covering authoring invariants, scene setup, lighting, shadows,
textures, animation, performance, Three.js parity, and troubleshooting.
npx @layoutit/polycss-skills
It installs into .claude/skills/polycss/ and .agents/skills/polycss/,
detecting whichever your project already uses. Upgrades are content-hashed, so
re-running it refreshes the shipped docs and leaves your edits alone.
The same content is served at polycss.com/skill.md,
with the reference docs under polycss.com/skill/docs/, for agents that can
fetch a URL but not run a command.
cssQuake -> A CSS port of Quake (1996)
Layoutit Terra -> A CSS Terrain Generator
MIT.
(top 24 of 27)
6,061 followers · starred Jan 2026
3,743 followers · starred Dec 2025
788 followers · starred Jun 2026
1,325 followers · starred Jun 2026
A CSS 3D engine for the DOM. Renders polygon meshes in HTML by leveraging matrix3d transforms.
See the codeA CSS polygon mesh library. A 3D engine for the DOM. Renders OBJ/MTL, STL, glTF/GLB, and VOX as real HTML elements transformed with CSS matrix3d(...). Supports colors, textures, lighting, shadows, shapes and animations. Works with React, Vue or plain JavaScript.
Visit polycss.com for docs and model examples.
Join chat.polycss.com for support and community discussions.
# Vanilla
npm install @layoutit/polycss
# React
npm install @layoutit/polycss-react
# Vue
npm install @layoutit/polycss-vue
You can also load PolyCSS directly from a CDN. Here is a minimal custom-element scene:
<script type="module" src="https://esm.sh/@layoutit/polycss/elements"></script>
<poly-camera rot-x="65" rot-y="45">
<poly-scene>
<poly-orbit-controls drag wheel></poly-orbit-controls>
<poly-box size="100" color="#ffd166"></poly-box>
</poly-scene>
</poly-camera>
React and Vue expose the same component model. <PolyCamera> owns the viewpoint, <PolyScene> owns lighting and options, and <PolyMesh> loads or receives polygon data.
import { PolyCamera, PolyScene, PolyOrbitControls, PolyMesh } from "@layoutit/polycss-react";
export default function App() {
return (
<PolyCamera rotX={65} rotY={45}>
<PolyScene textureLighting="dynamic">
<PolyOrbitControls drag wheel />
<PolyMesh src="/gallery/obj/cottage.obj" mtl="/gallery/obj/cottage.mtl" />
</PolyScene>
</PolyCamera>
);
}
rotX, rotY control the orbit angle in degrees.zoom scales the projected scene.target pans the camera target in world coordinates.distance adds dolly pull-back.PolyCamera is the orthographic default. Use PolyPerspectiveCamera when you want perspective depth.polygons renders a static Polygon[] directly.directionalLight, pointLights (direction-only, baked mode; optional per-light castShadow), and ambientLight control scene lighting.textureLighting chooses "baked" or "dynamic".textureQuality controls atlas raster budget.strategies can disable selected render strategies for diagnostics.autoCenter rotates around the rendered mesh bounds instead of world origin.src loads .obj, .gltf, .glb, or .vox files.mtl loads companion OBJ materials.polygons accepts pre-parsed geometry.position, scale, and rotation transform the mesh wrapper.autoCenter shifts the mesh bbox center to local origin.meshResolution chooses "lossy" (default) or "lossless" optimization. STL imports use the conservative lossless path in both modes.castShadow emits CSS-projected shadows in dynamic lighting mode.<PolyOrbitControls> adds drag orbit, shift-drag pan, wheel zoom, and optional auto-rotate.<PolyMapControls> uses pan-first map-style input.<PolyFirstPersonControls> provides keyboard and pointer-look navigation.<PolyTransformControls> adds translate/rotate gizmos for selected mesh handles.The vanilla package exports exportPolySceneSnapshot(target). It clones the current rendered .polycss-camera / .polycss-scene DOM, injects only the PolyCSS CSS needed by that snapshot, inlines CSS url(...) image assets as data:image/...;base64,..., strips scripts and inline event handlers, and returns a standalone HTML document string with no PolyCSS runtime import. It works with rendered React/Vue scenes too; import it from @layoutit/polycss and pass the rendered camera or scene element.
import { exportPolySceneSnapshot } from "@layoutit/polycss";
const html = await exportPolySceneSnapshot(scene.host);
If any referenced asset cannot be inlined, the function throws PolySceneSnapshotError with code: "ASSET_INLINE_FAILED".
Use @layoutit/polycss-morph for models built ahead of time and updated through
a stable PolyCSS DOM graph.
Preparation runs in Node:
import { preparePolyMorphModel } from "@layoutit/polycss-morph/prepare";
await preparePolyMorphModel({
configPath: "./source/prepare.json",
outputRoot: "./public/model/package",
});
The browser entry loads, mounts, samples, and applies updates:
import {
createPolyMorphDeformationRuntime,
loadPolyMorphPackage,
mountPolyMorphModel,
} from "@layoutit/polycss-morph";
const loaded = await loadPolyMorphPackage("/model/");
const mounted = mountPolyMorphModel(host, loaded.model, {
resources: loaded.resources,
});
const deformation = createPolyMorphDeformationRuntime(loaded.model);
const frame = deformation.sample({
tick: 0,
morphWeights: { "corner-lift": 0.5 },
});
mounted.apply({ leaves: frame.leafUpdates });
See the PolyCSS Morph guide, including the interactive cube-to-sphere example.
Each polygon describes one renderable face:
const polygons = [
{
vertices: [[0, 0, 0], [60, 0, 0], [0, 60, 0]],
color: "#f97316",
},
{
vertices: [[0, 0, 0], [60, 0, 0], [60, 60, 0], [0, 60, 0]],
texture: "/texture.png",
uvs: [[0, 0], [1, 0], [1, 1], [0, 1]],
},
];
Render polygons directly when you need per-face DOM events or custom styling:
<PolyCamera>
<PolyScene>
{polygons.map((polygon, index) => (
<Poly
key={index}
{...polygon}
onClick={() => console.log("clicked polygon", index)}
className="my-polygon"
/>
))}
</PolyScene>
</PolyCamera>
Use loadMesh() to parse supported model formats:
import { createPolyCamera, createPolyScene, loadMesh } from "@layoutit/polycss";
const host = document.getElementById("polycss")!;
const camera = createPolyCamera({ rotX: 65, rotY: 45 });
const scene = createPolyScene(host, { camera });
const mesh = await loadMesh("https://polycss.com/gallery/obj/cottage.obj", {
mtlUrl: "https://polycss.com/gallery/obj/cottage.mtl",
});
scene.add(mesh);
Supported formats:
map_Kd textures and UV coordinates.TEXCOORD_0..vox, with direct voxel fast paths when eligible.PolyCSS renders through the DOM, so performance is mostly shaped by two things: the number of mounted leaves, and the amount of texture atlas area the browser has to paint. The renderer tries to keep the common cases cheap. Simple surfaces stay as solid CSS elements, while textured, irregular, or high-detail geometry falls back to atlas-backed slices only when needed.
Each visible polygon is emitted as one leaf element; the renderer chooses the least expensive CSS primitive that can represent the polygon, then uses matrix3d(...) to place that primitive in 3D space.
<b> uses background: currentColor on a fixed box for solid rectangles and stable quads.<u> uses corner-shape for stable triangles and beveled-corner solids, with a border-width triangle fallback when needed.<i> clips solid polygons with border-shape: polygon(...) when the browser supports it.<s> maps a packed texture-atlas slice with background-image, and is the fallback for textured or unsupported shapes.PolyCSS ships an agent skill: an entry-point SKILL.md plus a folder of
reference docs covering authoring invariants, scene setup, lighting, shadows,
textures, animation, performance, Three.js parity, and troubleshooting.
npx @layoutit/polycss-skills
It installs into .claude/skills/polycss/ and .agents/skills/polycss/,
detecting whichever your project already uses. Upgrades are content-hashed, so
re-running it refreshes the shipped docs and leaves your edits alone.
The same content is served at polycss.com/skill.md,
with the reference docs under polycss.com/skill/docs/, for agents that can
fetch a URL but not run a command.
cssQuake -> A CSS port of Quake (1996)
Layoutit Terra -> A CSS Terrain Generator
MIT.
(top 24 of 27)
6,061 followers · starred Jan 2026
3,743 followers · starred Dec 2025
788 followers · starred Jun 2026
1,325 followers · starred Jun 2026