Skip to content

Instantly share code, notes, and snippets.

@OrionReed
Last active July 13, 2026 17:34
Show Gist options
  • Select an option

  • Save OrionReed/4c3778ebc2b5026d2354359ca49077ca to your computer and use it in GitHub Desktop.

Select an option

Save OrionReed/4c3778ebc2b5026d2354359ca49077ca to your computer and use it in GitHub Desktop.
3D DOM viewer, copy-paste this into your console to visualise the DOM topographically.
// 3D Dom viewer, copy-paste this into your console to visualise the DOM as a stack of solid blocks.
// You can also minify and save it as a bookmarklet (https://www.freecodecamp.org/news/what-are-bookmarklets/)
(() => {
const SHOW_SIDES = false; // color sides of DOM nodes?
const COLOR_SURFACE = true; // color tops of DOM nodes?
const COLOR_RANDOM = false; // randomise color?
const COLOR_HUE = 190; // hue in HSL (https://hslpicker.com)
const MAX_ROTATION = 180; // set to 360 to rotate all the way round
const THICKNESS = 20; // thickness of layers
const DISTANCE = 10000; // ¯\\_(ツ)_/¯
function getRandomColor() {
const hue = Math.floor(Math.random() * 360);
const saturation = 50 + Math.floor(Math.random() * 30);
const lightness = 40 + Math.floor(Math.random() * 30);
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
const getDOMDepth = element => [...element.children].reduce((max, child) => Math.max(max, getDOMDepth(child)), 0) + 1;
const domDepthCache = getDOMDepth(document.body);
const getColorByDepth = (depth, hue = 0, lighten = 0) => `hsl(${hue}, 75%, ${Math.min(10 + depth * (1 + 60 / domDepthCache), 90) + lighten}%)`;
// Apply initial styles to the body to enable 3D perspective
const body = document.body;
body.style.overflow = "visible";
body.style.transformStyle = "preserve-3d";
body.style.perspective = DISTANCE;
const perspectiveOriginX = (window.innerWidth / 2);
const perspectiveOriginY = (window.innerHeight / 2);
body.style.perspectiveOrigin = body.style.transformOrigin = `${perspectiveOriginX}px ${perspectiveOriginY}px`;
traverseDOM(body, 0, 0, 0);
document.addEventListener("mousemove", (event) => {
const rotationY = (MAX_ROTATION * (1 - event.clientY / window.innerHeight) - (MAX_ROTATION / 2));
const rotationX = (MAX_ROTATION * event.clientX / window.innerWidth - (MAX_ROTATION / 2));
body.style.transform = `rotateX(${rotationY}deg) rotateY(${rotationX}deg)`;
});
// Create side faces for an element to give it a 3D appearance
function createSideFaces(element, color) {
if (!SHOW_SIDES) { return }
const width = element.offsetWidth;
const height = element.offsetHeight;
const fragment = document.createDocumentFragment();
// Helper function to create and style a face
const createFace = ({ width, height, transform, transformOrigin, top, left, right, bottom }) => {
const face = document.createElement('div');
face.className = 'dom-3d-side-face';
Object.assign(face.style, {
transformStyle: "preserve-3d",
backfaceVisibility: 'hidden',
position: 'absolute',
width: `${width}px`,
height: `${height}px`,
background: color,
transform,
transformOrigin,
overflow: 'hidden',
willChange: 'transform',
top,
left,
right,
bottom
});
fragment.appendChild(face);
}
// Top face
createFace({
width,
height: THICKNESS,
transform: `rotateX(-270deg) translateY(${-THICKNESS}px)`,
transformOrigin: 'top',
top: '0px',
left: '0px',
});
// Right face
createFace({
width: THICKNESS,
height,
transform: 'rotateY(90deg)',
transformOrigin: 'left',
top: '0px',
left: `${width}px`
});
// Bottom face
createFace({
width,
height: THICKNESS,
transform: `rotateX(-90deg) translateY(${THICKNESS}px)`,
transformOrigin: 'bottom',
bottom: '0px',
left: '0px'
});
// Left face
createFace({
width: THICKNESS,
height,
transform: `translateX(${-THICKNESS}px) rotateY(-90deg)`,
transformOrigin: 'right',
top: '0px',
left: '0px'
});
element.appendChild(fragment);
}
// Recursive function to traverse child nodes, apply 3D styles, and create side faces
function traverseDOM(parentNode, depthLevel, offsetX, offsetY) {
for (let children = parentNode.childNodes, childrenCount = children.length, i = 0; i < childrenCount; i++) {
const childNode = children[i];
if (!(1 === childNode.nodeType && !childNode.classList.contains('dom-3d-side-face'))) continue;
const color = COLOR_RANDOM ? getRandomColor() : getColorByDepth(depthLevel, COLOR_HUE, -5);
Object.assign(childNode.style, {
transform: `translateZ(${THICKNESS}px)`,
overflow: "visible",
backfaceVisibility: "hidden",
isolation: "auto",
transformStyle: "preserve-3d",
backgroundColor: COLOR_SURFACE ? color : getComputedStyle(childNode).backgroundColor,
willChange: 'transform',
});
let updatedOffsetX = offsetX;
let updatedOffsetY = offsetY;
if (childNode.offsetParent === parentNode) {
updatedOffsetX += parentNode.offsetLeft;
updatedOffsetY += parentNode.offsetTop;
}
createSideFaces(childNode, color);
traverseDOM(childNode, depthLevel + 1, updatedOffsetX, updatedOffsetY);
}
}
})()
@DrSensor

Copy link
Copy Markdown

Better to change mousemove with pointermove to make it works on touchscreen. Though it will be bit annoying to scroll on mobile phone.
Another way to make it seamless on mobile phone is by handle deviceorientation event then use: alpha, beta, gamma; value 🤔

@yagnikvamja

Copy link
Copy Markdown

Great..! it was cool..

@jonrandy

jonrandy commented Mar 28, 2024

Copy link
Copy Markdown

Anyone remember when Firefox had a far better version of this built in? https://www.youtube.com/watch?v=zqHV625EU3E

@Mte90

Mte90 commented Mar 28, 2024

Copy link
Copy Markdown

Anyone remember when Firefox had this built in?

yes

@OrionReed

Copy link
Copy Markdown
Author

@krzentner could you give an example of the isolation: isolate issue? Happy to make the change once I see what it's doing.

@ylluminate

Copy link
Copy Markdown

Everyone: since @OrionReed is now developing browser extensions (https://github.com/OrionReed/dom3d), it might be best to file issues at https://github.com/OrionReed/dom3d/issues - even if they are referenced here.

I think it might make it easier to track and resolve specific things.

@OrionReed

Copy link
Copy Markdown
Author

@ylluminate ^ absolutely. we can push improvement from that repo into this single-function gist and reference those issues/changes here.

@malikalimoekhamedov

Copy link
Copy Markdown

This should be a browser plugin.

@swnck

swnck commented Mar 29, 2024

Copy link
Copy Markdown

I have a updated version without the blue color:
https://gist.github.com/swnck/7aa80d2968a115ada8d920d772823cc8

@OrionReed

Copy link
Copy Markdown
Author

@malikalimoekhamedov it is being plugin-ified here here

@malikalimoekhamedov

Copy link
Copy Markdown

@malikalimoekhamedov it is being plugin-ified here here

Beautiful

@OrionReed

Copy link
Copy Markdown
Author

I've added some of the few major issues to the extension repo and if anyone can fix any of them I will buy you a coffee or drink of your choice!

Specifically, there are 3 issues that are causing the majority of problems:

@OrionReed

Copy link
Copy Markdown
Author

Revised the gist, should now work on many more websites which previously looked flat thanks to forcing isolation: auto

@Tristan-Trainual

Copy link
Copy Markdown

This is amazing!

@ichux

ichux commented Aug 6, 2024

Copy link
Copy Markdown

FYI you can make a bookmarklet in Firefox/Chrome and add this as the url to create a 1-click way of loading this into any site.

javascript:(()=>{function e(){return`hsl(${Math.floor(360*Math.random())}, ${50+Math.floor(30*Math.random())}%, ${40+Math.floor(30*Math.random())}%)`}let t=e=>[...e.children].reduce((e,n)=>Math.max(e,t(n)),0)+1,n=t(document.body),r=(e,t=0,r=0)=>`hsl(${t}, 75%, ${Math.min(10+e*(1+60/n),90)+r}%)`,o=document.body;o.style.overflow="visible",o.style.transformStyle="preserve-3d",o.style.perspective=1e4;let i=window.innerWidth/2,l=window.innerHeight/2;function s(e,t){}function f(e,t,n,o){for(let i=e.childNodes,l=i.length,$=0;$<l;$++){let a=i[$];if(!(1===a.nodeType&&!a.classList.contains("dom-3d-side-face")))continue;let d=r(t,190,-5);Object.assign(a.style,{transform:"translateZ(20px)",overflow:"visible",backfaceVisibility:"hidden",transformStyle:"preserve-3d",backgroundColor:d,willChange:"transform"});let c=n,m=o;a.offsetParent===e&&(c+=e.offsetLeft,m+=e.offsetTop),s(a,d),f(a,t+1,c,m)}}o.style.perspectiveOrigin=o.style.transformOrigin=`${i}px ${l}px`,f(o,0,0,0),document.addEventListener("mousemove",e=>{let t=180*(1-e.clientY/window.innerHeight)-90,n=180*e.clientX/window.innerWidth-90;o.style.transform=`rotateX(${t}deg) rotateY(${n}deg)`})})();

This is excellent. Waoh! Well done

@geode-main

Copy link
Copy Markdown

FYI you can make a bookmarklet in Firefox/Chrome and add this as the url to create a 1-click way of loading this into any site.

javascript:(()=>{function e(){return`hsl(${Math.floor(360*Math.random())}, ${50+Math.floor(30*Math.random())}%, ${40+Math.floor(30*Math.random())}%)`}let t=e=>[...e.children].reduce((e,n)=>Math.max(e,t(n)),0)+1,n=t(document.body),r=(e,t=0,r=0)=>`hsl(${t}, 75%, ${Math.min(10+e*(1+60/n),90)+r}%)`,o=document.body;o.style.overflow="visible",o.style.transformStyle="preserve-3d",o.style.perspective=1e4;let i=window.innerWidth/2,l=window.innerHeight/2;function s(e,t){}function f(e,t,n,o){for(let i=e.childNodes,l=i.length,$=0;$<l;$++){let a=i[$];if(!(1===a.nodeType&&!a.classList.contains("dom-3d-side-face")))continue;let d=r(t,190,-5);Object.assign(a.style,{transform:"translateZ(20px)",overflow:"visible",backfaceVisibility:"hidden",transformStyle:"preserve-3d",backgroundColor:d,willChange:"transform"});let c=n,m=o;a.offsetParent===e&&(c+=e.offsetLeft,m+=e.offsetTop),s(a,d),f(a,t+1,c,m)}}o.style.perspectiveOrigin=o.style.transformOrigin=`${i}px ${l}px`,f(o,0,0,0),document.addEventListener("mousemove",e=>{let t=180*(1-e.clientY/window.innerHeight)-90,n=180*e.clientX/window.innerWidth-90;o.style.transform=`rotateX(${t}deg) rotateY(${n}deg)`})})();

This is great! Thanks!

@ThatOneCalculator

ThatOneCalculator commented Aug 7, 2024

Copy link
Copy Markdown

Pardon my french, but what the fuck.

edit: the video isn't embedding. basically, it causes firefox to spaz out on twitter.

link (bright flashing lights warning): https://utfs.io/f/b6f07e7a-6460-4dd0-862a-de3d6bd85152-1yk8hb.webm

@dmarx

dmarx commented Aug 7, 2024

Copy link
Copy Markdown

Very cool! Just commenting to add a link to your tweet that led me here: https://x.com/OrionReedOne/status/1772934478421188620

@AshokTak

AshokTak commented Aug 8, 2024

Copy link
Copy Markdown

It's cool! Love it <3

@ashtonmeuser

Copy link
Copy Markdown

Very cool bookmarklet! Some of y'all may find my service, bookmarkl.ink, handy to distribute things like this. Here's this bookmarklet all packed up: https://bookmarkl.ink/OrionReed/4c3778ebc2b5026d2354359ca49077ca. Cheers!

@OrionReed

OrionReed commented Mar 26, 2025

Copy link
Copy Markdown
Author

Here's a slightly simpler version, would still love to figure out the draw ordering bugs (if that's even possible without control over the DOM)

(() => {
  const COLOR_HUE = 190; // hue in HSL (https://hslpicker.com)
  const THICKNESS = 20; // thickness of layers

  const getDOMDepth = () => {
    let m = 0;
    const t = (e, d) => {m = Math.max(m, d); for(let c of e.children) t(c, d + 1)};
    t(document.body, 0);
    return m;
  };

  const maxDepth = getDOMDepth();
  const getColorByDepth = depth => `hsl(${COLOR_HUE}, 75%, ${5 + (depth * 80 / maxDepth)}%)`;

  // Rotate the document based on mouse position
  document.addEventListener("pointermove", ({clientX, clientY}) => {
    const rotationY = 180 * (0.5 - clientY / innerHeight);
    const rotationX = 180 * (clientX / innerWidth - 0.5);
    document.body.style.transform = `rotateX(${rotationY}deg) rotateY(${rotationX}deg)`;
  });

  // Apply initial styles to the body to enable 3D perspective
  Object.assign(document.body.style, {
    perspective: `${10000}px`,
    overflow: "visible",
    perspectiveOrigin: `${innerWidth/2}px ${innerHeight/2}px`,
    transformOrigin: `${innerWidth/2}px ${innerHeight/2}px`
  });

  // Recursive function to traverse child nodes and apply 3D styles
  const traverseDOM = (node, depthLevel) => {
    // Only process element nodes
    if (node.nodeType !== 1) return;
    
    // Style current node
    Object.assign(node.style, {
      transform: `translateZ(${THICKNESS}px)`,
      backfaceVisibility: "hidden", // helps with performance
      isolation: "auto",
      transformStyle: "preserve-3d",
      backgroundColor: getColorByDepth(depthLevel),
    });

    // Traverse children
    for (const childNode of node.children) {
      traverseDOM(childNode, depthLevel + 1);
    }
  }

  traverseDOM(document.body, 0);
})()

@OrionReed

Copy link
Copy Markdown
Author

^ ths minifies into just this

(()=>{const e=(()=>{let e=0;const t=(n,i)=>{e=Math.max(e,i);for(let e of n.children)t(e,i+1)};return t(document.body,0),e})();document.addEventListener("pointermove",(({clientX:e,clientY:t})=>{const n=180*(.5-t/innerHeight),i=180*(e/innerWidth-.5);document.body.style.transform=`rotateX(${n}deg) rotateY(${i}deg)`})),Object.assign(document.body.style,{perspective:"10000px",overflow:"visible",perspectiveOrigin:`${innerWidth/2}px ${innerHeight/2}px`,transformOrigin:`${innerWidth/2}px ${innerHeight/2}px`});const t=(n,i)=>{if(1===n.nodeType){var o;Object.assign(n.style,{transform:"translateZ(20px)",backfaceVisibility:"hidden",isolation:"auto",transformStyle:"preserve-3d",backgroundColor:(o=i,`hsl(190, 75%, ${5+80*o/e}%)`)});for(const e of n.children)t(e,i+1)}};t(document.body,0)})();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment