Letter 3D Swap
A text component that swap the letters in a text with a box 3D effect.
SET YOUR MIND TO IT
Installation
1pnpm dlx shadcn@latest add "https://fancycomponents.dev/r/letter-3d-swap.json"
Usage
Just wrap your text with the component and set the rotateDirection
prop to the direction you want the text to rotate, the rest will be taken care by the component.
Understanding the component
Splitting the text into characters
First, we split the text into WorldObject
objects, each containing an array of characters and a boolean indicating whether there should be a space after the character. We use a handy function for this, which should respect emojis too.
Splitting the text into characters
1// handy function to split text into characters with support for unicode and emojis2const splitIntoCharacters = (text: string): string[] => {3 if (typeof Intl !== "undefined" && "Segmenter" in Intl) {4 const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" })5 return Array.from(segmenter.segment(text), ({ segment }) => segment)6 }7 // Fallback for browsers that don't support Intl.Segmenter8 return Array.from(text)9}
This method also helps us to ensure that words stay together and properly spaced when the text wraps across multiple lines. Without this approach, simply splitting by characters would break words at line boundaries.
Splitting the text into animation segments
1// Splitting the text into animation segments2const characters = useMemo(() => {3 const t = text.split(" ")4 const result = t.map((word: string, i: number) => ({5 characters: splitIntoCharacters(word),6 needsSpace: i !== t.length - 1,7 }))8 return result9}, [text])
3D Transforms
When rendering each character, we create two instances of it - a front face and a second face. The second face is positioned relative to the first one and uses 3D CSS transforms to create the illusion that it's on a different face of a 3D box. The face it appears on depends on the rotateDirection
prop:
"top"
- Character appears to flip upward from the top face"right"
- Character appears to flip from the right side"bottom"
- Character appears to flip downward from the bottom face"left"
- Character appears to flip from the left side
Top and bottom rotations
For top and bottom rotations, we create a 3D box effect through a series of transforms:
- The front face is brought forward by translating it
0.5lh
on the Z axis (lh
represents one line height) - For the second face, we:
- Rotate it 90° (or -90°) on the X axis
- Then translate it
0.5lh
forward in its local coordinate system to align with the edge of our virtual box
- Finally, we translate the container back by
-0.5lh
to account for the initial translation of the front face
This creates the illusion of characters flipping between two faces of a 3D cube. The demo below shows how these transforms work together:
Left and right rotations
For left/right rotations, we need to handle the box dimensions more carefully. Unlike top/bottom rotations where we can use line height (lh
) as a fixed measurement, the width of each character varies. The side faces of our 3D box need to match the actual character width.
To achieve this, we use percentage-based translations on the X and Y axes, since these can automatically adapt to each character's width. The transform sequence works like this:
-
First face:
- Rotate 90° on Y axis to face sideways
- Translate 50% of character width to align with edge
- Rotate -90° on Y axis to face forward again
-
Second face:
- Apply the same transforms as the first face
- Add additional transforms to position it correctly on the side
-
Lastly, we push back both faces to account for the initial translation
The demo below shows this transform sequence step by step:
Why the initial translation?
The initial forward translation of our box (using 0.5lh
for top
/bottom
rotations, or the transform chain for left
/right
rotations) serves an important purpose. It ensures the rotation axis passes through the center of our virtual 3D box, rather than along its front face. This creates a more natural flipping motion, as the character rotates around its center point rather than pivoting from its front edge. Without this translation, the box rotation would appear to swing outward in an unnatural arc rather than flipping in place.
Of course, you can achieve the same result by applying (other) transforms in a different order, and even playing with the transform origins. I apologise if this seems overcomplicated, this is how it made sense to me :).
Animation
Now that we have our virtual 3D box, the only thing left is to rotate each character box. For this, we use the useAnimate
hook from motion. This gives us a scope and an animate
function to control the animation. We add .letter-3d-swap-char-box-item
class name to each char box, so we can select and animate them with the animate
function. After the animation is completed, we reset the transform to the original state.
Animation
1// Animate each character with its specific delay2await animate(3 ".letter-3d-swap-char-box-item",4 { transform: rotationTransform },5 {6 ...transition,7 delay: (i: number) => delays[i],8 }910// Reset all boxes11await animate(12 ".letter-3d-swap-char-box-item",13 { transform: "rotateX(0deg) rotateY(0deg)" },14 { duration: 0 }15)
The transform is just a 90/-90 degree rotation either on the X or Y axis, depending on the rotateDirection
prop.
Stagger
The delay is calculated based on the staggerFrom
prop, which can be set to first
, last
, center
, random
or a number. If it's a number, it's used as the index of the character to stagger from. For example, if staggerFrom
is set to 2
, the second character will be staggered from the third one. We have a handy function to calculate the correct delay for each character:
Stagger delay calculation
1// Helper function to calculate stagger delay for each text segment2const getStaggerDelay = useCallback(3 (index: number, totalChars: number) => {4 const total = totalChars5 if (staggerFrom === "first") return index * staggerDuration6 if (staggerFrom === "last") return (total - 1 - index) * staggerDuration7 if (staggerFrom === "center") {8 const center = Math.floor(total / 2)9 return Math.abs(center - index) * staggerDuration10 }11 if (staggerFrom === "random") {12 const randomIndex = Math.floor(Math.random() * total)13 return Math.abs(randomIndex - index) * staggerDuration14 }15 return Math.abs(staggerFrom - index) * staggerDuration16 },17 [staggerFrom, staggerDuration]18)
Check out the demo to see the possible values for staggerFrom
.
Rotate Left, Stagger First
Rotate Right, Stagger Last
Rotate Top, Stagger Center
Rotate Bottom, Stagger Random
Resources
- Intro to CSS 3D transforms by David DeSandro
Props
Letter3DSwapProps
Prop | Type | Default | Description |
---|---|---|---|
children* | React.ReactNode | - | The content to be displayed and animated |
as | ElementType | p | HTML Tag to render the component as |
mainClassName | string | - | Class name for the main container element |
frontFaceClassName | string | - | Class name for the front face element |
secondFaceClassName | string | - | Class name for the secondary face element |
staggerDuration | number | 0.05 | Duration of stagger delay between elements in seconds |
staggerFrom | "first" | "last" | "center" | "random" | number | "first" | Direction to stagger animations from |
transition | ValueAnimationTransition | AnimationOptions | { type: "spring", damping: 25, stiffness: 300 } | Animation transition configuration |
rotateDirection | "top" | "right" | "bottom" | "left" | "right" | Direction of rotation |