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

@surstromming/custom-web-components

v0.1.0

Published

Selected components compiled as framework-agnostic custom elements.

Readme

@surstromming/custom-web-components

Selected components compiled as framework-agnostic custom elements — usable in plain HTML, React, Angular, or anywhere.

The repo's one compiled package: every other package ships raw .vue source for consumers who share our Vite/Sass pipeline. A custom-element consumer doesn't, so this one is built to dist/. Vue is a peer dependency — the consumer provides it (the bundle imports it, doesn't ship it).

Dependency graph

graph LR
  custom_web_components["@surstromming/custom-web-components"]
  button["@surstromming/button"]
  design["@surstromming/design"]
  input["@surstromming/input"]
  custom_web_components --> button
  button --> design
  custom_web_components --> design
  custom_web_components --> input
  input --> design

Exports

| Path | What | | --------- | ---------------------------------------------------------------- | | . | Re-exports every component (currently Button). | | ./button| Just Button. | | ./util | defineElement, register, trackTheme / untrackTheme. |

The package ships components, not pre-made element classes — the consumer turns a component into an element and names the tag:

import { Button } from '@surstromming/custom-web-components'
import { defineElement } from '@surstromming/custom-web-components/util'

customElements.define('ss-button', defineElement(Button))
<ss-button variant="outline" size="lg">Click me</ss-button>

register() does several at once (still the consumer's call, with their prefix):

import { Button } from '@surstromming/custom-web-components'
import { register } from '@surstromming/custom-web-components/util'

register({ button: Button })          // <ss-button>
register({ button: Button }, 'acme')  // <acme-button>

Attributes map to props (variant, size, disabled); light-DOM children project through <slot>; DOM events fire natively — it's a real <button>.

The demo lives in the app at src/pages/webcomponents (route /webcomponents): webComponentsRegister.ts defines the tag, WebComponents.vue uses it. The app's Vite marks <ss-*> as custom elements (isCustomElement) so Vue leaves them alone.

Theming (opt-in)

Design tokens are inherited CSS custom properties, so the dark theme's values cross into the shadow root for free — dark is already token-correct. A component's dark-gated rules, though, compile to :host([data-theme='dark']), which matches only when the attribute is on the host. trackTheme(hostEl) mirrors <html data-theme> onto a host to switch those on:

import { trackTheme } from '@surstromming/custom-web-components/util'
// e.g. from a wrapper element's connectedCallback, or over document.querySelectorAll('ss-button')

It's deliberately not wired into defineElement — the theme source/selector may be computed differently per app, so this is left as a seam.

How the shadow DOM is made to work

The components use <style module>, which @vitejs/plugin-vue refuses to compile in shadow-DOM (customElement: true) mode. Two build-time moves fix it, with no component changes:

  1. Strip the module attribute (a pre Vite plugin). Shadow DOM isolates class names per element, so hashing is pointless — the block compiles as plain CSS (.root, .variant-primary) injected into the shadow root.
  2. Identity useCssModule() (a vue alias to src/vue-shim.ts) so $style.x returns 'x', matching those plain names. The alias must win over externalization — only the shim's deep vue/dist/… import is external, never bare vue, or Rollup would externalize it before the alias runs.

process.env.NODE_ENV is inlined to production (no consumer bundler defines it for a direct-browser path).

Shadow reset

The app's global reset.scss can't cross into a shadow root, so the inner controls would fall back to user-agent styling (grey buttonface, Arial, native chrome). defineElement injects a small button/anchor slice of that reset as the first style in each shadow root; the component's own styles follow and override where they set things (a variant's own background/color wins; a variant that sets neither — ghost, link — keeps the reset's transparent/inherit). Not the whole reset.scss — that emits @font-face and body rules that don't belong in a shadow root.

Build

npm run build --workspace=@surstromming/custom-web-componentsdist/ (one entry per component, a barrel, util, the shim chunk). No CSS file — each element injects its own styles into its shadow root. Rebuild after changing source; the app consumes dist/ (types resolve to src/ via the package's export conditions).

Notes

  • Leaf, presentational components are the right fit. Teleport-based ones (dialog, toast, sidebar) escape the shadow root; array-prop components (options-driven) can't take data from HTML attributes. Neither is in scope.
  • Assumes a component reads classes via useCssModule() (the repo's classes convention). One using the template $style magic var directly would need the shim extended.