TextPath
Create dynamic SVG text flowing around elements with infinite scroll
TextPath creates scrolling text that flows around elements along SVG paths. It supports circles, ovals, rounded rectangles, and custom paths with seamless infinite scroll animation.
Basic Usage
import { TextPath } from 'faster-motion';
// Create scrolling text around a circle element
const textPath = TextPath.create('.badge', 'Premium Quality • ', {
offset: 30, // Distance from element edge
borderRadius: 100, // Creates circular path
animate: {
infiniteScroll: true,
duration: 5000 // 5 second loop
}
});
// Later: cleanup
textPath.destroy();Try it: TextPath
const textPath = TextPath.create('.badge', 'Premium • ', {
offset: 30,
borderRadius: 100,
animate: { infiniteScroll: true, duration: 5000 }
});Circle TextPath
Try it: Circle
TextPath.create('#circle', 'Premium Quality • ', {
offset: 30,
borderRadius: 100,
animate: { infiniteScroll: true, duration: 5000 }
});Oval TextPath
Try it: Oval
TextPath.create('#oval', 'Elliptical Path • ', {
offset: 30,
borderRadius: 100,
animate: { infiniteScroll: true, duration: 5000 }
});Rectangle TextPath
Try it: Rectangle
TextPath.create('#rectangle', 'Rounded Rectangle • ', {
offset: 30,
borderRadius: 20,
animate: { infiniteScroll: true, duration: 5000 }
});TextPath.create()
Create an SVG text path around an element.
Syntax
TextPath.create(
element: Element | string,
text: string,
options?: TextPathOptions
): TextPathDataParameters
element- Target element or CSS selectortext- Text to display (automatically repeated for seamless scroll)options- Configuration object (see options below)
Returns
TextPathData object:
interface TextPathData {
container: HTMLElement; // Wrapper container
svg: SVGSVGElement; // SVG element
path: SVGPathElement; // Path element
textPath: SVGTextPathElement; // TextPath element
textElement: SVGTextElement; // Text element
update: (transformOverride?: string) => void;
destroy: () => void;
}Options
offset
Type: number | { x: number; y: number }
Default: 30
Distance of path from element edges in pixels.
- Positive values: Path outside element
- Zero: Path at element edges
- Negative values: Path inside element
// Path 50px outside element
TextPath.create('.element', 'Text • ', { offset: 50 });
// Path at element edge
TextPath.create('.element', 'Text • ', { offset: 0 });
// Path 20px inside element
TextPath.create('.element', 'Text • ', { offset: -20 });borderRadius
Type: number
Default: 30
Corner radius for the path. Higher values create more rounded paths.
// Circle (large radius)
TextPath.create('.circle', 'Text • ', { borderRadius: 100 });
// Rounded rectangle
TextPath.create('.box', 'Text • ', { borderRadius: 20 });
// Sharp corners
TextPath.create('.box', 'Text • ', { borderRadius: 0 });Auto-detection:
- Square element + large radius = Circle path
- Non-square + large radius = Ellipse path
- Any element + small radius = Rounded rectangle
customPath
Type: string
Custom SVG path data (d attribute). Overrides automatic path generation.
const pathData = 'M10,80 Q95,10 180,80 Q265,150 350,80';
TextPath.create('.element', 'Custom Curve • ', {
customPath: pathData
});autoRotate
Type: boolean
Default: false
Rotate text to follow path tangent. Useful for custom curved paths.
TextPath.create('.element', 'Text • ', {
autoRotate: true,
borderRadius: 100
});watchResize
Type: boolean
Default: true
Monitor element resize and update path automatically.
// Disable resize observation
TextPath.create('.element', 'Text • ', {
watchResize: false
});animate
Type: object
Animation configuration for the text path.
Properties:
infiniteScroll(boolean) - Enable seamless infinite scrollingduration(number) - Animation duration in millisecondsease(string) - Easing function (for manual mode)repeat(number) - Repeat count (for manual mode)startOffset([number, number]) - [from, to] percentage (for manual mode)
Infinite Scroll (Recommended):
TextPath.create('.element', 'Scrolling Text • ', {
animate: {
infiniteScroll: true,
duration: 5000 // 5 second loop
}
});Manual Mode:
TextPath.create('.element', 'Text • ', {
animate: {
startOffset: [0, 100], // 0% to 100%
duration: 3000,
ease: 'linear',
repeat: Infinity
}
});Instance Methods
.update(transformOverride?)
Update path position. Call after element transforms.
const textPath = TextPath.create('.element', 'Text • ');
// Element rotated
element.style.transform = 'rotate(45deg)';
// Update TextPath to match
textPath.update('rotate(45deg)');.destroy()
Cleanup and remove the TextPath.
const textPath = TextPath.create('.element', 'Text • ');
// Later: cleanup
textPath.destroy();Static Methods
TextPath.get(element)
Get existing TextPath instance for an element.
const textPath = TextPath.get('.element');
if (textPath) {
console.log(textPath.svg); // Access SVG element
}TextPath.update(element)
Update TextPath for an element (useful after transforms).
element.style.transform = 'rotate(45deg)';
TextPath.update(element);TextPath.destroy(element)
Destroy TextPath instance for a specific element.
TextPath.destroy('.element');TextPath.destroyAll()
Destroy all TextPath instances. Useful for SPA route changes.
// Cleanup all on route change
TextPath.destroyAll();Helper Functions
createCircularTextPath()
Convenience function for circular text paths.
import { createCircularTextPath } from 'faster-motion';
const textPath = createCircularTextPath('.circle', 'Text • ', 100, {
offset: 30,
animate: {
infiniteScroll: true,
duration: 5000
}
});Examples
Basic Circle
<div class="circle" style="width: 200px; height: 200px; border-radius: 100px;"></div>TextPath.create('.circle', 'Spinning Text • ', {
offset: 30,
borderRadius: 100,
animate: {
infiniteScroll: true,
duration: 5000
}
});Oval/Ellipse
<div class="oval" style="width: 300px; height: 150px; border-radius: 150px;"></div>TextPath.create('.oval', 'Elliptical Path • ', {
offset: 40,
borderRadius: 75,
animate: {
infiniteScroll: true,
duration: 6000
}
});Rounded Rectangle
<div class="box" style="width: 300px; height: 200px; border-radius: 20px;"></div>TextPath.create('.box', 'Rounded Box • ', {
offset: 20,
borderRadius: 20,
animate: {
infiniteScroll: true,
duration: 8000
}
});Inside Element
// Text inside element borders
TextPath.create('.element', 'Inside Text • ', {
offset: -20, // Negative = inside
borderRadius: 50,
animate: {
infiniteScroll: true,
duration: 4000
}
});Styling Text
const textPath = TextPath.create('.element', 'Styled Text • ', {
offset: 30,
borderRadius: 100
});
// Style the text
textPath.textElement.style.fill = '#ff0000';
textPath.textElement.style.fontSize = '24px';
textPath.textElement.style.fontWeight = 'bold';
textPath.textElement.style.fontFamily = 'Arial, sans-serif';With Scroll-Synced Rotation
import { TextPath, ScrollObserver } from 'faster-motion';
const textPath = TextPath.create('.hero-badge', 'Scroll Down • ', {
offset: 30,
borderRadius: 80,
animate: {
infiniteScroll: true,
duration: 6000
}
});
ScrollObserver.observe('.hero-badge', ({ progress }) => {
const rotation = progress * 720; // 2 full rotations
const element = document.querySelector('.hero-badge');
const transform = `rotate(${rotation}deg)`;
element.style.transform = transform;
textPath.update(transform);
});Multiple Elements
const elements = document.querySelectorAll('.badge');
elements.forEach(element => {
TextPath.create(element, 'Badge Text • ', {
offset: 10,
borderRadius: 50,
animate: {
infiniteScroll: true,
duration: 3000
}
});
});Cleanup on Route Change
// Create TextPaths
TextPath.create('.badge-1', 'Text • ', { animate: { infiniteScroll: true, duration: 5000 } });
TextPath.create('.badge-2', 'Text • ', { animate: { infiniteScroll: true, duration: 5000 } });
// SPA route change
router.on('beforeNavigate', () => {
TextPath.destroyAll(); // Cleanup all
});Best Practices
Do
- Use a separator character (like
•) in your text for visual spacing - Call
destroy()when unmounting components - Use
TextPath.destroyAll()on SPA route changes - Update after transforms with
textPath.update(transformString) - Use
infiniteScroll: truefor continuous animations
Don't
- Forget the separator:
"Text"will run together, use"Text • " - Create without cleanup: causes memory leaks in SPAs
- Use with tiny elements where text is larger than the element
- Forget to update after transforms (TextPath will be out of sync)
- Use negative duration values
TypeScript
import {
TextPath,
TextPathOptions,
TextPathData,
createCircularTextPath
} from 'faster-motion';
// Type-safe options
const options: TextPathOptions = {
offset: 30,
borderRadius: 100,
watchResize: true,
animate: {
infiniteScroll: true,
duration: 5000
}
};
// Type-safe create
const element: Element = document.querySelector('.element')!;
const textPath: TextPathData = TextPath.create(element, 'Text • ', options);
// Type-safe instance properties
const container: HTMLElement = textPath.container;
const svg: SVGSVGElement = textPath.svg;
// Type-safe methods
textPath.update('rotate(45deg)');
textPath.destroy();Performance Notes
Transform Composition
TextPath uses a wrapper architecture for perfect sync with element transforms:
Container (receives element transforms)
├── Element (original element)
└── SVG (only handles infinite scroll rotation)Benefits:
- No JavaScript transform coordination needed
- CSS handles composition via DOM hierarchy
- No getBoundingClientRect() reads
- Perfect sync at all times
Cached Layout
Layout properties are cached for fast transform updates. Only resize triggers recalculation.
Seamless Infinite Scroll
Uses modulo math for seamless wrapping - continuous scrolling with no visible reset.
Browser Support
- Chrome/Edge: Full support
- Firefox: Full support
- Safari: Full support (iOS 12+)
- Requires: SVG support (all modern browsers)
See Also
- TextSplitter - Text splitting for animation
- TextEffects - Scroll-driven text effects
- TextAnimation - Canvas per-character animation