@stackonward/composables

Framework-agnostic Vue composables — headless technical capabilities, no styling or business logic
372
Install
npm install @stackonward/composables
Latest Version:0.1.0
Vue:3
License:MIT
Last Updated:Oct 15, 2026
Links: npm
Maintainer: yhy2001

@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 ^13 for the composables identified above
  • Modern browsers for DOM, storage, observer, fullscreen, Picture-in-Picture, and media APIs
  • ESM with TypeScript declarations

Related packages

  • @stackonward/utils provides the underlying scroll-parent utility.

License

MIT

Related Packages

@snapgridjs/dnd

Framework-agnostic dnd-kit grid engine for snapgrid — the shared drag/resize/cro...

248,702 35
@vt7/vue2-composables

Vue2 composables

523 1
@shopware/composables

Shopware Frontends composables for Vue

141,216 230
@tb-dev/vue

Vue utilities

12,617
revdojo-vue-components

This is vue 3 component

4,676

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