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

@charstudios/pallet

v0.3.1

Published

Runtime, hook-driven theming layer for shadcn/ui apps. Full control over colors, fonts, roundness, elevation, spacing and motion, with swappable visual variants.

Readme

@charstudios/pallet

A runtime, hook-driven theming layer for shadcn/ui apps. Pallet gives you and your users advanced, live control over colors, fonts, roundness, elevation, spacing, and motion, plus swappable visual variants ("skins") — all through a small React API that works with your existing, unmodified shadcn components.

Built for reuse across every Char Studios product: install it once, wrap your app, and every project shares the same theming system.

  • Runtime theming — change any token live; no rebuild, no CSS regeneration.
  • Variants/skins — ship flat (crisp), toon (cel-shaded lips on surfaces only), and frosted (quiet glass) — or define your own.
  • Full token control — colors (light + dark, OKLCH), typography (with Google Fonts auto-loading), radius scale, shadow ramp, density, and motion.
  • Presets — start from a named preset and layer overrides on top; reset() returns to baseline.
  • SSR-safe — inline the initial theme from a Server Component to avoid a flash of the wrong theme.
  • Persistence — optionally remember a user's theme in localStorage.

Table of contents


How it works

shadcn/ui styles everything through CSS custom properties (--primary, --radius, --font-sans, ...). Pallet:

  1. Takes a ThemeConfig (a preset + your overrides).
  2. Resolves it into a flat map of CSS variables for the active color scheme.
  3. Injects those variables onto the document root (or a scoped element) at runtime.
  4. Sets a data-pallet-variant attribute so the shipped variant stylesheets can restyle surfaces (lips, glass, borders) that aren't expressible through tokens alone.
ThemeConfig (preset + overrides)
        │  resolveTheme()
        ▼
CSS variables  ──inject──▶  :root / scope element
        │                         │
        │                    data-pallet-variant="toon"
        ▼                         ▼
your shadcn components  ◀── variant stylesheet skins

Your components never change. You only add a provider and (optionally) a stylesheet import.


Installation

npm install @charstudios/pallet
# or
pnpm add @charstudios/pallet

Peer dependencies: react >= 18 and react-dom >= 18 (works with React 19). Assumes a shadcn/ui + Tailwind setup using CSS variables (the shadcn default).


Quick start

1. Import the styles

Add this once to your global stylesheet (e.g. app/globals.css), after Tailwind:

@import "tailwindcss";
@import "@charstudios/pallet/styles";

This registers the base token fallbacks and all built-in variant skins. (You can import individual skins instead — see Variants.)

2. Wrap your app

// app/layout.tsx  (or your root component)
import { ThemeProvider } from "@charstudios/pallet";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider defaultPreset="toon" defaultScheme="system" storageKey="app-theme">
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

That's it — your shadcn components now render with the toon theme, honoring system light/dark, and any user changes persist under app-theme.

3. Change the theme at runtime

"use client";
import { useThemeControls, usePreset, useColorScheme } from "@charstudios/pallet";

export function ThemeButtons() {
  const { setPrimary, setRadius, reset } = useThemeControls();
  const { applyPreset } = usePreset();
  const { toggle } = useColorScheme();

  return (
    <div className="flex gap-2">
      <button onClick={() => applyPreset("toon")}>Toon</button>
      <button onClick={() => applyPreset("frosted")}>Frosted</button>
      <button onClick={() => applyPreset("flat")}>Flat</button>
      <button onClick={() => setPrimary("oklch(0.7 0.15 300)")}>Purple</button>
      <button onClick={() => setRadius("0.25rem")}>Sharp corners</button>
      <button onClick={toggle}>Toggle dark</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Presets

Each built-in preset pairs a token palette with a matching variant skin. The previews below use the same component gallery (buttons, inputs, badges, tabs, alerts, select, card, avatar) so you can compare Flat, Toon, and Frosted side by side.

flat

Crisp hairline borders, minimal shadows, tighter radius — the default shadcn feel.

toon

Cel-shaded / neo-brutalist skin for real surfaces only (cards, buttons, inputs, badges, tabs, alerts, overlays…):

  • Moderate radius, no soft shadows
  • Outline and thick bottom lip share one auto-darkened color from --toon-fill (color-mix toward black)
  • Ghost buttons stay borderless until hover / focus
  • Layout shells, sidebar nav rows, and wrappers stay flat
  • Main Sidebar panel is square-edged (hard right edge, no rounded lip tile)

frosted

Quiet glass: light backdrop blur, hairline luminous borders, translucent fills — restrained, not heavy.

import { presets, flat, toon, frosted, getPreset } from "@charstudios/pallet/presets";

presets.flat; // ThemeConfig
getPreset("toon"); // ThemeConfig | undefined
getPreset("frosted"); // ThemeConfig | undefined

Set the starting preset via defaultPreset on the provider, or switch live with usePreset().applyPreset(...).


Hooks API

All hooks must be used within a <ThemeProvider>.

useTheme()

Returns the full context.

  • theme: ThemeConfig — the resolved theme (preset + overrides).
  • basePreset: ThemeConfig — the active preset before overrides.
  • overrides: ThemeOverride — the current overrides.
  • scheme: "light" | "dark" — the actual scheme rendered.
  • schemePreference: "light" | "dark" | "system" — the user's preference.
  • vars: Record<string, string> — resolved CSS variables for the current scheme.
  • setTheme(theme) — replace the whole theme (clears overrides).
  • applyPreset(preset) — apply a preset by name or object (clears overrides).
  • updateTheme(override) — deep-merge a partial override.
  • resetOverrides() — clear overrides.
  • setSchemePreference(pref) — set light/dark/system.

useThemeControls()

Granular setters for building an editor UI. Color setters default to updating both light and dark; pass a target ("light" | "dark" | "both") to scope them.

  • setColor(role, value, target?)
  • setPrimary(value, target?)
  • setAccent(value, target?)
  • setColors(partialColors, target?)
  • setRadius(base) — e.g. "0.5rem", "1.25rem"
  • setRadiusScale(partialScale)
  • setFont("sans" | "heading" | "mono", family)
  • setGoogleFonts(families) — auto-loads them at runtime
  • setVariant(name) — switch the skin
  • setElevationLevel("none" | "xs" | "sm" | "md" | "lg" | "xl")
  • setShadow(key, value)
  • setBorderStrength(0..1)
  • setDensity({ spacing?, scale? })
  • setMotion({ duration?, easing?, intensity? })
  • update(override) — escape hatch
  • reset() — clear overrides

usePreset()

  • preset: ThemeConfig — the active base preset.
  • builtinNames: string[] — names of built-in presets.
  • applyPreset(preset) — switch presets.

useColorScheme()

  • scheme: "light" | "dark" — the resolved scheme.
  • preference: "light" | "dark" | "system".
  • setPreference(pref).
  • toggle() — flip light/dark.

Building a theme editor

Everything you need to build a settings panel is exposed as hooks. A minimal example:

"use client";
import { useTheme, useThemeControls } from "@charstudios/pallet";

export function ThemeEditor() {
  const { theme, scheme } = useTheme();
  const c = useThemeControls();

  return (
    <div className="space-y-4">
      <label>
        Primary
        <input
          type="color"
          onChange={(e) => c.setPrimary(e.target.value)}
        />
      </label>

      <label>
        Roundness: {theme.radius.base}
        <input
          type="range"
          min={0}
          max={2}
          step={0.05}
          onChange={(e) => c.setRadius(`${e.target.value}rem`)}
        />
      </label>

      <label>
        Heading font
        <select onChange={(e) => { c.setFont("heading", e.target.value); c.setGoogleFonts([e.target.value]); }}>
          <option value="Inter">Inter</option>
          <option value="Plus Jakarta Sans">Plus Jakarta Sans</option>
          <option value="Outfit">Outfit</option>
        </select>
      </label>

      <label>
        Variant
        <select onChange={(e) => c.setVariant(e.target.value)}>
          <option value="flat">Flat</option>
          <option value="toon">Toon</option>
          <option value="frosted">Frosted</option>
        </select>
      </label>

      <button onClick={c.reset}>Reset</button>
    </div>
  );
}

Tip: <input type="color"> emits hex; that works fine as a CSS color. Prefer OKLCH strings for perceptual consistency with the presets when you can.


Variants (skins)

A variant is a named "skin" applied via the data-pallet-variant attribute (set automatically by the provider). The shipped stylesheets restyle shadcn surfaces that tokens can't reach on their own — chunk lips, glass, elevation treatment, and so on.

Skins target shadcn data-slot parts. They intentionally do not restyle every slot: layout shells (sidebar chrome, field groups, scroll areas, menu rows, etc.) stay flat so apps don't look like every div is a tile.

| Skin | What it does | | --- | --- | | flat | Hairline borders, tight radius, minimal elevation | | toon | Cel-shaded outline + thick lip on surfaces; ghost borders on hover; square Sidebar | | frosted | Light backdrop blur, hairline luminous borders, translucent fills |

Import all skins:

@import "@charstudios/pallet/styles";

Or cherry-pick to reduce CSS:

@import "@charstudios/pallet/styles/base";
@import "@charstudios/pallet/styles/toon";

Switch at runtime:

useThemeControls().setVariant("toon");

The variant stylesheets are wrapped in the pallet-variants cascade layer, so your own app styles always take precedence.


Server-side rendering (no flash)

For app-wide theming Pallet writes variables to <html> on mount. To avoid a brief flash of the wrong theme on first paint, inline the theme's CSS from a Server Component using the server-safe entry:

// app/layout.tsx (Server Component)
import { themeToCss } from "@charstudios/pallet/server";
import { toon } from "@charstudios/pallet/presets";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <style
          id="pallet-ssr"
          dangerouslySetInnerHTML={{ __html: themeToCss(toon) }}
        />
      </head>
      <body>{/* <ThemeProvider>…</ThemeProvider> */}</body>
    </html>
  );
}

themeToCss emits structural tokens + light colors on :root and dark colors on .dark. The provider then takes over for any live/persisted changes.

Scoped theming / previews: use scope="self" on the provider to apply the theme to a wrapper <div> instead of the document root. This also emits SSR inline styles automatically — ideal for rendering a live preview of a different theme inside a settings page.

<ThemeProvider defaultPreset="toon" scope="self">
  <PreviewSurface />
</ThemeProvider>

Custom presets and variants

Custom preset

A preset is just a ThemeConfig. Start from a built-in and tweak:

import { toon } from "@charstudios/pallet/presets";
import { deepMerge } from "@charstudios/pallet/server";
import type { ThemeConfig } from "@charstudios/pallet/server";

export const brand: ThemeConfig = deepMerge(toon, {
  name: "brand",
  variant: "toon",
  colors: { light: { primary: "oklch(0.62 0.2 15)" }, dark: { primary: "oklch(0.6 0.2 15)" } },
  radius: { base: "0.75rem" },
});

Register it on the provider so applyPreset("brand") works:

<ThemeProvider defaultPreset={brand} presets={{ brand }}>

Custom variant (skin)

  1. Pick a name and set it: setVariant("neo") (or variant: "neo" in a preset).
  2. Add a stylesheet that targets [data-pallet-variant="neo"] [data-slot="..."]:
@layer pallet-variants {
  [data-pallet-variant="neo"] [data-slot="card"] {
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
  }
  [data-pallet-variant="neo"] [data-slot="button"] {
    border-radius: 0; /* brutalist square buttons */
  }
}

shadcn components expose data-slot on every part (card, button, input, select-trigger, popover, dropdown-menu-content, dialog-content, sheet-content, badge, ...), which is what the skins hook into.


Token reference

Resolved CSS variables written by Pallet:

  • Colors (per scheme): --background, --foreground, --card(-foreground), --popover(-foreground), --primary(-foreground), --secondary(-foreground), --muted(-foreground), --accent(-foreground), --destructive(-foreground), --border, --input, --ring, --chart-1..5, --sidebar*.
  • Radius: --radius plus the derived scale --radius-sm | md | lg | xl | 2xl | 3xl | 4xl.
  • Typography: --font-sans, --font-heading, --font-mono.
  • Elevation: --shadow-xs | sm | md | lg | xl, --elevation, --surface-border-strength (and the helper --pallet-surface-border).
  • Density: --spacing, --density.
  • Motion: --motion-duration, --motion-ease, --motion-intensity.

TypeScript

Fully typed. Import types from the server entry (safe everywhere):

import type {
  ThemeConfig,
  ThemeOverride,
  ColorTokens,
  Typography,
  RadiusConfig,
  ElevationConfig,
  Density,
  Motion,
  ColorScheme,
  VariantName,
} from "@charstudios/pallet/server";

Entry points

| Import path | Contents | Environment | | -------------------------------- | ---------------------------------------------------- | ---------------------- | | @charstudios/pallet | ThemeProvider, hooks (+ re-exports of the below) | Client components | | @charstudios/pallet/server | themeToCss, resolveTheme, deepMerge, types, … | Server or client | | @charstudios/pallet/presets | flat, toon, frosted, presets, getPreset | Server or client | | @charstudios/pallet/styles | All variant CSS | CSS @import | | @charstudios/pallet/styles/frosted | Frosted glass skin only | CSS @import |


FAQ

Does this modify my shadcn components? No. It only injects CSS variables and adds a data-pallet-variant attribute; the variant stylesheets restyle via data-slot selectors on real surfaces (not every layout wrapper). Components need modern shadcn data-slot attributes for the skins to attach.

Does it replace next-themes? It can. Pallet manages the .dark class and light/dark/system itself. If you already use next-themes, use scope="self" or let one own the .dark class to avoid conflicts.

Tailwind v3 or v4? Both work — Pallet is Tailwind-version agnostic; it only reads/writes CSS variables and ships plain CSS.

Can users export/share a theme? Yes. Use serializeTheme(theme) / deserializeTheme(json) from @charstudios/pallet/server, and feed a saved override back in via defaultOverride or updateTheme.

Why don't sidebar nav items get Toon lips? By design. Toon only chunks interactive surfaces (cards, buttons, inputs, overlays…). Sidebar shells stay square and menu rows stay flat so dense admin UIs don't look tiled.


MIT © Char Studios