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

@pixelmatters/tweaks

v1.3.0

Published

Headless dev-only scenario switcher: a config-driven tweaks store and panel (React and Vue) for forcing unhappy paths in sandbox and demo apps.

Readme

@pixelmatters/tweaks

npm license bundle

A dev-only panel for forcing the states a demo can't normally reach: a failed load, a save that errors, an expired session, a forbidden page. A floating button opens a small non-modal card, you pick a scenario, and the page behind it reacts.

Each screen declares its scenarios in a config. The panel renders controls from that config, and the page reads the chosen values back and branches on them. The store and panel logic are framework-free, with bindings for React and Vue.

What you get

  • Scenarios are data. You write a config and the panel renders it, so your app contains no panel UI code.
  • The types follow the config. defineTweaksConfig rejects duplicate ids, a default that isn't one of the field's options, and ids widened to string, all at the call site. config.useTweak(id) autocompletes ids and returns that field's value type.
  • /scenarios has ready-made fields for the unhappy paths every project ends up needing (load state, action outcome, session timeout, permission denied, unsaved changes), so they behave the same in every app.
  • Forced values are saved to sessionStorage, so a scenario survives the reload it often triggers. Each screen's panel also remembers whether it was open.
  • Reads work without a provider and return the default. Put the provider and panel behind import.meta.env.DEV and production renders the happy path with no changes at the call sites.
  • The panel uses native controls and injects its own stylesheet from JS: no design system dependency, no CSS import. Restyle it with CSS variables and classNames.
  • For your own UI, useTweaksPanel gives you sections, the badge count, reset and open state without any markup.
  • A crashing field renderer shows an error inside the card instead of taking down your app.
  • The server render and hydration read the store as empty, so hydration matches the default HTML.
  • Controls have real labels, segmented fields are radio groups, Escape only acts inside the card, focus goes back to the button on close, and reduced motion is respected.

Install

# pnpm
pnpm add -D @pixelmatters/tweaks
# or yarn
yarn add -D @pixelmatters/tweaks
# or npm
npm install -D @pixelmatters/tweaks

Peer dependencies are React 19+ or Vue 3.5+. Both are optional; install the one your app uses. The package ships compiled ESM with .d.ts types, and the CSS is inlined into the JS, so your bundler doesn't need to handle it.

Quickstart

Mount the provider once, at the app root:

import { TweaksProvider } from '@pixelmatters/tweaks/react'

createRoot(document.getElementById('root')!).render(
  <TweaksProvider storageKey="my-app.tweaks">
    <App />
  </TweaksProvider>,
)

Then declare a screen's scenarios, read them through the config, and render the panel on that screen:

import { defineTweaksConfig, TweaksPanel } from '@pixelmatters/tweaks/react'

const CONTACTS_TWEAKS = defineTweaksConfig({
  id: 'contacts',
  title: 'Scenarios',
  sections: [
    {
      title: 'Data state',
      fields: [
        {
          id: 'loadState',
          label: 'Load state',
          kind: 'enum',
          default: 'loaded',
          options: [
            { value: 'loaded', label: 'Loaded' },
            { value: 'empty', label: 'Empty — no contacts yet' },
            { value: 'error', label: 'Failed to load' },
          ],
        },
      ],
    },
  ],
})

function Contacts() {
  const loadState = CONTACTS_TWEAKS.useTweak('loadState')
  //    ^? 'loaded' | 'empty' | 'error'
  // …branch on loadState…
  return <TweaksPanel config={CONTACTS_TWEAKS} />
}

The common cases already exist as ready-made fields:

import { loadStateField, systemOutcomeField } from '@pixelmatters/tweaks/scenarios'

const CONTACTS_TWEAKS = defineTweaksConfig({
  id: 'contacts',
  title: 'Scenarios',
  sections: [
    { title: 'Data state', fields: [loadStateField()] },
    { title: 'Actions', fields: [systemOutcomeField('saveOutcome', 'Save contact')] },
  ],
})

Keeping it out of production

Without a provider, useTweak returns the field's default. Put the provider and panel behind your dev flag and every call site renders the happy path in production:

{
  import.meta.env.DEV ? (
    <TweaksProvider storageKey="my-app.tweaks">{children}</TweaksProvider>
  ) : (
    children
  )
}

The write hooks (useTweaksControls, config.useControls, useTweaksPanel) throw without a provider. That's on purpose, since they should only ever run in dev-gated code.

Entry points

| Import | Contents | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | @pixelmatters/tweaks | Framework-free core: createTweaksStore, the TweaksConfig schema and types, the define<Kind>TweakField factories, and the panel logic a custom renderer needs (resolveSections, countActive, resolvePresentation, resolveTweakValue, resolveRecordValue) | | @pixelmatters/tweaks/react | TweaksProvider, defineTweaksConfig, the hooks, the default <TweaksPanel>, and the headless useTweaksPanel | | @pixelmatters/tweaks/vue | The same surface for Vue: TweaksProvider, defineTweaksConfig, the composables, the default <TweaksPanel>, and the headless useTweaksPanel (Vue) | | @pixelmatters/tweaks/scenarios | Ready-made field definitions, id constants, and value-union types for the recurring unhappy-path families |

Fields

There are three kinds. kind says what the value means; how it renders is a separate, optional presentation hint.

kind: 'enum'

One value from a fixed set. With three options or fewer it renders as a segmented control, otherwise as a select. resolvePresentation makes that call, or you set presentation: 'segmented' | 'select' yourself. Short lists with long labels usually read better as 'select'. 'segmented' with more than five options is a compile error, because it won't fit in the card.

{
  id: 'loadState',
  label: 'Load state',
  kind: 'enum',
  default: 'loaded',
  options: [{ value: 'loaded', label: 'Loaded' }, { value: 'error', label: 'Failed' }],
  hint: 'Optional helper text, rendered under the row and wired as aria-describedby',
}

Option values are what the page reads back, so treat them as API: keep them stable and change the labels instead.

kind: 'boolean'

A switch.

{ id: 'forceUnsavedChanges', label: 'Force unsaved changes', kind: 'boolean', default: false }

kind: 'action'

A one-shot trigger, like "deliver the webhook now", as opposed to a state you leave switched on. Actions don't go through the store. Nothing is persisted, Reset ignores them, they don't count toward the badge, and a handler that subscribes after a press won't see it.

{ id: 'deliverWebhook', label: 'Deliver webhook', kind: 'action' }

Pages handle them with useTweakAction(id, handler). If you catch yourself modeling a trigger as an enum with a 'none' value that the page writes back after acting on it, use an action instead.

Field families

When the same field shows up on several screens, define it once with define<Kind>TweakField. You pass the shared defaults and get back a builder. Each call to the builder makes a field and can override any of them: id, label, hint, default, presentation, or individual option labels.

import { defineEnumTweakField } from '@pixelmatters/tweaks'

const loadStateField = defineEnumTweakField({
  id: 'loadState',
  label: 'Load state',
  default: 'loaded',
  options: [
    { value: 'loaded', label: 'Loaded' },
    { value: 'loading', label: 'Loading' },
    { value: 'empty', label: 'Empty' },
    { value: 'error', label: 'Failed to load' },
  ],
})

loadStateField() // the family as-is
loadStateField({ emptyLabel: 'Empty — no results yet' }) // per-option label override
loadStateField({ id: 'detailLoadState', default: 'error' }) // same family, second instance

Per-option label overrides are named after the option values ({ value: 'empty' } becomes emptyLabel). If you leave id and label out of the defaults, the builder requires them on every call. That's how systemOutcomeField(id, label) works, since each action needs its own id.

Build fields with the factories, or with defineTweakField for one-offs. Don't write a helper annotated : TweakField instead. The annotation widens the id to string, which turns the whole config's value map into an index signature: useTweak stops checking ids and everything still compiles. defineTweaksConfig rejects a widened config to catch this.

Ready-made scenarios

@pixelmatters/tweaks/scenarios exports field definitions, id constants, and the value types pages branch on:

| Export | Kind | What it forces | | ------------------------------------------------------------- | ------- | --------------------------------------------- | | loadStateField / LOAD_STATE_ID / LoadState | enum | 'loaded' \| 'loading' \| 'empty' \| 'error' | | systemOutcomeField(id, label) / SystemOutcome | enum | 'success' \| 'error' for one named action | | sessionTimeoutField / SESSION_TIMEOUT_ID | boolean | Session expired | | sessionTimeoutWarningField / SESSION_TIMEOUT_WARNING_ID | boolean | Session about to expire | | permissionDeniedPageField / PERMISSION_DENIED_PAGE_ID | boolean | Whole page forbidden | | permissionDeniedActionField / PERMISSION_DENIED_ACTION_ID | boolean | One action forbidden | | forceUnsavedField / FORCE_UNSAVED_ID | boolean | Dirty-state / unsaved-changes guards |

The package stops there. The dialog, gate, guard or toast that responds to a scenario is your app's code, so the scenarios stay consistent across apps while each app keeps its own look.

Reading and writing

Through a config (preferred)

defineTweaksConfig returns the config with hooks bound to it, in the style of TanStack Router:

  • config.useTweak(id): the id must be one of the config's fields, and the return type comes from that field. There's no fallback argument because the field's default is the fallback. A stored value the field no longer accepts (after an option is renamed, say) also resolves to the default.
  • config.useTweakAction(id, handler): the id must be one of the config's action fields. useTweak rejects action ids at compile time, and useTweakAction rejects everything else.
  • config.useRecord(id): reads one of the config's records (see below). It always returns a string; anything unset or of the wrong type reads as ''.
  • config.useControls(): write access, with setTweak(id, value) for fields (the value limited to what the field declares) and setRecord(id, value) for records. It doesn't subscribe to any values, so a component that only writes won't re-render when something else changes.

Standalone hooks

These are for ids that don't belong to a config, which in practice means the provider's globalSections (below):

  • useTweak(id, fallback): the fallback also sets the return type.
  • useTweakAction(id, handler) and useTweaksControls(): unscoped versions of the config hooks.
  • useRawTweakValue(id): the stored value as-is, or undefined. Custom renderers build on this.

Don't read a config's field through the standalone useTweak. The provider only knows about its global fields, so the value skips stale-value resolution. That's how you end up with a page forcing a scenario while the badge, the controls and Reset all say everything is at its default.

Records

config.records are store keys the panel has no control for, such as a restored file, an approval or a returned document. A flow in the page writes them with setRecord:

const APPROVALS_TWEAKS = defineTweaksConfig({
  id: 'approvals',
  title: 'Scenarios',
  sections: [/* … */],
  records: ['lastDecision'],
})

const decision = APPROVALS_TWEAKS.useRecord('lastDecision')

The badge counts every record a flow has written ('' and '{}' count as nothing written), and Reset clears records along with fields. Otherwise Reset would claim to undo a decision the screen still remembers.

The provider

<TweaksProvider storageKey="my-app.tweaks" globalSections={GLOBAL_SECTIONS}>
  • storageKey (optional): the sessionStorage key for forced values. Without it nothing persists. Open state is stored per config id, under ${storageKey}:open:${configId}, so each screen remembers its own.
  • globalSections (optional): sections that apply everywhere, like session timeout or permission denied. They're appended to every panel and read with the standalone hooks. An inline array is fine; the provider compares contents and won't recreate the context on every render.
  • webmcp (optional, off by default): exposes the mounted panels to browser agents as WebMCP tools. See Agent tools (WebMCP).

Give every config a unique config.id. It keys the persisted open state, the data-config-id attribute on the wrapper (handy as an e2e or debug selector), and the dev-only warning for field ids that collide with globalSections, which fires once per config id.

Theming

The panel comes in light and dark. Pick one with the theme prop: 'light' (the default), 'dark', or 'system', which follows prefers-color-scheme. If your app has its own theme toggle, pass its current value so the panel follows it. The prop takes only those three values, so narrow a looser type first.

'system' is resolved in CSS, so the server can render it without knowing the answer. A theme that's only known in the browser needs care under server rendering. Neither React nor Vue corrects an attribute that differs between the server's HTML and the first client render, so the panel would keep the server's theme until the value next changed. Render 'system' until the component has mounted (useEffect in React, onMounted in Vue). With next-themes, whose resolvedTheme is undefined on the server:

const { resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])

<TweaksPanel
  config={CONTACTS_TWEAKS}
  theme={!mounted ? 'system' : resolvedTheme === 'dark' ? 'dark' : 'light'}
/>

To restyle the panel, redeclare the --px-tweaks-* variables on the .px-tweaks wrapper. Two things decide whether your values win:

  • Declare them on the wrapper, not :root. The defaults live on the wrapper, and a value declared there beats an inherited one.
  • Keep them out of any @layer. The panel's defaults are in a cascade layer that's added when the panel mounts. Layers rank in the order they first appear, so it outranks every layer your stylesheet declared before then, Tailwind's base and components included. Unlayered rules beat all layers.

The wrapper carries the theme as data-px-tweaks-theme. A value on plain .px-tweaks applies to both themes; scope one to a single theme with the attribute:

.px-tweaks {
  --px-tweaks-font: var(--font-sans);
}

.px-tweaks[data-px-tweaks-theme='light'] {
  --px-tweaks-accent: var(--color-primary);
}

.px-tweaks[data-px-tweaks-theme='dark'] {
  --px-tweaks-accent: var(--color-primary-dark);
}

With theme="system", the attribute reads system in both themes, so the rules above don't match. Scope the light values to system and repeat the selector inside a media query for the dark ones:

.px-tweaks[data-px-tweaks-theme='system'] {
  --px-tweaks-accent: var(--color-primary);
}

@media (prefers-color-scheme: dark) {
  .px-tweaks[data-px-tweaks-theme='system'] {
    --px-tweaks-accent: var(--color-primary-dark);
  }
}

| Variable | What it colors / sets | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ | | --px-tweaks-bg | Card, select and switch-thumb background, and the base every hover and tint is mixed toward | | --px-tweaks-fg | Foreground text | | --px-tweaks-border | Card, FAB, and control borders | | --px-tweaks-muted-fg | Section titles, hints, the select's chevron, and the tint of the segmented track, the switch-off track and hovers | | --px-tweaks-accent | FAB, selected segment, switch-on track | | --px-tweaks-accent-fg | Text and glyphs on the accent color | | --px-tweaks-badge | FAB badge background, shown while a scenario is forced | | --px-tweaks-badge-fg | FAB badge text. Set it with --px-tweaks-badge, so the count stays readable on your color | | --px-tweaks-danger | The message shown in place of fields that failed to render | | --px-tweaks-ring | Focus ring | | --px-tweaks-radius | The card's corner radius (default 1rem). Controls are fully round | | --px-tweaks-font | Font family. Defaults to the host page's font, and it's the only variable a :root declaration reaches. Sizes are fixed | | --px-tweaks-z | z-index of FAB and card (default 9999) |

Variables starting --_px-tweaks- are internal and can change in any release.

For per-element classes, pass classNames to <TweaksPanel>. They're appended to the package's own classes, the way sonner does it:

<TweaksPanel config={CONTACTS_TWEAKS} classNames={{ fab: 'bottom-left', card: 'shadow-xl' }} />

Keys: root, fab, badge, card, header, title, resetButton, closeButton, body, section, sectionTitle, row, label, hint, select, segmented, segment, switch, action, crash.

Colors and the font face come from the variables above, not from classNames. The panel's own rules set them on its elements and load after your stylesheet, so a color or font utility in classNames can lose to them. Use classNames for layout, spacing and position.

You can move the FAB with classNames.fab. The card is attached to it with CSS anchor positioning and flips sides to stay on screen.

Stacking

The FAB and card are position: fixed with z-index: var(--px-tweaks-z), so they sit above the host's UI in the stacking context they render in. If the host isolates its root (#root { isolation: isolate }, for example), a dialog portalled to body paints above the panel. That's intended: a forced dialog covers the panel, and the panel is back once the dialog closes. Raising --px-tweaks-z won't change this, because z-index isn't compared across an isolation boundary.

Anything rendered inside the card has to stay inside it. A portal out of the card leaves its stacking context and paints behind it.

Custom renderer

useTweaksPanel(config) is <TweaksPanel> without the markup. It has all the behavior and injects no styles:

function MyPanel({ config }: { config: TweaksConfig }) {
  const panel = useTweaksPanel(config)
  // panel.open / panel.setOpen  — persisted open state
  // panel.sections              — config + globalSections, resolved
  // panel.activeCount           — fields forced away from default + records written
  // panel.setValue(id, value)   — write one field
  // panel.fire(id)              — fire one action field
  // panel.reset()               — reset, scoped to this panel's ids
}

There's no values map, on purpose: subscribing the panel to every value would re-render every control on every write. Have each control read its own id with useRawTweakValue(id) and resolve it with resolveTweakValue(field, value), which is what the default renderer does. resolvePresentation(field) returns the default segmented/select choice.

Error handling

The default panel has two error boundaries, so a bug in a dev tool can't unmount your app:

  • The inner one wraps the card's controls, which is where crashes actually happen (usually a field shape a renderer didn't expect). It shows a message in their place and leaves the header working. Closing and reopening the card remounts the controls.
  • The outer one wraps the rest of the panel and renders nothing if it catches an error.

Both log the config id to the console. Your pages' own useTweak calls live in your app's tree, outside both boundaries, so a bad id there still fails as an error in your app.

Accessibility

  • Every control is named by its row label. Selects and switches get a <label for>, so clicking the label works, and segmented fields are a role="radiogroup" with aria-labelledby. A field's hint becomes its aria-describedby.
  • Escape closes the card only when focus is inside it, and the event stops there, so a host dialog underneath doesn't close too.
  • Opening the card from the FAB moves focus into it. A card that's restored already open on page load leaves focus where it is. Closing returns focus to the FAB.
  • Under prefers-reduced-motion, the open/close animation is cut to almost nothing.

Agent tools (WebMCP)

WebMCP lets a page register tools that a browser agent can call. With the webmcp prop, the provider registers four of them, so an agent can force a scenario with one typed call instead of finding the panel and clicking through it:

<TweaksProvider storageKey="my-app.tweaks" webmcp>

| Tool | What it does | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | | tweaks_list | Every field of every mounted panel and of globalSections, with its options, default and current value, plus the records | | tweaks_set | Forces one or more values, e.g. { "values": { "loadState": "empty" } }. All or nothing: an invalid id or value writes nothing | | tweaks_reset | Clears everything the tools cover, as the panel's Reset does | | tweaks_trigger | Fires an action field, e.g. { "id": "receiveContact" }. Registered only when a mounted panel or globalSections has actions |

  • The tools cover every panel that's mounted (the default <TweaksPanel> and custom renderers built on useTweaksPanel alike), plus globalSections. They update as panels mount and unmount. Two panels share one set of tools.
  • The input schemas come from your configs, so an agent sees each field's ids and allowed values before calling. Invalid input comes back as { "error": "…" } naming the problem, rather than as a thrown error, because Chrome replaces a thrown error's message with a generic one.
  • When a call returns, the page already shows its result, including updates made by an action's handler, so an agent can read the DOM straight away.
  • Nothing is registered during a server render, in a browser without document.modelContext, or while the prop is off. The prop only exposes what the panel already does, so gate it the same way you gate the provider.
  • Only one provider per page should turn it on. A second one's tools clash on name and wait (you get a console warning). Once the first provider unmounts, they take over the next time one of the second provider's panels mounts or unmounts.
  • If you install a WebMCP polyfill, install it before the provider mounts. The provider looks for document.modelContext when its panels change, not continuously.

This is experimental, like the spec it follows. The tool names, inputs and outputs may change in a minor release as WebMCP settles. As of Chrome 153, native support is behind the origin trial or --enable-features=WebMCP. Chrome DevTools MCP can call the tools with --categoryExperimentalWebmcp.

SSR

The panel is safe to render on the server. In React, the store goes through useSyncExternalStore with a server snapshot that reports it as empty, so server HTML always shows defaults and values restored from sessionStorage can't cause a hydration mismatch. The Vue provider gets the same result its own way: it reads the store as empty during the server render and hydration, then shows the restored values once mounted. Neither needs configuring.

Vue

@pixelmatters/tweaks/vue matches the React entry: the same config, fields, scenarios, provider props, panel markup, theming and behavior. Hooks become composables, and the config type is TweaksConfigWithComposables. Everything above applies; this section covers the differences.

Mount the provider once around your app:

<!-- App.vue -->
<script setup lang="ts">
import { TweaksProvider } from '@pixelmatters/tweaks/vue'

const dev = import.meta.env.DEV
</script>

<template>
  <TweaksProvider v-if="dev" storage-key="my-app.tweaks">
    <RouterView />
  </TweaksProvider>
  <RouterView v-else />
</template>

Declare scenarios with the Vue entry's defineTweaksConfig and read them in setup. Reads return a ComputedRef, so they're reactive and unwrap in the template:

<script setup lang="ts">
import { defineTweaksConfig, TweaksPanel } from '@pixelmatters/tweaks/vue'
import { loadStateField } from '@pixelmatters/tweaks/scenarios'

const CONTACTS_TWEAKS = defineTweaksConfig({
  id: 'contacts',
  title: 'Scenarios',
  sections: [{ title: 'Data state', fields: [loadStateField()] }],
})

const loadState = CONTACTS_TWEAKS.useTweak('loadState')
//    ^? ComputedRef<'loaded' | 'loading' | 'empty' | 'error'>
const dev = import.meta.env.DEV
</script>

<template>
  <p v-if="loadState === 'error'" role="alert">Failed to load contacts.</p>
  <!-- … -->
  <TweaksPanel v-if="dev" :config="CONTACTS_TWEAKS" :class-names="{ fab: 'bottom-left' }" />
</template>
  • Reads: config.useTweak(id), config.useRecord(id), useTweak(id, fallback) and useRawTweakValue(id) return a ComputedRef. Typing and fallbacks work as in React.
  • Actions: useTweakAction(id, handler) stays subscribed for as long as the calling component (or effect scope) is alive.
  • Writes: config.useControls() and useTweaksControls() return plain setters, same as React.
  • Headless: useTweaksPanel(config) accepts a config, a ref or a getter. open, sections and activeCount come back as refs, next to the same setOpen, setValue, fire and reset. Read each control with useRawTweakValue(id) rather than a values map, for the same reason as in React.
  • Errors: the panel's boundaries catch render and setup errors inside the card. An error thrown by your own useTweakAction handler when someone presses the action goes to your app's errorHandler, the same way it would get past a React boundary.

Call composables from setup or <script setup>, since they get the provider through inject.

Requirements

  • React 19+ (with react-dom) or Vue 3.5+, as optional peer dependencies. Install the one your app uses.
  • The card uses CSS anchor positioning where the browser supports it. Elsewhere it falls back to fixed insets and works the same.
  • @tanstack/store is an internal dependency. It doesn't appear in the public types and may be replaced without a breaking change.

License

MIT