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

habitat.css

v0.1.0

Published

Ambient-adapting CSS: eink and camo display modes driven by measured light and room color.

Readme

habitat.css

CI npm MIT license Pages

habitat (noun): the natural home or environment of an organism — the specific conditions of light, temperature, and surroundings it has adapted to live in.

habitat.css senses the light and color actually around you and lets your interface adapt to that habitat, instead of assuming a fixed one. Two modes, one sensor:

  • Eink mode — a warm paper display whose tone tracks measured ambient brightness, not a fixed sepia filter.
  • Camo mode — the page's palette is derived from the room's own dominant colors (median-cut on a blurred, immediately-discarded camera frame), with every color walked until it clears your chosen WCAG contrast target.

Either mode can also be forced toward a light or dark identity — see Color scheme below.

TypeScript-first, zero runtime dependencies, ships ESM/CJS/IIFE, works from npm, Deno, and the CDN of your choice.

Live demo · Landing page

Install

npm install habitat.css
pnpm add habitat.css
yarn add habitat.css
bun add habitat.css
// Deno
import { HabitatSensor } from "npm:habitat.css";
<!-- unpkg -->
<link rel="stylesheet" href="https://unpkg.com/habitat.css/dist/habitat.css">
<script src="https://unpkg.com/habitat.css/dist/habitat.global.js"></script>
<script>const sensor = new Habitat.HabitatSensor();</script>
// esm.sh
import { HabitatSensor } from "https://esm.sh/habitat.css";

Usage

<link rel="stylesheet" href="node_modules/habitat.css/dist/habitat.css">
import { HabitatSensor, initHabitatBanner } from "habitat.css";

const sensor = new HabitatSensor();
const banner = initHabitatBanner(sensor); // consent UI: banner + status pill

document.documentElement.dataset.habitatMode = "eink"; // or "camo" / "off"
banner.request(); // shows the consent banner if a mode wants the camera

Wrap the parts of the page eink's filter should apply to in .habitat-filter (fixed-position overlays like the consent banner deliberately sit outside it). Style everything else with habitat's semantic custom properties — --habitat-bg, --habitat-surface, --habitat-text, --habitat-text-muted, --habitat-accent, --habitat-border, --habitat-shadow — and every mode, present and future, works for free.

API

| Export | What it does | |---|---| | HabitatSensor | EventTarget subclass. Camera capture, median-cut, palette derivation, persistence. Emits habitatupdate and habitatstate (cast e to CustomEvent<HabitatUpdateDetail> / CustomEvent<HabitatStateDetail> to read .detail). | | initHabitatBanner(sensor) | Injects the consent banner + "camera active" status pill. Returns { request(), resume() }. | | headroom(el, opts?) | Dependency-free scroll-direction watcher: hides el on scroll-down, shows it on scroll-up. | | medianCut(pixelSource, count?) | Pure dominant-color extraction from any { data: ArrayLike<number> } (works with real ImageData or synthetic test data). | | deriveSemanticPalette(dominants, targets?, options?) | Pure: turns dominant colors into a contrast-guaranteed semantic palette. options.colorScheme forces it light or dark. | | contrast(a, b) | WCAG contrast ratio between two [r,g,b] triplets. | | CONTRAST_LEVELS | { aa: {text:4.5,accent:3}, aaa: {text:7,accent:3}, high: {text:10,accent:4.5} } |

HabitatSensor constructor options (all optional, backward-compatible defaults): captureIntervalMs (15000), blurPx (2), frameWidth (64), frameHeight (48), contrastLevel ("aa" | "aaa" | "high"), colorScheme ("light" | "dark", omit for automatic). sensor.setContrastLevel(level) and sensor.setColorScheme(scheme | null) change either afterward.

Full type definitions ship in the package — see dist/habitat.d.ts or your editor's autocomplete.

Color scheme

Automatic by default: everything follows prefers-color-scheme, same as any CSS. Force it toward light or dark instead — this shapes every mode, not just the base palette: off's tokens, eink's paper and filter (pinned to a fixed bright/dim endpoint instead of tracking measured brightness), and camo's derived palette (background lightness confined to that half of the range, ink pole fixed, contrast guarantee still enforced).

sensor.setColorScheme("dark"); // or "light", or null to go back to automatic

No JavaScript required at all if your app already manages its own theme — these are equivalent:

<html data-habitat-scheme="dark">
<html class="habitat-dark">
<html class="dark"> <!-- the same convention shadcn/next-themes use -->

shadcn/ui

habitat.css/habitat-shadcn.css maps habitat's semantic tokens onto shadcn/ui's full theming variable contract — --background/--foreground, --card, --popover, --primary, --secondary, --muted, --accent, --destructive, --border, --input, --ring, --chart-1--chart-5, the --sidebar-* family, and --radius — the same lightweight distribution model as shadcn's own Typeset. Load it after habitat.css and any shadcn/ui + Tailwind project re-themes under eink/camo modes automatically — no component changes:

<link rel="stylesheet" href="habitat.css">
<link rel="stylesheet" href="habitat-shadcn.css">

habitat.css's own dark handling (prefers-color-scheme, or a forced color scheme) already flows through --habitat-*, so this mapping needs no separate .dark block of its own. If your project uses Tailwind v4, add the matching @theme inline block from shadcn's own docs to your global CSS so utilities like bg-primary resolve. The chart tokens are a 5-step monochromatic ramp derived from the room's own accent color rather than 5 independent hues — introducing new colors would work against the "camouflage into the room" premise, so it's a deliberate choice, not a missing feature.

See it live: the landing page embeds a real shadcn-style component showcase (buttons, a card, badges, an alert) styled with nothing but this token set, re-theming live as you switch Off/Eink/Camo.

Privacy

Camo mode's camera use is consent-gated and minimal by construction: one 64×48 frame every 15 seconds (configurable), blurred in the same instant it's drawn, analyzed in-browser, then erased — at most one blurred frame exists at any moment, never shown, stored, or transmitted. Only a brightness number and a handful of hex colors persist, on-device, in localStorage.

Development

npm install
npm run typecheck   # tsc --noEmit
npm run build       # tsup (ESM/CJS/IIFE + .d.ts) + CSS build
npm test             # vitest — pure logic: median-cut, contrast, palette
                      # derivation (across aa/aaa/high), consent state
                      # machine, headroom pin/unpin
deno task test        # Deno-native proof of zero Node dependencies

License

MIT © Arvind