SVG and HTML overlays are useful for outlines, labels, and interaction feedback in a 3D interface. They are often easier to style than equivalent geometry inside Three.js, especially for thin lines and text.
The limitation is that a 2D overlay has no 3D depth information. Anything drawn there sits above the rendered scene and is not automatically occluded by objects in front of it.
I ran into this in a cube-manipulation prototype. Unselected cubes used thin SVG outlines to separate them from the background. With only a few cubes, the result looked fine. Once several cubes were placed close together, outlines from cubes in the back became visible through cubes in front.
The screen coordinates were not wrong. The problem was the rendering layer: SVG knew where the projected edges were, but not which 3D object was closer to the camera.
The root-cause diagnosis and implementation approach mainly came from Claude Code. My part was validating whether the revised behavior produced the correct occlusion in dense scenes.
The final solution did not add more occlusion logic to SVG. Instead, different outline states were assigned to different rendering layers.
Normal outlines for unselected objects moved into the 3D scene. Each cube uses its own EdgesGeometry. Because the lines are real 3D objects, they participate in depth testing and inherit the scene's occlusion behavior automatically.
Selection and hover outlines remain in the SVG overlay. Those states have a different purpose. Even if part of the object is hidden behind something else, the user still needs a clear indication of what is selected or currently under the pointer. Keeping those outlines above the scene is therefore intentional.
The rule became straightforward: if the visual should respect real spatial occlusion, render it in 3D. If it needs to remain visible regardless of depth, render it as a 2D overlay.
It would be possible to keep all outlines in SVG and calculate which projected edges are hidden by other objects.
But that would recreate a problem the 3D renderer already solves through its depth buffer. As object count, camera angle, and scene complexity increase, the custom visibility logic would also become more complicated.
For ordinary object outlines, there is little value in moving depth responsibility into the 2D layer. Once the effect is placed back in the 3D scene, the renderer can handle the relationship naturally.
The issue was not that SVG could never be made to work. It was that a 2D layer was being asked to manage spatial behavior that belonged to the 3D layer.
Moving from SVG to WebGL lines reduced some visual precision.
SVG can specify sub-pixel widths such as 0.5px. Basic WebGL line width is much less controllable across platforms and is often effectively limited to around 1px.
The trade-off was acceptable because the two problems were not equally important:
Opacity was adjusted to compensate for the heavier 3D line. The normal and hover states still needed enough contrast to separate adjacent cubes clearly in dense arrangements.
This issue was easy to miss with only a few objects.
A rear outline becomes visibly wrong only when its screen projection overlaps an object in front. Dense layouts therefore create the test condition that exposes the missing depth relationship.
For 3D outlines, labels, and overlays, it is useful to test more than isolated objects:
The question is not only whether an individual component renders correctly, but whether it still behaves correctly once it becomes part of the full spatial composition.
The specific diagnosis and the EdgesGeometry implementation mainly came from Claude Code. My responsibility was deciding which outline states should follow real occlusion and validating whether the result matched the intended interaction.
The more reusable question is not "should this outline use SVG or Three.js?" It is: should this visual still be visible when another object is physically in front of it?
Normal and selected outlines can have different answers, even though they look similar. Once that semantic decision is clear, the rendering layer becomes much easier to choose.