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

soft-feedback

v0.1.0

Published

Soft, delightful, white-label feedback & micro-survey widgets your users actually want to answer. Headless, framework-agnostic, accessible. CSAT, NPS, CES.

Readme

Status: v0, in active development. Zero runtime dependencies. ~25 KB gzipped.


Why

Most in-app surveys are either ugly, annoying, or a hosted service that owns your data. soft-feedback is none of those:

  • Delightful by default — motion-native widgets (a morphing emoji dial, real emoji, springy reveals), reduce-motion safe.
  • Polite by default — nothing auto-fires on load; frequency-capped, sampled, never mid-task. The anti-irritation engine ships as defaults, not homework.
  • White-label & own-your-data — theme with CSS tokens, send responses anywhere you like (onSubmit, webhook, your DB). No hosted backend, cookieless/anonymous mode.
  • Framework-agnostic & isolated — vanilla Web Components with Shadow DOM, so styles can't collide with your app. Works in React, Vue, Svelte, or plain HTML.
  • A dashboard included — a static, local-first results dashboard that reads the exact payloads you store.

Install

npm i soft-feedback
import { soft } from 'soft-feedback';
// No CSS import — styles live inside each widget's Shadow DOM.

No <Provider>, no mount point, no stylesheet. Just call soft.*.


Quick start

import { soft } from 'soft-feedback';

// Register a survey that fires on one of your app events.
soft.csat({
  trigger: { type: 'event', name: 'ticket_resolved', delayMs: 800 },
  onSubmit: (p) => fetch('/api/feedback', { method: 'POST', body: JSON.stringify(p) }),
});

// Fire the moment when it's right. The engine decides if it's polite to show.
soft.track('ticket_resolved');

Want to show one immediately (e.g. on a button click)?

soft.nps().show(true); // bypasses the politeness gates

React

There's no adapter to learn — it's just a function call from an effect or handler.

import { useEffect } from 'react';
import { soft } from 'soft-feedback';

function FeedbackTriggers() {
  useEffect(() => {
    const w = soft.csat({
      trigger: { type: 'event', name: 'ticket_resolved' },
      onSubmit: (p) => fetch('/api/feedback', { method: 'POST', body: JSON.stringify(p) }),
    });
    return () => w.destroy(); // clean up on unmount
  }, []);
  return null;
}

// elsewhere, when a ticket closes:
soft.track('ticket_resolved');

Embed one inline in your content (e.g. a "Was this helpful?" under an article):

function Helpful() {
  useEffect(() => {
    const w = soft.helpful({ render: { pattern: 'inline', selector: '#helpful' } });
    return () => w.destroy();
  }, []);
  return <div id="helpful" />;
}

Vue, Svelte, Angular, or vanilla JS: identical — call soft.* from onMounted / onMount / an event handler. Nothing is React-specific.


Widgets

| Call | Metric | Default look | |---|---|---| | soft.csat() | CSAT | morphing emoji dial (drag) | | soft.nps() | NPS | 0–10 connected scale | | soft.ces() | CES | 1–7 effort scale | | soft.reaction() | CSAT | one-tap emoji row 😡🙂😍 | | soft.helpful() | CSAT | inline 👍 / 👎 | | soft.pmf() | PMF | Sean-Ellis choice | | soft.churn() | — | exit-reason picker | | soft.tab() | — | always-on feedback tab |

Every widget is a self-contained flow: rating → optional follow-up → thank-you. Rating displays are interchangeable via display: 'emoji-dial' | 'number' | 'stars' | 'emoji' | 'thumbs'.

soft.csat({ display: 'stars', scale: { min: 1, max: 5 } }); // 5-star CSAT

When it shows (the politeness engine)

This is the part most libraries leave to you. You declare intent; the engine enforces it.

soft.nps({
  trigger:   { type: 'event', name: 'feature_used', delayMs: 1500 },
  conditions:[{ type: 'url', op: 'contains', value: '/app' },
              { type: 'device', value: 'desktop' }],
  frequency: { oncePerUser: true, cooldownDaysAfterResponse: 90, globalWaitDays: 14 },
});
  • Triggers: manual, event, elementVisible, timeOnPage, scrollDepth, exitIntent, idle, routeChange.
  • Conditions: url, device, selector, property, rollout (sampling %), predicate.
  • Frequency: once-per-user, max shows, cooldowns, and a global "don't show ANY survey within N days" cap.

Nothing fires until you soft.track(...) the matching event (or call .show()).


Storing data (you own it)

Pick whichever fits your stack. All of them receive the same ResponsePayload.

// 1. Per-survey callback — POST to your endpoint.
soft.csat({ onSubmit: (p) => fetch('/api/feedback', { method:'POST', body: JSON.stringify(p) }) });

// 2. Global sinks — fan every response out to one or more destinations.
import { soft, webhook, consoleSink } from 'soft-feedback';
soft.init({ sinks: [ webhook('https://api.example.com/feedback'), consoleSink() ] });

// 3. Every lifecycle event (shown / sent / dismissed / abandoned), for your analytics.
soft.on((e) => myAnalytics.track('survey_' + e.type, e));

The payload is self-describing and analytics-stable (responses keyed by stable question ids):

{
  "surveyId": "sf:nps",
  "surveyName": "NPS",
  "metric": "nps",
  "responses": { "sf:nps:rating": 9, "sf:nps:why": "fast and reliable" },
  "questions": [{ "id": "sf:nps:rating", "prompt": "How likely are you to recommend us?" }],
  "score": { "metric": "nps", "value": 9, "bucket": "promoter" },
  "startedAt": "2026-06-01T10:00:00.000Z",
  "submittedAt": "2026-06-01T10:00:12.000Z",
  "context": { "url": "https://acme.app/checkout", "device": "desktop", "locale": "en-US" }
}

Store this object as-is in your DB / warehouse and the dashboard below will read it directly. Anonymous and cookieless by default; no PII unless you add it via soft.setProperties(...).


Theming

White-label out of the box. Override any --sf-* token; a single override re-skins the whole widget.

soft.csat({
  appearance: {
    theme: 'auto',                 // 'light' | 'dark' | 'auto'
    tokens: { 'color-accent': '#e11d48', 'radius': '20px' },
    maxWidth: 420,
  },
});

Built-in presets (spread into appearance.tokens): minimal, soft, glass, glass-dark, high-contrast.

import { THEME_PRESETS } from 'soft-feedback';
soft.csat({ appearance: { theme: 'dark', tokens: THEME_PRESETS['glass-dark'] } });

Motion presets via render.motion: subtle (default), smooth, bouncy, snappy. Patterns via render.pattern: popover, inline, modal, banner, tab, headless. Positions: bottom-right, bottom-left, bottom-center, top, center.


Accessibility

WCAG 2.2 AA. APG patterns (radiogroup / slider / checkbox) with full keyboard support and roving tabindex, visible focus on every control, meaning never carried by color alone, prefers-reduced-motion and forced-colors honored, ≥44px touch targets on primary actions. The headless renderer (pattern: 'headless') lets you drive the flow and render your own UI.


Dashboard

A static, local-first results dashboard lives in apps/dashboard/index.html. It visualizes a ResponsePayload[] — the exact shape you store — and computes NPS, CSAT, CES, and PMF correctly client-side, with trends, distributions, verbatims, and segments. Nothing leaves the browser. Self-host it anywhere (Vercel, Netlify, S3, GitHub Pages, your own server).

It opens with demo data. Feed it your real responses three ways:

1. Connect to a backend (URL) — works with anything

Click Connect, paste an endpoint that returns your payloads as JSON, and (optionally) an API key. It's saved locally and re-fetched on load. The token is sent as both an Authorization: Bearer … and an apikey header, and the dashboard unwraps common envelopes ([...], { data }, { results }, { rows }, { value }), so most backends "just work":

| Backend | Endpoint | Token field | |---|---|---| | Supabase | https://<project>.supabase.co/rest/v1/feedback?select=* | your anon key | | Firebase RTDB | https://<db>.firebaseio.com/feedback.json?auth=<token> | (in the URL) | | Postgres / your API | https://api.you.com/feedback returning ResponsePayload[] | a Bearer token | | Static file / S3 | https://cdn.you.com/feedback.json | — | | Google Sheets | a published-to-web JSON endpoint mapping rows to payloads | — |

The dashboard only reads. Expose a read-only endpoint that returns the stored payloads (a Supabase view, a serverless function, a presigned URL). For exotic schemas, transform your rows into the payload shape inside that endpoint.

2. Import a file

Import a .json file (or drag-drop it) containing a ResponsePayload[]. Handy for a one-off look at an export from your warehouse.

3. The playground loop (zero setup)

The playground writes submitted responses to localStorage; the dashboard reads the same key. Answer a few widgets, open the dashboard, and your real interactions appear. Great for demos and local development.

You can also Export CSV from the dashboard for spreadsheets.


Playground

apps/playground/index.html is an interactive builder: pick a widget, configure everything (pattern, position, motion, theme, presets, colors, follow-up), hit Fire, and copy the generated soft.* code. Self-contained, no build.

Run both locally:

python3 -m http.server 8000
# playground → http://localhost:8000/apps/playground/index.html
# dashboard  → http://localhost:8000/apps/dashboard/index.html

Packages

| Package | What | |---|---| | soft-feedback | Everything: engine + renderers + widgets + scoring + sinks (zero deps) |

Framework adapters (@soft-feedback/react, vue, svelte) and optional data-sink integrations are planned; today the core is framework-agnostic and needs no adapter.


License

MIT.