DepthText is a lightweight, dependency-free JavaScript library that creates smooth multi-layer 3D text with depth, parallax, and interactive rotation.
Designed for high performance, accessibility, and modern UX motion guidelines.
It is the spiritual successor of ztext.js, but rewritten from scratch with a cleaner API, better performance, and a modern ES module architecture.
- π True 3D layered depth using CSS
transform-style: preserve-3d - π±οΈ Interactive rotation (auto, pointer, scrollβ¦)
- ποΈ Configurable number of layers
- π¨ Optional fade effect across layers
- β‘ GPU-optimized and jank-free
- π¦Ύ Accessibility-friendly (
aria-hidden, reduced-motion support) - π₯ No dependencies, only 4β6 KB minified
- π¦ Works with bundlers, ES modules, and browsers
- πΌοΈ Supports images, SVGs, and emojis within text content
- π Custom CSS classes for advanced styling control
- π¨ Per-layer styling with
layerClassMapand auto-generated classes - π Dynamic updates with
.update()method
npm install depthtext<!-- ESM -->
<script type="module">
import { DepthTextify } from "https://cdn.jsdelivr.net/npm/depthtext@latest/dist/depthtext.mjs";
DepthTextify();
</script>
<!-- Global/IIFE (classic script tag) -->
<script src="https://cdn.jsdelivr.net/npm/depthtext@latest/dist/depthtext.global.js"></script>
<script>
DepthText.DepthTextify();
</script><!-- ESM -->
<script type="module">
import { DepthTextify } from "https://unpkg.com/depthtext@latest/dist/depthtext.mjs";
DepthTextify();
</script>
<!-- Global/IIFE -->
<script src="https://unpkg.com/depthtext@latest/dist/depthtext.global.js"></script>
<script>
DepthText.DepthTextify();
</script>import { DepthTextify } from "depthtext";<h1 class="depthtext" data-depth="2rem" data-depth-event="pointer">DepthText Rocks</h1>import { DepthTextify } from "depthtext";
DepthTextify(); // Enhances all elements with [data-depth]DepthText can be configured using either:
- HTML data attributes
- or JavaScript options
| Attribute | Type | Default | Description |
|---|---|---|---|
data-depth |
string | 1rem |
Z-axis distance between layers (e.g., "2rem", "20px") |
data-depth-layers |
number | 10 |
Number of 3D layers (1-40, recommended β€25 for performance) |
data-depth-direction |
string | both |
Layer direction: both, forwards, backwards |
data-depth-event |
string | none |
Interaction: none, pointer, scroll, scrollX, scrollY |
data-depth-event-rotation |
string | 30deg |
Max rotation angle on interaction (e.g., "45deg", "0.5rad") |
data-depth-event-direction |
string | default |
Rotation direction: default, reverse |
data-depth-fade |
boolean | false |
Fade layers as depth increases |
data-depth-perspective |
string | 500px |
CSS perspective value |
data-depth-engaged |
boolean | true |
Enable/disable the effect |
data-depth-add-class |
string | "" |
Custom CSS class(es) to add to ALL layers |
data-depth-engaged to enable/disable, NOT data-depth alone.
import { DepthTextInstance } from "depthtext";
const instance = new DepthTextInstance(element, {
depth: "1rem", // Z-distance between layers
layers: 10, // Number of layers
direction: "both", // "both" | "forwards" | "backwards"
event: "pointer", // "none" | "pointer" | "scroll" | "scrollX" | "scrollY"
eventRotation: "30deg", // Max tilt angle
eventDirection: "default", // "default" | "reverse"
fade: false, // Enable opacity fade
perspective: "500px", // CSS perspective
engaged: true, // Enable/disable effect
addClass: "my-class", // Custom class for ALL layers
layerClassMap: ["front", "middle", "back"], // Custom class per layer (NEW in v1.2.0)
});
// Dynamically update options (NEW in v1.2.0)
instance.update({
depth: "2rem",
event: "scroll",
});
// Cleanup
instance.destroy();Each layer automatically receives a unique class:
<!-- 3 layers example -->
<span class="depthtext-layer depthtext-layer-0">...</span>
<span class="depthtext-layer depthtext-layer-1">...</span>
<span class="depthtext-layer depthtext-layer-2">...</span>Target specific layers with CSS:
.depthtext-layer-0 {
/* Front layer */
}
.depthtext-layer-1 {
/* Middle layer */
}
.depthtext-layer-2 {
/* Back layer */
}Map custom classes to individual layers by index:
new DepthTextInstance(element, {
layers: 5,
layerClassMap: ["front", "mid-front", "center", "mid-back", "back"],
});Result:
<span class="depthtext-layer depthtext-layer-0 front">...</span>
<span class="depthtext-layer depthtext-layer-1 mid-front">...</span>
<span class="depthtext-layer depthtext-layer-2 center">...</span>
<span class="depthtext-layer depthtext-layer-3 mid-back">...</span>
<span class="depthtext-layer depthtext-layer-4 back">...</span>Rainbow gradient example:
new DepthTextInstance(element, {
layers: 7,
layerClassMap: ["red", "orange", "yellow", "green", "blue", "indigo", "violet"],
});.red {
color: #ff0000;
}
.orange {
color: #ff7f00;
}
.yellow {
color: #ffff00;
}
.green {
color: #00ff00;
}
.blue {
color: #0000ff;
}
.indigo {
color: #4b0082;
}
.violet {
color: #9400d3;
}new DepthTextInstance(element, {
layers: 10,
layerClassMap: ["special-0", "special-1", "special-2"],
// Layers 3-9 will only have: depthtext-layer depthtext-layer-N
});<h1 data-depth="1rem" data-depth-add-class="gradient-text">Stylized Text</h1>.gradient-text {
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}Multiple classes:
new DepthTextInstance(element, {
addClass: "gradient-text shadow-effect animated",
});Because layers are clones sharing the same text formatting context, color inheritance can be tricky.
β This WON'T work reliably:
.depthtext-layer-0 {
color: blue;
}
.depthtext-layer-1 {
color: red;
}β Use the universal selector instead:
.depthtext-layer-0 * {
color: blue !important;
}
.depthtext-layer-1 * {
color: red !important;
}Or add a global reset:
.depthtext-layer > * {
color: inherit !important;
}This ensures each layer's content truly inherits its parent layer's color.
/* Neon glow effect */
.neon-glow {
color: #00ffff;
text-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff, 0 0 30px #00ffff;
}
/* Metallic gradient */
.metallic {
background: linear-gradient(90deg, #c0c0c0, #e8e8e8, #c0c0c0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Rainbow animation */
@keyframes rainbow {
0%,
100% {
color: #ff0000;
}
16% {
color: #ff7f00;
}
33% {
color: #ffff00;
}
50% {
color: #00ff00;
}
66% {
color: #0000ff;
}
83% {
color: #8b00ff;
}
}
.rainbow-text {
animation: rainbow 3s linear infinite;
}
/* Per-layer depth-based opacity */
.depthtext-layer-0 {
opacity: 1;
}
.depthtext-layer-1 {
opacity: 0.9;
}
.depthtext-layer-2 {
opacity: 0.8;
}
.depthtext-layer-3 {
opacity: 0.7;
}Change options after initialization without recreating the instance:
const instance = new DepthTextInstance(element, {
depth: "1rem",
event: "pointer",
});
// Later, change behavior
instance.update({
depth: "2rem",
event: "scroll",
layers: 15,
});Perfect for:
- Responsive designs (change depth on mobile)
- Interactive controls (user preferences)
- State-based animations
Auto-enhance all elements with [data-depth] attribute:
<h1 data-depth="2rem" data-depth-event="pointer">Auto-enhanced</h1>
<h2 data-depth="1rem" data-depth-layers="5">Another one</h2>import { DepthTextify } from "depthtext";
// Initialize all [data-depth] elements
DepthTextify();
// Or with custom selector
DepthTextify(".custom-selector");
// Or with default options
DepthTextify({
depth: "2rem",
event: "pointer",
});DepthText is designed to remain invisible to assistive technologies:
- Wrappers are automatically marked with
aria-hidden="true" - Motion reacts to
prefers-reduced-motion: reduce - Original text remains intact and readable in source
- Screen readers only see the original content
<script src="dist/depthtext.global.js"></script>
<script>
// DepthText is available globally
DepthText.DepthTextify();
</script>DepthText is framework-agnostic. You can use it with React, Vue, Angular, Svelte, etc., by instantiating DepthTextInstance on a mounted DOM element.
import { useEffect, useRef } from "react";
import { DepthTextInstance } from "depthtext";
export default function DepthComponent() {
const textRef = useRef(null);
useEffect(() => {
if (!textRef.current) return;
const dt = new DepthTextInstance(textRef.current, {
layers: 10,
depth: "1rem",
event: "pointer",
layerClassMap: ["front", "mid", "back"], // Per-layer styling
});
// Cleanup on unmount
return () => dt.destroy();
}, []);
return <h1 ref={textRef}>DepthText in React</h1>;
}<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import { DepthTextInstance } from "depthtext";
const textRef = ref(null);
let dt = null;
onMounted(() => {
if (textRef.value) {
dt = new DepthTextInstance(textRef.value, {
layers: 10,
event: "pointer",
addClass: "custom-style",
layerClassMap: ["layer-a", "layer-b", "layer-c"],
});
}
});
onUnmounted(() => {
if (dt) dt.destroy();
});
</script>
<template>
<h1 ref="textRef">DepthText in Vue</h1>
</template>import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy } from "@angular/core";
import { DepthTextInstance } from "depthtext";
@Component({
selector: "app-depth-text",
template: "<h1 #depthText>DepthText in Angular</h1>",
})
export class DepthTextComponent implements AfterViewInit, OnDestroy {
@ViewChild("depthText") textRef!: ElementRef;
private dt?: DepthTextInstance;
ngAfterViewInit() {
this.dt = new DepthTextInstance(this.textRef.nativeElement, {
layers: 10,
event: "pointer",
layerClassMap: ["front", "middle", "back"],
});
}
ngOnDestroy() {
this.dt?.destroy();
}
}DepthText works with various content types:
- Text content (including unicode, special characters)
- Emojis (π, π, β€οΈ, etc.)
- Inline SVG (
<svg>...</svg>) - Images (
<img src="...">) - Mixed content (text + images + SVGs together)
<h1 class="depthtext" data-depth="1rem" data-depth-event="pointer" data-depth-add-class="colorful">
Hello World! π
<svg width="30" height="30"><circle cx="15" cy="15" r="10" fill="blue" /></svg>
<img src="logo.png" width="40" alt="Logo" />
</h1>- Images should be loaded before initialization for best results
- Very large images may impact performance
- External SVGs (
<img src="icon.svg">) work like regular images
- DepthText uses CSS transforms; parent elements must not flatten 3D contexts
- Avoid nested DepthText unless you understand
transform-style: preserve-3d - Images: For complex layouts, consider using
background-imageon wrapper elements - Performance: Very high layer counts (>25) with large images may impact performance
- Color styling: Use
.depthtext-layer-N * { color: ... }for reliable per-layer coloring
Pull requests are welcome!
If you add a major feature, please include documentation updates.
MIT License β free to use in commercial and open-source projects.
Created by MobiWise.
A modern reimagining of the classic ztext.js.
