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

@dotopototo/phosphor-vue

v0.4.0

Published

CRT effect for live DOM — Vue 3 directive + component. Barrel distortion, scanlines, phosphor glow, chromatic fringing, noise, jitter, flicker, tracking artifacts.

Readme

@dotopototo/phosphor-vue

CRT visual + audio effect for Vue 3 — barrel, scanlines, phosphor glow, chromatic fringing, and optional programmatic audio (hum, whine, pops, ticks).

Install

bun add @dotopototo/phosphor-vue

Import the stylesheet once in your app entry:

import '@dotopototo/phosphor-vue/style.css';

Usage

Four ways, pick whichever fits.

Plugin + directive (most common):

// main.ts
import { createApp } from 'vue';
import { PhosphorPlugin } from '@dotopototo/phosphor-vue';
import '@dotopototo/phosphor-vue/style.css';
import App from './App.vue';

createApp(App).use(PhosphorPlugin).mount('#app');
<template>
  <div v-crt="{ theme: 'phosphor' }">
    your content here
  </div>
</template>

Component wrapper (same effect, slot-based):

<script setup lang="ts">
import { CrtSurface } from '@dotopototo/phosphor-vue';
</script>

<template>
  <CrtSurface :options="{ theme: 'amber' }">
    your content here
  </CrtSurface>
</template>

Composable (when you want reactive state alongside the effect):

<script setup lang="ts">
import { ref, useTemplateRef } from 'vue';
import { useCrt } from '@dotopototo/phosphor-vue';

const host = useTemplateRef<HTMLElement>('host');
const options = ref({ theme: 'phosphor' as const, transition: true });
const { snapshot, attached, powered, phase } = useCrt(host, options);
</script>

<template>
  <div ref="host">your content here</div>
  <button v-if="attached && !powered" @click="options.powered = true">Power on</button>
</template>

Imperative (script-driven, framework-agnostic):

import { CRT } from '@dotopototo/phosphor-vue';

const controller = CRT.attach(myElement, { theme: 'apple' });
controller.update({ screen: { jitter: 0.2 } });
controller.detach();

Power & lifecycle

Toggle the effect on and off by flipping powered. With the directive, mutate the bound options object; with the composable or controller, call update({ powered }).

<template>
  <!-- Snap on/off (default) -->
  <div v-crt="{ powered }">…</div>

  <!-- Animated, library defaults (1200ms on / 800ms off) -->
  <div v-crt="{ powered, transition: true }">…</div>

  <!-- Custom durations — clamped to [500, 2000] ms or 0 for snap -->
  <div v-crt="{ powered, transition: { on: 1500, off: 600 } }">…</div>
</template>

The composable surfaces three reactive views over the controller's live state:

const { snapshot, attached, powered, phase } = useCrt(host, options);

// snapshot.value:  { attached, powered, phase }
// attached.value:  is the effect bound to the element right now?
// powered.value:   live visible state — true during on / powering-on / powering-off,
//                  false ONLY once the off animation has fully settled.
// phase.value:     'off' | 'powering-on' | 'on' | 'powering-off'

Use powered (not options.value.powered) for UI gating like a Power-on button — it stays true through the entire fade-out, so the button doesn't appear over the still-visible screen.

For non-Vue code, subscribe directly to the controller:

const c = CRT.attach(el, { transition: true });
let state = c.snapshot;
const unsubscribe = c.onChange((s) => { state = s; /* re-render */ });

controller.attached / controller.powered are plain getters and not reactive — onChange (or the useCrt composable, which uses it internally) is the reactive bridge.

Audio

Optional programmatic audio engine — hum, flyback whine, pops, ticks, zaps, plus power-on/off events. Imported from a separate sub-entry so consumers who don't need audio don't ship it:

import { CrtAudio, useCrtAudio } from '@dotopototo/phosphor-vue/audio';

Browsers block AudioContext until a user gesture. The composable holds a session iff BOTH master.enabled === true AND powerIntent === true. The audible startup / shutdown ceremony only plays alongside an animated visual transition — snap on/off stays silent.

<script setup lang="ts">
import { ref } from 'vue';
import { CRT } from '@dotopototo/phosphor-vue';
import { CrtAudio, useCrtAudio } from '@dotopototo/phosphor-vue/audio';

const visualOpts = ref({ powered: true, transition: true });
const audioOpts = ref(CrtAudio.defaults);

const { running, needsGesture, start, audition } = useCrtAudio(
  audioOpts,
  () => visualOpts.value.powered,                              // power coordination
  () => CRT.normalize(visualOpts.value).transition,            // ceremony fires only on animated transitions
);

// Enabling audio in owner mode: flip master.enabled (the source of truth)
// AND call start() in the same click handler. The flip keeps the persisted
// option in sync; start() resumes the AudioContext synchronously inside
// the user-gesture window (the autoplay policy requires gesture-driven
// resume). Skipping start() would push the resume into the reconcile
// watcher's microtask, which may fall outside the gesture window.
function onEnable() {
  audioOpts.value.master.enabled = true;
  void start();
}
</script>

<template>
  <button @click="onEnable">Enable audio</button>
  <button @click="audition('startup')">Audition startup</button>
</template>

12 named presets ship as a frozen registry:

import { CRT_AUDIO_PRESETS } from '@dotopototo/phosphor-vue/audio';
audioOpts.value = structuredClone(CRT_AUDIO_PRESETS.bunkerTerminal);

(roomCrt, cleanOffice, bunkerTerminal, oldTv, dyingTube, palWorkstation, arcadeMonitor, distantRoom, whisper, brandNew, failingPower, serverRoom.)

Options

Rather than mirror every field in this README — they'd drift — explore the options two ways:

  • TypeScript types. Your editor surfaces every field with its bounds and defaults as you author config. The relevant types: CrtOptions, ResolvedCrtOptions, CrtAudioOptions, ResolvedCrtAudioOptions, CrtControllerSnapshot, CrtAudioState.
  • Live playground. Clone the repo, run bun run dev, tinker visually, hit Copy Settings to paste the JS literal of your overrides into your code.

If you need bounds at runtime (e.g. building your own settings UI), the same registries the library uses internally are exported:

import { CRT_FIELDS, type CrtNumericFieldPath } from '@dotopototo/phosphor-vue';
import { AUDIO_FIELDS } from '@dotopototo/phosphor-vue/audio';

CRT_FIELDS['screen.barrel'];    // { type: 'number', default: 0.018, min: 0, max: 0.035 }
AUDIO_FIELDS['hum.level'];      // { type: 'number', default: 0.34, min: 0, max: 1 }

Every row is deeply frozen. Keys are type-checked — typos are compile errors.

CRT.defaults and CrtAudio.defaults are the frozen active defaults — i.e. with mode exclusivity applied. CRT.normalize(opts) and CrtAudio.normalize(opts) show how an input resolves.

Caveats

  • The element you hand to CRT is a positioning shell, not a layout container. When CRT attaches, it moves your children into an internal .crt-content wrapper and adds an overlay sibling. Author your layout one level deeper:

    <!-- bad: grid on the v-crt host breaks when the wrapper is inserted -->
    <div v-crt style="display: grid; grid-template-rows: auto 1fr auto">
      <header/><main/><footer/>
    </div>
    
    <!-- good: grid lives in your own inner div -->
    <div v-crt>
      <div style="display: grid; grid-template-rows: auto 1fr auto; height: 100%">
        <header/><main/><footer/>
      </div>
    </div>
  • CSS is separate. The one stylesheet import is required; there's no auto-inject because it doesn't play nicely with SSR.

  • SSR-safe import. Importing the package server-side won't crash. CRT.normalize() and CRT.defaults work anywhere. DOM-touching methods (attach, attachAll, prime) throw a clear "requires a browser environment" error if called server-side — keep them inside onMounted / useEffect. The v-crt directive and <CrtSurface> component are already mount-gated.

  • Modern Chromium only. Chrome, Edge, Brave, Arc, recent Electron. Firefox and Safari may render acceptably but no compatibility work is invested.

  • Glow + ancestor overflow: hidden. Text glow is a text-shadow halo around 30–40px wide. Any ancestor with overflow: hidden (including text-overflow: ellipsis) clips the halo at the box edge, leaving visible hard edges. Put clipping on a wrapper with enough padding, or move it higher up the tree.

  • Form controls. Native rendering (appearance: button / textfield) draws text via the OS and ignores text-shadow. To get glow + chromatic on form controls, set appearance: none and theme them yourself — standard practice for terminal/CRT UIs.

  • Position. If the host has position: static, the library sets it to relative so the overlay can anchor. Authored absolute / fixed / sticky is preserved.

License

MIT. See LICENSE.