npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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, and mask are constrained by type. Setting style: { words: {} } when type: ["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-maskChar
  • data-maskWord
  • data-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 masks

Children 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 children

Multi-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 elements

Additional Behaviors

  • Letter spacing: Automatically applied to character elements when the parent has letter-spacing set
  • 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: scroll returns 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 observer

Config

| 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 itself
  • pin: "#element" — pins a specific element by CSS selector
  • pin: 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 start restores the element to its original styles
  • kill() and destroy() 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(); // unsubscribe

API

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.