spax-utils
v0.1.0
Published
Small viewport utilities for lazy media, reveal animations, and active sections in single-page apps.
Maintainers
Readme
spax-utils
Small viewport utilities for single-page apps: lazy media, reveal animations, active sections, and cleanup-friendly observers.
spax-utils is framework-agnostic. Use it in plain HTML, React, Vue, Svelte, Astro islands, or any app that needs lightweight scroll/viewport behavior without shipping a large animation package.
Features
- Lazy load
<img>,<picture>, and CSS background images. - Adds native hints like
loading="lazy",decoding="async", andfetchpriority="low". - Reveal elements when they enter the viewport.
- Keep navigation links active while scrolling sections.
- Shared controller API with
disconnect()for SPA route cleanup. - Respects
prefers-reduced-motionby default for reveal animations. - No dependencies.
Install
npm install spax-utilsQuick Start
import { createViewportManager } from "spax-utils";
const viewport = createViewportManager();
viewport.lazyImages();
viewport.reveal(".reveal");
viewport.sections("section[id]", {
linkSelector: ".nav-link"
});
// Call this when a SPA route/page is destroyed.
// viewport.disconnect();Lazy Images
Use data-src for deferred image sources:
<img
data-src="/images/project.jpg"
alt="Finished painting project"
width="800"
height="600"
/>import { lazyImages } from "spax-utils";
const controller = lazyImages("img[data-src]", {
rootMargin: "250px 0px",
loadedClass: "is-loaded",
onLoad: (target) => target.classList.add("fade-in")
});For <picture>:
<picture data-lazy>
<source data-srcset="/images/project.avif" type="image/avif" />
<source data-srcset="/images/project.webp" type="image/webp" />
<img data-src="/images/project.jpg" alt="Project" width="800" height="600" />
</picture>For background images:
<div class="hero-photo" data-bg-src="/images/hero.jpg"></div>lazyImages("[data-bg-src]");Reveal on Scroll
.reveal {
opacity: 0;
transform: translateY(16px);
transition: opacity 0.4s ease, transform 0.4s ease;
}
.reveal.show {
opacity: 1;
transform: translateY(0);
}import { observeReveal } from "spax-utils";
observeReveal(".reveal", {
className: "show",
rootMargin: "80px 0px",
once: true
});Active Sections
<nav>
<a class="nav-link" href="#services">Services</a>
<a class="nav-link" href="#work">Work</a>
</nav>
<section id="services"></section>
<section id="work"></section>import { observeSections } from "spax-utils";
observeSections("section[id]", {
linkSelector: ".nav-link",
activeClass: "active",
rootMargin: "-35% 0px -55% 0px"
});Generic Viewport Observer
import { observeViewport } from "spax-utils";
const controller = observeViewport(".metric", {
once: true,
threshold: 0.4,
onEnter: (element) => {
element.dataset.visible = "true";
}
});Every observer returns a controller:
controller.refresh();
controller.unobserve(element);
controller.disconnect();SPA Cleanup Examples
React:
import { useEffect } from "react";
import { createViewportManager } from "spax-utils";
export function Page() {
useEffect(() => {
const viewport = createViewportManager();
viewport.lazyImages();
viewport.reveal(".reveal");
viewport.sections("section[id]", { linkSelector: ".nav-link" });
return () => viewport.disconnect();
}, []);
return null;
}Vue:
import { onMounted, onUnmounted } from "vue";
import { createViewportManager } from "spax-utils";
let viewport;
onMounted(() => {
viewport = createViewportManager();
viewport.lazyImages();
viewport.reveal(".reveal");
});
onUnmounted(() => {
viewport?.disconnect();
});API
lazyImages(targets?, options?)
loadLazyImage(element, options?)
observeReveal(targets?, options?)
observeSections(targets?, options?)
observeViewport(targets, options?)
createViewportManager(defaults?)Common target values:
- CSS selector string, for example
".reveal". - Single
Element. NodeList, array, or any iterable of elements.
Publish
From this folder:
npm run check
npm run pack:dry
npm publishFor a scoped package, change the name in package.json to something like @your-scope/spax-utils.
Browser Support
spax-utils is built on IntersectionObserver. Browsers without it load deferred images immediately and reveal elements without scroll observation.
License
MIT
