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

@zekee-senju/nocturne

v1.0.0

Published

Dependency-free theme transition animations for UI, powered by the View Transitions API.

Readme

@zekee-senju/nocturne

Dependency-free theme transition animations for UI, powered by the View Transitions API.

Nocturne handles the trigger — checking browser support and wrapping your theme switch in document.startViewTransition(). The actual look of the animation (circle reveal, blur, wipe, GIF mask, etc.) lives entirely in CSS, so you can swap or customize it without touching JS.

📦 Install

npm install @zekee-senju/nocturne

No runtime dependencies. React is an optional peer dependency — only needed if you use the /react entry point.

🚀 Quick start

Vanilla JS / TS

import { nocturne } from "@zekee-senju/nocturne";
import "@zekee-senju/nocturne/styles.css";

document.documentElement.classList.add("nocturne-circle");

button.addEventListener("click", () => {
  nocturne(() => {
    const isDark = document.documentElement.classList.toggle("dark");
    localStorage.setItem("theme", isDark ? "dark" : "light");
  });
});

React

import { useState } from "react";
import { useNocturne } from "@zekee-senju/nocturne/react";
import "@zekee-senju/nocturne/styles.css";

function App() {
  const [theme, setTheme] = useState<"light" | "dark">("light");

  const toggleTheme = useNocturne(() => {
    const next = theme === "light" ? "dark" : "light";
    setTheme(next);
    document.documentElement.classList.toggle("dark", next === "dark");
  });

  return <button onClick={toggleTheme}>Toggle theme</button>;
}

Add a variant class to your root <html> element (see Choosing a variant below):

<html lang="en" class="nocturne-circle">

That's the whole setup. Click the button — the theme flips with an animated reveal instead of an instant snap.

🧩 API

| Export | Where | What it does | |---|---|---| | nocturne(switchTheme) | @zekee-senju/nocturne | Runs switchTheme inside document.startViewTransition() if supported, otherwise calls it directly. Framework-agnostic — no DOM assumptions beyond what you do inside your own callback. | | useNocturne(switchTheme) | @zekee-senju/nocturne/react | Thin useCallback wrapper around nocturne() for React. Returns a stable function you can wire to onClick. |

nocturne() and useNocturne() both take one argument: a function that actually performs your theme change (toggling a class, updating state, writing to storage — whatever your app needs). Nocturne doesn't know or care what "switching theme" means for your app; it only handles when and how that function runs relative to the transition.

nocturne(switchTheme: () => void): void
useNocturne(switchTheme: () => void): () => void

🎨 Choosing a variant

Import the stylesheet once, then add exactly one variant class to your <html> element:

<link rel="stylesheet" />
<!-- or via JS import "@zekee-senju/nocturne/styles.css" -->

<html class="nocturne-circle">

| Class | Effect | |---|---| | nocturne-circle | Clean circular reveal, expanding from center | | nocturne-circle-blur | Same as above, with a soft blurred edge | | nocturne-corner | Blurred circle reveal pivoted to the top-left corner — good if your toggle button lives there | | nocturne-diagonal | Diagonal clip-path wipe, no image/mask needed | | nocturne-curtain | Vertical wipe from the top, like a curtain dropping | | nocturne-fade | Plain crossfade — no shape, just a smooth opacity transition | | nocturne-iris | Circle reveal with a slight overshoot before settling, like a camera aperture | | nocturne-gif | Reveal masked by an image/GIF you provide (see below) |

Only apply one variant class at a time — they all target the same ::view-transition-* pseudo-elements, so combining them will cause conflicting animations.

GIF variant setup

nocturne-gif doesn't ship with an image baked in (shipping someone else's GIF permanently in an npm package isn't great practice). Set the mask image yourself via a CSS custom property:

html.nocturne-gif {
  --nocturne-gif-url: url('/your-image-or-gif.gif');
}

Note: browsers generally rasterize an animated GIF used as a CSS mask to its first frame — you won't see the GIF itself animate, but its shape becomes the reveal mask, which is then scaled up by Nocturne's own @keyframes. It's a shape-based reveal using an arbitrary image, not "the GIF plays inside the transition."

⚠️ Setup nuances

The .dark class convention. Every variant's CSS is written expecting a .dark class on your document root (html.dark) to represent dark mode. If your project uses a different convention — data-theme="dark", a class on <body> instead of <html>, a different class name entirely — the shipped variants won't respond correctly out of the box. You'll need to either adjust your theme-switch logic to add .dark to <html>, or copy the relevant CSS block from styles.css and adjust the selector to match your setup.

Browser support. document.startViewTransition is currently supported in Chromium-based browsers (Chrome, Edge, Opera) and not in Firefox or Safari. Nocturne detects this automatically — in unsupported browsers, your switchTheme callback still runs, the theme still changes, it just happens instantly with no animation. Nothing breaks, nothing needs a fallback on your end.

The variant class lives on <html>, not in React state. Because ::view-transition-* pseudo-elements are evaluated against the DOM at the moment the transition fires, the variant class needs to be a real class present on the root element — not something conditionally rendered by React on every render. Set it once, statically, in your HTML (or via a useEffect that runs once on mount if you need it dynamic).

One switchTheme call per transition. Don't call your theme-switching logic both inside the nocturne()/useNocturne() callback and separately in a useEffect watching theme state — that causes the DOM class toggle to run twice per click, which can produce inconsistent or double-applied styles. Let the callback you pass to nocturne() be the single source of truth for the actual switch.

CSS import is separate from the JS import. nocturne/useNocturne only handles the trigger logic — importing @zekee-senju/nocturne/styles.css is a required, separate step. Forgetting it means nocturne() will still run and your theme will still switch, but with no animation, since there's no CSS for the browser to animate with.

📝 Notes

  • Zero runtime dependencies.
  • React is an optional peer dependency — plain JS/TS users don't need it installed.
  • Import the CSS once, anywhere before first use.
  • All animation logic lives in CSS — inspect styles.css directly if you want to build a custom variant on top of the existing ones.

License

ISC