@stackonward/composables
@stackonward/composables
Headless Vue composables for recurring countdowns, polling, infinite scroll, waterfall layout, scroll-parent discovery, and native video control. The package contains no product models, components, or styling.
Install
pnpm add @stackonward/composables vue
Install @vueuse/core when using polling, infinite scroll, or waterfall layout:
pnpm add @vueuse/core
Available composables
| API | Purpose | Browser dependency |
|---|---|---|
useCountdown |
Persist and display a recurring countdown | localStorage, timers |
usePolling |
Poll until completion or a business timeout | VueUse interval lifecycle |
useInfiniteScroll |
Load pages from an intersection sentinel | VueUse IntersectionObserver |
computeWaterfallPositions |
Pure shortest-column masonry calculation | none |
useWaterfallLayout |
Responsive masonry positions from container width | VueUse element sizing |
findScrollParent |
Resolve the nearest scrollable ancestor | DOM |
useVideoPlayer |
Native video state, controls, fullscreen, and Picture-in-Picture | browser media APIs |
isAutoplayBlockedError |
Classify browser autoplay-policy rejections | none |
Countdown
import { onMounted, onUnmounted } from "vue";
import { useCountdown } from "@stackonward/composables";
const countdown = useCountdown({
duration: 15 * 60,
storageKeyFn: () => "checkout-offer",
});
onMounted(countdown.start);
onUnmounted(countdown.stop);
remaining, display, and isActive are readonly reactive values. When the
duration reaches zero, the composable clears the stored start time and begins a
new interval. start() is an SSR-safe no-op; the owner must call stop() when
its lifecycle ends.
Polling
import { usePolling } from "@stackonward/composables";
const polling = usePolling(
async () => {
const job = await getJob();
return job.status === "completed";
},
{
interval: 5000,
timeout: 180000,
onTimeout: () => showTimeoutMessage(),
onError: (error) => reportPollingError(error),
},
);
polling.startPolling();
Returning true stops polling; false continues. Callback failures continue
until completion or timeout, so production callers should always provide
onError. Vue scope disposal stops the interval and timeout automatically.
Infinite scroll
import { ref } from "vue";
import { useInfiniteScroll } from "@stackonward/composables";
const trigger = ref<HTMLElement | null>(null);
const items = ref<Item[]>([]);
const scroll = useInfiniteScroll({
triggerRef: trigger,
fetchFn: (page) => api.listItems({ page }),
onData: (next, loadMore) => {
items.value = loadMore ? [...items.value, ...next] : next;
},
});
await scroll.fetch();
The composable detects the nearest scroll parent, ignores concurrent loads, and
uses a generation counter so responses started before reset() cannot replace
newer state. An empty or null page sets hasMore to false.
Waterfall layout
import { ref } from "vue";
import { useWaterfallLayout } from "@stackonward/composables";
interface ImageItem {
width: number;
height: number;
}
const container = ref<HTMLElement | null>(null);
const layout = useWaterfallLayout(container, {
gap: 16,
aspectRatio: (item: ImageItem) => item.height / item.width,
});
layout.distribute(images);
Render positionedItems in their original flat DOM order and bind each
generated style; set the container height from containerHeight.
computeWaterfallPositions provides the same calculation without Vue or DOM
access.
Native video
<script setup lang="ts">
import { useVideoPlayer } from "@stackonward/composables";
const { containerElement, state, togglePlay, videoElement } = useVideoPlayer({
muted: true,
keyboardShortcuts: true,
});
</script>
<template>
<div ref="containerElement">
<video ref="videoElement" src="/demo.mp4" />
<button type="button" @click="togglePlay">
{{ state.playing ? "Pause" : "Play" }}
</button>
</div>
</template>
The instance exposes readonly media state plus play, pause, seek, volume,
playback-rate, fullscreen, Picture-in-Picture, reset, and destroy controls.
Autoplay-policy rejections remain distinguishable through
isAutoplayBlockedError; play() still rejects so the caller can update its UI.
Compatibility
- Vue
^3.4.0 - Optional VueUse
^11,^12, or^13for the composables identified above - Modern browsers for DOM, storage, observer, fullscreen, Picture-in-Picture, and media APIs
- ESM with TypeScript declarations
Related packages
@stackonward/utilsprovides the underlying scroll-parent utility.
License
MIT
Related Packages
Framework-agnostic dnd-kit grid engine for snapgrid — the shared drag/resize/cro...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.1.0 | ^3.4.0 | MIT | |
| 0.0.2 | ^3.4.0 | MIT | |
| 0.0.1 | ^3.4.0 | MIT |