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

@circadian/luna

v0.1.3

Published

Lunar-aware React widgets that follow the real phase of the moon.

Readme

Lunar-aware React widgets that follow the real phase of the moon.

npm · GitHub · circadian.dev


Sol follows the sun. Luna follows the moon.

Most UIs stop at light mode and dark mode. Sol pushed that further — an interface that reacts to the real position of the sun. Luna is the other half of that story.

Luna computes the moon's real phase, illumination, and arc position from the user's location and current time, then transitions the interface through 8 lunar phases — new, waxing crescent, first quarter, waxing gibbous, full, waning gibbous, last quarter, and waning crescent — with smooth blends, arc-tracking orbs, and 10 richly designed skins.

No API key. No manual toggle. Your UI just follows the moon.

Luna is the companion package to @circadian/sol, and the foundation for the upcoming @circadian/ambient — a unified layer that morphs between Sol and Luna as the sun sets and rises.


bun add @circadian/luna
# or
npm install @circadian/luna
# or (Deno / Fresh)
deno add npm:@circadian/luna

@circadian/luna gives you a full LunaWidget, a CompactLunaWidget, 10 skins, 8 lunar phases, and a dev-only phase + arc scrubber via LunaDevTools. Lunar position is computed locally from latitude, longitude, timezone, and current time using Meeus astronomical algorithms — no external API required.


Features

  • 2 widget variantsLunaWidget (full card) and CompactLunaWidget (slim pill/bar)
  • 10 skinsfoundry, paper, signal, meridian, mineral, aurora, tide, void, sundial, parchment
  • 8 lunar phasesnew, waxing-crescent, first-quarter, waxing-gibbous, full, waning-gibbous, last-quarter, waning-crescent
  • Real moon tracking — orb follows the moon's arc from moonrise → zenith → moonset
  • Illumination display — real-time percentage (0% new moon → 100% full moon)
  • Moonrise/moonset times — computed locally, no API
  • Built-in fallback strategy — geolocation → browser timezone → timezone centroid
  • Dev preview toolingLunaDevTools lets you pick phases and scrub the arc position
  • SSR-safe — works in Next.js, Remix, TanStack Start, Blade, Fresh, and Vite

Quick Start

Luna uses browser APIs for geolocation and lunar computation. The exact setup depends on your framework — pick yours below.


Vite

No special setup needed. Wrap your app with the provider and use widgets directly.

// main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { LunaThemeProvider } from '@circadian/luna';
import App from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <LunaThemeProvider initialDesign="void">
      <App />
    </LunaThemeProvider>
  </StrictMode>,
);
// App.tsx
import { LunaWidget } from '@circadian/luna';

export default function App() {
  return <LunaWidget showIllumination showMoonrise />;
}

Next.js (App Router)

Add 'use client' at the top of any file that uses Luna. This marks it as a client component and prevents it from running during server rendering.

// components/providers.tsx
'use client';
import { LunaThemeProvider } from '@circadian/luna';

export default function Providers({ children }: { children: React.ReactNode }) {
  return (
    <LunaThemeProvider initialDesign="void">
      {children}
    </LunaThemeProvider>
  );
}
// components/luna-widget.tsx
'use client';
import { LunaWidget } from '@circadian/luna';

export default function Luna() {
  return <LunaWidget showIllumination showMoonrise />;
}
// app/layout.tsx
import Providers from '../components/providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
// app/page.tsx
import Luna from '../components/luna-widget';

export default function Page() {
  return <Luna />;
}

Remix

Name any file that uses Luna with a .client.tsx extension. Remix excludes .client files from the server bundle automatically.

// app/components/providers.client.tsx
import { LunaThemeProvider } from '@circadian/luna';

export default function Providers({ children }: { children: React.ReactNode }) {
  return (
    <LunaThemeProvider initialDesign="void">
      {children}
    </LunaThemeProvider>
  );
}
// app/components/luna-widget.client.tsx
import { LunaWidget } from '@circadian/luna';

export default function Luna() {
  return <LunaWidget showIllumination showMoonrise />;
}
// app/root.tsx
import Providers from './components/providers.client';

export default function App() {
  return (
    <html lang="en">
      <body>
        <Providers>
          <Outlet />
        </Providers>
      </body>
    </html>
  );
}
// app/routes/_index.tsx
import Luna from '../components/luna-widget.client';

export default function Index() {
  return <Luna />;
}

TanStack Start

Use the ClientOnly component from @tanstack/react-router to prevent Luna from rendering during SSR.

// app/components/luna-widget.tsx
import { ClientOnly } from '@tanstack/react-router';
import { LunaThemeProvider, LunaWidget } from '@circadian/luna';

export default function Luna() {
  return (
    <ClientOnly fallback={null}>
      <LunaThemeProvider initialDesign="void">
        <LunaWidget showIllumination showMoonrise />
      </LunaThemeProvider>
    </ClientOnly>
  );
}
// app/routes/index.tsx
import Luna from '../components/luna-widget';

export const Route = createFileRoute('/')({
  component: () => <Luna />,
});

Blade

Name any file that uses Luna with a .client.tsx extension. Blade runs pages server-side; component files run client-side.

// components/providers.client.tsx
import { LunaThemeProvider } from '@circadian/luna';

export default function Providers({ children }: { children: React.ReactNode }) {
  return (
    <LunaThemeProvider initialDesign="void">
      {children}
    </LunaThemeProvider>
  );
}
// components/luna-widget.client.tsx
import { LunaWidget } from '@circadian/luna';

export default function Luna() {
  return <LunaWidget showIllumination showMoonrise />;
}
// pages/layout.tsx
import Providers from '../components/providers.client';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return <Providers>{children}</Providers>;
}
// pages/index.tsx
import Luna from '../components/luna-widget.client';

export default function Page() {
  return <Luna />;
}

Fresh (v2)

Fresh uses Preact, so Luna works via preact/compat. Add the React compatibility aliases to your vite.config.ts and deno.json, then create an island for the widget.

1. Install

deno add npm:@circadian/luna

2. Configure Vite aliases — add resolve.alias to vite.config.ts:

// vite.config.ts
import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";

export default defineConfig({
  plugins: [fresh()],
  resolve: {
    alias: {
      "react": "preact/compat",
      "react-dom": "preact/compat",
      "react/jsx-runtime": "preact/jsx-runtime",
      "react/jsx-dev-runtime": "preact/jsx-runtime",
      "react-dom/client": "preact/compat/client",
    },
  },
});

3. Add import map entries — add to the "imports" in deno.json:

// deno.json (imports section)
{
  "imports": {
    "react": "npm:preact@^10.27.2/compat",
    "react-dom": "npm:preact@^10.27.2/compat",
    "react/jsx-runtime": "npm:preact@^10.27.2/jsx-runtime",
    "react-dom/client": "npm:preact@^10.27.2/compat/client"
  }
}

4. Create an island — islands are client-hydrated in Fresh, which is what Luna needs:

// islands/LunaWidget.tsx
import { LunaThemeProvider, LunaWidget } from '@circadian/luna';

export default function LunaIsland() {
  return (
    <LunaThemeProvider initialDesign="void">
      <LunaWidget showIllumination showMoonrise />
    </LunaThemeProvider>
  );
}

5. Use it in a route:

// routes/index.tsx
import { define } from "../utils.ts";
import LunaIsland from "../islands/LunaWidget.tsx";

export default define.page(function Home() {
  return <LunaIsland />;
});

Provider Props

LunaThemeProvider is the shared runtime for lunar phase computation, timezone, coordinates, and skin selection.

| Prop | Type | Default | Description | |---|---|---|---| | children | ReactNode | — | Required | | initialDesign | DesignMode | 'void' | Starting skin | | latitude | number \| null | — | Override latitude | | longitude | number \| null | — | Override longitude | | timezone | string \| null | — | Override timezone |

Location is automatic

LunaThemeProvider resolves the user's location using the same 3-step fallback as Sol:

  1. Browser Geolocation API — most accurate, requires user permission
  2. Browser timezone (Intl.DateTimeFormat) — instant, no permission needed
  3. Timezone centroid lookup — maps the IANA timezone to approximate coordinates

Lunar phases and moonrise/moonset times are computed locally using Meeus astronomical algorithms — no external API required.


LunaWidget

The full card widget. Reads its design from the nearest LunaThemeProvider.

<LunaWidget
  expandDirection="top-left"
  size="lg"
  showIllumination
  showMoonrise
/>

Props

| Prop | Type | Default | Description | |---|---|---|---| | expandDirection | LunaExpandDirection | 'bottom-right' | Direction the card expands | | size | LunaWidgetSize | 'lg' | Widget size (xs, sm, md, lg, xl) | | showIllumination | boolean | true | Show illumination percentage | | showMoonrise | boolean | true | Show moonrise/moonset times | | simulatedDate | Date | — | Simulate a specific time | | forceExpanded | boolean | — | Lock expanded or collapsed state | | hoverEffect | boolean | false | Enable hover animation | | className | string | — | Wrapper CSS class |


CompactLunaWidget

The slim pill/bar variant. Always uses the provider's active skin.

<CompactLunaWidget
  size="md"
  showIllumination
  showMoonrise
/>

Props

| Prop | Type | Default | Description | |---|---|---|---| | size | CompactLunaSize | 'md' | Compact size (sm, md, lg) | | showIllumination | boolean | true | Show illumination percentage | | showMoonrise | boolean | true | Show moonrise/moonset times | | simulatedDate | Date | — | Simulate a time | | className | string | — | Wrapper CSS class |


Skins

10 designs, each with a full widget and compact variant. Luna shares the same skin names as Sol — when used together in @circadian/ambient, both widgets use the same active skin.

type DesignMode =
  | 'aurora'      // luminous ethereal
  | 'foundry'     // cold steel by moonlight
  | 'tide'        // fluid organic wave
  | 'void'        // minimal negative space
  | 'mineral'     // faceted crystal gem
  | 'meridian'    // hairline geometric
  | 'signal'      // pixel/blocky lo-fi
  | 'paper'       // silver ink on dark stock
  | 'sundial'     // classical carved latin
  | 'parchment';  // manuscript illustrated

LunaDevTools

When your interface depends on the live moon, manual testing is impossible — you can't wait two weeks for a full moon. LunaDevTools gives you two independent controls: a phase picker (8 lunar phases) and an arc slider (moonrise → zenith → moonset), so you can preview every phase and every orb position instantly.

Imported from a dedicated subpath — never included in production bundles unless explicitly imported.

import { LunaDevTools } from '@circadian/luna/devtools';

// Vite
{import.meta.env.DEV && <LunaDevTools />}

// Next.js / Remix / TanStack Start / Blade
{process.env.NODE_ENV === 'development' && <LunaDevTools />}

Full example

import { LunaThemeProvider, LunaWidget } from '@circadian/luna';
import { LunaDevTools } from '@circadian/luna/devtools';

export default function Demo() {
  return (
    <LunaThemeProvider initialDesign="void">
      <LunaWidget showIllumination showMoonrise />
      {process.env.NODE_ENV === 'development' && (
        <LunaDevTools position="bottom-center" />
      )}
    </LunaThemeProvider>
  );
}

Props

| Prop | Type | Default | Description | |---|---|---|---| | defaultOpen | boolean | false | Start expanded | | position | 'bottom-left' \| 'bottom-center' \| 'bottom-right' | 'bottom-left' | Panel position | | enabled | boolean | true | Programmatic enable/disable |


useLunaTheme

import { useLunaTheme } from '@circadian/luna';

function DebugPanel() {
  const { phase, illumination, moonProgress, isVisible, design } = useLunaTheme();
  return (
    <pre>{JSON.stringify({ phase, illumination, moonProgress, isVisible, design }, null, 2)}</pre>
  );
}

Return shape

| Property | Type | Description | |---|---|---| | phase | LunarPhase | Current lunar phase | | illumination | number | 0–1 illumination (0 = new, 1 = full) | | ageInDays | number | 0–29.53 days into the lunar cycle | | daysUntilFull | number | Days until next full moon | | daysUntilNew | number | Days until next new moon | | moonriseMinutes | number \| null | Moonrise in minutes from midnight | | moonsetMinutes | number \| null | Moonset in minutes from midnight | | moonProgress | number | 0–1 arc position (0 = rise, 0.5 = zenith, 1 = set) | | isVisible | boolean | Whether the moon is above the horizon | | isReady | boolean | Whether lunar data has computed | | design | DesignMode | Active skin name | | activeSkin | LunaSkinDefinition | Full skin definition object | | accentColor | string | Active accent hex | | setDesign | (skin: DesignMode) => void | Change active skin | | setOverridePhase | (phase \| null) => void | Set/clear phase override | | setSimulatedDate | (date \| undefined) => void | Set/clear simulated date |


useLunarPosition

Standalone hook — no provider required. Use this to build your own lunar-aware components.

import { useLunarPosition } from '@circadian/luna';

function MoonInfo() {
  const moon = useLunarPosition();

  return (
    <div>
      <p>Phase: {moon.phase}</p>
      <p>Illumination: {Math.round(moon.illumination * 100)}%</p>
      <p>Visible: {moon.isVisible ? 'Yes' : 'No'}</p>
    </div>
  );
}

Options

| Option | Type | Default | Description | |---|---|---|---| | latitude | number \| null | 51.5074 | Latitude | | longitude | number \| null | -0.1278 | Longitude | | timezone | string \| null | 'Europe/London' | IANA timezone | | updateIntervalMs | number | 60000 | Update interval in ms | | simulatedDate | Date | — | Override current time |


Multiple Widgets

<LunaThemeProvider initialDesign="void">
  <LunaWidget showIllumination />
  <CompactLunaWidget />
  <LunaWidget showMoonrise />
</LunaThemeProvider>

The provider manages shared lunar state — location, phase, and position are computed once and shared across all children.


TypeScript

import type {
  LunarPhase,
  LunarPosition,
  LunaThemeContext,
  LunaSkinDefinition,
  LunarPalette,
  LunarSkinPalettes,
  LunaWidgetProps,
  LunaExpandDirection,
  LunaWidgetSize,
  CompactLunaWidgetPublicProps,
  CompactLunaSize,
} from '@circadian/luna';

What's Included

| | | |---|---| | ✅ | Full widget + compact widget | | ✅ | 10 skins with full + compact variants | | ✅ | Lunar math (Meeus algorithms, no external API) | | ✅ | Real moonrise/moonset computation | | ✅ | Moon arc tracking (rise → zenith → set) | | ✅ | Illumination percentage | | ✅ | Timezone fallback logic | | ✅ | Dev phase + arc scrubber | | ✅ | Self-contained CSS (no Tailwind required in host app) | | ✅ | SSR-safe (Next.js, Remix, TanStack Start, Blade, Fresh, Vite) | | ❌ | No API key needed | | ❌ | No Tailwind needed in your app | | ❌ | No geolocation permission required |


Coming Soon

Luna is the second piece of the Circadian platform. Next up:

  • @circadian/ambient — a unified layer that morphs between Sol and Luna automatically as the sun sets and rises, with crossfade transitions and synced skin selection
  • More skins
  • Vue and Svelte adapters

MIT © Circadian