situs-kit
v0.2.9
Published
A creative developer helper library
Readme
Situs Toolkit
A zero-dependency creative developer toolkit
SplitText
Split text into characters, words, and lines while preserving inline HTML elements like <a>, <em>, <strong>, and <u>.
Basic Usage
import { SplitText } from "situs-kit/split-text";
const split = new SplitText("#my-text", {
type: ["chars", "words", "lines"],
});
split.chars; // HTMLElement[]
split.words; // HTMLElement[]
split.lines; // HTMLElement[]Constructor
new SplitText(
element: HTMLElement | HTMLElement[] | ArrayLike<HTMLElement> | string,
options?: SplitTextOptions<T>
)The element parameter accepts:
- A CSS selector string (e.g.
"#my-text",".my-class") - A single
HTMLElement - An array or array-like collection of
HTMLElements (creates sub-instances, aggregating results)
Options
new SplitText(element, {
type: ["chars", "words", "lines"], // which levels to split
tag: "span", // wrapper element tag
children: true, // split children of the target
mask: ["chars", "lines"], // which levels to mask
resize: true, // auto-reflow lines on resize
style: {
chars: { color: "red" },
words: {},
lines: {},
mask: {}, // flat: applies to all masks
// mask: { chars: {}, lines: {} }, // per-type: target specific masks
},
class: {
chars: "my-char",
words: "my-word",
lines: "my-line",
mask: "my-mask", // flat: applies to all masks
// mask: { chars: "c", lines: "l" }, // per-type: target specific masks
},
});| Option | Type | Default | Description |
|--------|------|---------|-------------|
| type | SplitType \| SplitType[] | ["chars", "words", "lines"] | Split granularity |
| tag | string | "span" | Wrapper element tag |
| children | boolean | false | Split the target's child elements instead of the target itself |
| mask | boolean \| SplitType[] | false | Create overflow-hidden wrappers. true masks all requested types; array masks only specified types |
| resize | boolean | true | Auto-reflow lines on window resize (only applies when lines are split) |
| style | object | — | Inline styles per split type. mask accepts a flat object (all masks) or per-type { chars?, words?, lines? } |
| class | object | — | CSS classes per split type. mask accepts a string (all masks) or per-type { chars?, words?, lines? } |
Type safety:
style,class, andmaskare constrained bytype. Settingstyle: { words: {} }whentype: ["chars"]is a compile-time error.
Types
type SplitType = "chars" | "words" | "lines"
type SplitTypeOption =
| SplitType
| ["chars"]
| ["words"]
| ["lines"]
| ["chars", "words"]
| ["chars", "lines"]
| ["words", "lines"]
| ["chars", "words", "lines"]Properties
| Property | Type | Description |
|----------|------|-------------|
| dom | HTMLElement \| HTMLElement[] | The original element(s) |
| chars | HTMLElement[] | Character wrapper elements |
| words | HTMLElement[] | Word wrapper elements |
| lines | HTMLElement[] | Line wrapper elements |
| masks | { chars: HTMLElement[], words: HTMLElement[], lines: HTMLElement[] } | Mask wrapper elements per split type |
Methods
reflow()
Recalculate line groupings without re-splitting characters or words. Called automatically on resize when resize: true. Does nothing if lines weren't requested in type.
split.reflow();revert()
Restore the element to its original HTML. Clears all chars, words, lines, and masks arrays. Calls destroy() internally.
split.revert();destroy()
Remove resize observers and cancel pending animation frames. Called automatically by revert(). Safe to call multiple times.
split.destroy();Data Attributes
Split elements include data attributes for CSS targeting:
[data-char] { display: inline-block; }
[data-word] { display: inline-block; }
[data-line] { display: block; }Mask elements use:
data-maskChardata-maskWorddata-maskLine
HTML Preservation
Inline elements like <a>, <em>, <strong>, <u>, <i>, <b>, <ins>, <s>, <strike>, and <del> are preserved and cloned around split elements.
Opaque elements like <div>, <span>, <svg>, <img>, <br>, <canvas>, <video>, <audio>, <iframe>, <input>, <textarea>, <select>, <button>, <picture>, and <figure> are treated as atomic units.
<!-- Input -->
<p id="text">Hello <strong>world</strong></p>
<!-- After split, <strong> wraps are preserved -->Masking
Masks wrap split elements in overflow: hidden containers, useful for reveal animations. You can specify which split levels to mask:
// mask only lines
const split = new SplitText("#text", {
type: ["words", "lines"],
mask: ["lines"],
});
split.masks.lines; // HTMLElement[] - overflow:hidden wrappers
// mask all split types
const split2 = new SplitText("#text", {
type: ["chars", "words", "lines"],
mask: true,
});Per-Type Mask Styles
Apply different styles or classes to mask wrappers depending on their split level:
new SplitText("#text", {
type: ["chars", "words", "lines"],
mask: true,
style: {
mask: {
chars: { perspective: "500px" },
lines: { clipPath: "inset(0)" },
},
},
class: {
mask: {
chars: "char-mask",
lines: "line-mask",
},
},
});A flat value still applies to all masks:
style: { mask: { overflow: "visible" } } // all masks
class: { mask: "my-mask" } // all masksChildren Mode
When children: true, SplitText splits each direct child element of the target instead of the target itself. Useful when a container holds multiple blocks (headings, paragraphs) that should all be split with one call.
// <div id="hero">
// <h1>Title</h1>
// <p>Subtitle text here</p>
// </div>
const split = new SplitText("#hero", {
type: "words",
children: true,
});
// splits both <h1> and <p> individually
split.words; // HTMLElement[] from all childrenMulti-Element Support
When passing multiple elements (via selector, array, or array-like), SplitText creates sub-instances and aggregates all results into the top-level chars, words, lines, and masks arrays.
const split = new SplitText(".my-text");
// chars/words/lines contain elements from all matched elements
split.chars; // HTMLElement[] from all .my-text elementsAdditional Behaviors
- Letter spacing: Automatically applied to character elements when the parent has
letter-spacingset - Text indent: Preserved on the first line
SmoothScroll
Smooth scroll with frame-rate independent damping. Intercepts wheel for smooth interpolation, uses native scroll for touch/mobile.
Usage
import { SmoothScroll } from "situs-kit/smooth-scroll";
const scroll = new SmoothScroll();Options
const scroll = new SmoothScroll({
wrapper: "#scroll-box", // window | HTMLElement | string (default: window)
direction: "vertical", // "vertical" | "horizontal"
duration: 800, // global to() duration in ms (0 = use damp)
ease: 0.09, // number → damp factor, function → tween ease (default: 0.09)
prevent: true, // stop wheel propagation (default: true for elements, false for window)
});Scroll State
const { current, target, velocity, direction, max, scrolling } = scroll.scroll;Note:
scrollreturns a shared object that is mutated each frame. Destructure or copy the values if you need to compare across frames.
| Property | Type | Description |
|----------|------|-------------|
| current | number | Current interpolated position |
| target | number | Target position |
| velocity | number | Pixels moved since last frame |
| direction | 1 \| -1 | 1 = forward, -1 = backward |
| max | number | Maximum scroll value |
| scrolling | boolean | Whether currently scrolling |
Methods
to(target, options?)
scroll.to(500);
scroll.to("#section-2", { offset: -100 });
scroll.to(element, { duration: 1000 });
scroll.to(0, { immediate: true });| Option | Type | Description |
|--------|------|-------------|
| offset | number | Pixel offset added to target |
| duration | number | Animation duration in ms (defaults to constructor duration) |
| ease | number \| (t: number) => number | Number → damp factor, function → tween ease curve |
| immediate | boolean | Jump instantly |
| onComplete | () => void | Callback when scroll completes |
start() / stop()
Resume or pause scroll listening and the animation loop.
destroy()
Full cleanup — removes listeners, stops the ticker, clears events.
Events
scroll.on("scroll", ({ current, target, velocity, direction, max }) => {});
scroll.off("scroll", callback);ScrollObserver
Observe when elements enter and leave the viewport based on scroll position.
scroll.observe({
trigger: "#my-element",
start: { dom: "top", viewport: "80%" },
end: { dom: "bottom", viewport: "20%" },
onEnter(info) { console.log("entered", info.progress); },
onLeave(info) { console.log("left"); },
});observe(config) / obs(config)
Returns a ScrollObserverInstance with kill() and info.
const observer = scroll.observe({ trigger: "#el" });
observer.info.progress; // 0–1
observer.info.isActive; // boolean
observer.kill(); // remove this observerConfig
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| trigger | HTMLElement \| string | — | Element to observe (required) |
| start | ScrollPosition | "top bottom" | When the observer activates |
| end | ScrollPosition | "bottom top" | When the observer deactivates |
| onEnter | (info) => void | — | Scrolling forward past start |
| onLeave | (info) => void | — | Scrolling forward past end |
| onEnterBack | (info) => void | — | Scrolling backward past end |
| onLeaveBack | (info) => void | — | Scrolling backward past start |
| onUpdate | (info) => void | — | Every frame while active |
| once | boolean | false | Auto-kill after first activation |
| markers | boolean | false | Show debug lines for start/end |
| toggleClass | string | — | Class added while active, removed otherwise |
| pin | boolean \| HTMLElement \| string | — | Pin an element between start and end. true pins the trigger. A string (CSS selector) or HTMLElement pins that specific element. Sticks at the start.viewport position |
ScrollPosition
start and end accept a string or an object:
// String form — "dom viewport"
start: "top 80%"
// Object form — self-documenting
start: { dom: "top", viewport: "80%" }Both dom and viewport tokens support named positions (top, center, bottom, left, right), percentages (80%), and offsets (top+=100, bottom-=50).
Pin
Pin an element in place between start and end scroll positions. The trigger element determines when pinning starts and ends, while pin specifies which element to pin.
pin: true— pins the trigger element itselfpin: "#element"— pins a specific element by CSS selectorpin: element— pins a specific HTMLElement
// Pin the trigger itself
scroll.observe({
trigger: "#section",
start: { dom: "top", viewport: "top" },
end: { dom: "bottom", viewport: "top" },
pin: true,
});
// Pin a different element based on the trigger's position
scroll.observe({
trigger: "#section",
start: { dom: "top", viewport: "top" },
end: { dom: "bottom", viewport: "top" },
pin: "#sticky-header",
onUpdate(info) {
console.log("pin progress:", info.progress);
},
});When pinning:
- Window mode: uses
position: fixed— GPU-composited, smooth on mobile Safari. A spacer element preserves document flow - Element mode: uses
transform: translateY()— element stays in flow, no spacer needed - When scrolling past
end, the element freezes at its last pinned position - Scrolling back before
startrestores the element to its original styles kill()anddestroy()clean up and restore original styles
Observer Info
| Property | Type | Description |
|----------|------|-------------|
| progress | number | 0–1 between start and end |
| isActive | boolean | Whether currently between start and end |
| state | "before" \| "active" \| "after" | Current state |
| direction | 1 \| -1 | Scroll direction |
| trigger | HTMLElement | The observed element |
Window vs Element Mode
Both modes use the same scroll-jacking approach: wheel is intercepted for smooth damping, touch and programmatic scrolls sync via native scroll events.
Window (default) — drives scroll via window.scrollTo().
Element — pass a wrapper to enable. Drives scroll via scrollTop/scrollLeft. The wrapper element should have overflow: auto or overflow: scroll.
Ticker
Shared singleton RAF loop. Used internally by SmoothScroll.
Usage
import { Ticker, getDeltaFrame } from "situs-kit/ticker";
const off = Ticker.add((elapsed) => {
const df = getDeltaFrame(); // 1.0 = 60fps frame
object.x += speed * df;
});
off(); // unsubscribeAPI
Ticker.add(callback): () => void
Add a callback to the loop. Returns an unsubscribe function. The callback receives elapsed (ms since added).
Ticker.remove(callback): void
Remove a callback by reference.
getDeltaFrame(): number
Normalized delta frame. 1.0 at 60fps, 2.0 at 30fps, 0.5 at 120fps.
