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

@quentinroy/word-cloud

v0.13.1

Published

Interactive word cloud custom element powered by Matter.js

Readme

word-cloud

Test npm version

Interactive word cloud custom element powered by Matter.js. Check out the demo!

Library

This package exports the HTMLWordCloudElement class, the public event classes, and the WordHandle / WordData / WordCloudWordAction types. It does not auto-register a custom element tag for you.

Installation

# Using npm:
npm install @quentinroy/word-cloud

# Using pnpm:
pnpm install @quentinroy/word-cloud

# Using yarn:
yarn add @quentinroy/word-cloud

# Using deno:
deno add npm:@quentinroy/word-cloud

# Using bun:
bun add @quentinroy/word-cloud

Register the element

Consumers are expected to register their own custom element tag:

import { HTMLWordCloudElement } from "@quentinroy/word-cloud"

customElements.define("x-word-cloud", HTMLWordCloudElement)

Basic usage

The component fills the size of its host element, so give it an explicit width and height.

<style>
  x-word-cloud {
    display: block;
    width: 100%;
    height: 70vh;
  }
</style>

<x-word-cloud word-action="drag" word-input></x-word-cloud>
import { HTMLWordCloudElement } from "@quentinroy/word-cloud"

customElements.define("x-word-cloud", HTMLWordCloudElement)

const wordCloud = document.querySelector("x-word-cloud")

if (!(wordCloud instanceof HTMLWordCloudElement)) {
  throw new Error("x-word-cloud not found")
}

wordCloud.add([
  { word: "TypeScript", x: 160, y: 120 },
  { word: "Web Components", x: 320, y: 180, checked: true },
  { word: "Matter.js", x: 240, y: 260, angle: 0.15 },
])

Interaction Settings

The element uses four independent attributes:

  • word-action: controls how words react to user interaction.
  • word-input: boolean, controls whether the built-in input form is shown and active.
  • physics-paused: boolean, pauses the physics runner while leaving the rendered state intact. Note that while other functions will continue to work, dragging and velocity changes won't have any effect while physics is paused.
  • show-framerate: boolean, controls whether the framerate display is shown.

Supported word-action values:

  • none: default, words are passive.
  • drag: words can be dragged.
  • check: clicking a word toggles its checked state.
  • delete: clicking a word removes it.

Set word-input to show the built-in input form:

<x-word-cloud word-action="check" word-input></x-word-cloud>

Each of these can also be read or set via the corresponding property on the element instance. Instance properties use camelCase instead of kebab-case. For example, the above configuration can be achieved with:

wordCloud.wordAction = "check"
wordCloud.wordInput = true
wordCloud.physicsPaused = false
wordCloud.showFramerate = false

Public API

add(options, defaults?)WordHandle | WordHandle[]

Adds one or more words to the cloud. Pass a single options object to get back a single WordHandle, or an iterable of options objects to get back an array of handles.

The optional second argument (defaults) provides default values that are merged into each word before creation (except word, which must always be specified per word). Any required field (except word) becomes optional in each word if provided in defaults. Individual word options always override defaults.

// Single word:
const entry = wordCloud.add({
  word: "Custom Element",
  x: 200,
  y: 150,
  angle: 0,
  checked: false,
  velocity: { x: 10, y: -15 },
  entryAnimation: "fade",
})

// Remove it later (fires word-delete):
entry.remove()

// Multiple words at once (any iterable works):
const [a, b] = wordCloud.add([
  { word: "Hello", x: 100, y: 100 },
  { word: "World", x: 200, y: 200 },
])

// Restore without animation (using defaults):
const savedWords: WordData[] = [...]
wordCloud.add(savedWords, { entryAnimation: "none" })

// Provide shared position defaults so x/y can be omitted per item:
wordCloud.add(
  [{ word: "A" }, { word: "B", y: 240 }],
  { x: 120, y: 200 }
)

Adding a word also fires word-add with the created WordHandle.

Supported options:

  • word: displayed text.
  • x: initial horizontal position in pixels.
  • y: initial vertical position in pixels.
  • angle (optional): initial rotation in radians. Defaults to 0.
  • checked (optional): initial checked state. Defaults to false.
  • velocity (optional): initial physics velocity { x, y }.
  • entryAnimation (optional): entry animation to run when the word is created. Supported values are "fade", "chip-fade", and "none". Defaults to "fade".

The defaults parameter (second argument) accepts any add option except word. When provided, individual word options override the defaults.

Typing note: Any required field (except word) becomes optional in each word if present in defaults. For example, if you provide defaults.x, then x is optional in each word object.

clear()

Removes all words from the cloud. This fires a word-delete event for each word removed.

wordCloud.clear()

By default, words are removed immediately without exit animations, but you can provide options to change this behavior:

wordCloud.clear({ exitAnimation: "fade" })

Supported options:

  • exitAnimation (optional): exit animation to run when the words are removed. Supported values are "fade", and "none". Defaults to "none".

getWords()Iterable<WordHandle>

Returns live WordHandle handles for all words currently in the cloud. Each property read reflects the real-time state (position, angle, checked). Useful for persistence:

const snapshot = Array.from(wordCloud.getWords())

WordHandle

A WordHandle is a live handle to a word in the cloud, returned by add and getWords. Its properties are always up to date — they read directly from the underlying physics body and DOM element.

| Property / method | Description | | ----------------- | -------------------------------------------------------- | | handle.word | The displayed text, readable and writable. | | handle.x | Current horizontal center position in pixels. | | handle.y | Current vertical center position in pixels. | | handle.angle | Current rotation in radians. | | handle.checked | Checked state — readable and writable. | | handle.remove() | Removes the word from the cloud and fires word-delete. |

const handle = wordCloud.add({ word: "Hello", x: 100, y: 100 })

// Read live state:
console.log(handle.x, handle.y, handle.checked)

// Rename the word (fires word-change):
handle.word = "Hello again"

// Toggle checked programmatically (fires word-check):
handle.checked = !handle.checked

// Remove it:
handle.remove()

remove()void

Removes the word from the cloud. This fires a word-delete event.

Contrarily to wordCloud.clear(), the word is removed with a fade animation by default. You can provide options to change this behavior:

handle.remove({ exitAnimation: "none" })

Supported options:

  • exitAnimation (optional): exit animation to run when the words are removed. Supported values are "fade", and "none". Defaults to "fade".

WordData

Plain serializable object describing a word. Accepted by add. WordHandle is structurally compatible with WordData, so handles obtained from getWords() can be passed directly to add() (as an iterable).

interface WordData {
  word: string
  x: number
  y: number
  angle?: number
  checked?: boolean
}

Persisting state

// Save
const saved = Array.from(wordCloud.getWords(), ({ word, x, y, angle, checked }) => {
  return { word, x, y, angle, checked }
})
localStorage.setItem("words", JSON.stringify(saved))

// Restore (without animation)
const saved = JSON.parse(localStorage.getItem("words") ?? "[]")
wordCloud.clear()
wordCloud.add(saved as WordData[], { entryAnimation: "none" })

Events

HTMLWordCloudElement dispatches the following bubbling events:

  • word-add — fired when a word is added to the cloud, including through add() or the built-in input form.
  • word-change — fired when a word's text changes, including programmatic assignment to handle.word.
  • word-check — fired when a word's checked state changes (user interaction while wordAction is check, or programmatic assignment to handle.checked).
  • word-delete — fired just before a word is removed including through user interaction or programmatic removal (clear() or handle.remove()).
  • word-action-change — fired when the element wordAction changes. Includes wordAction and oldWordAction.
  • word-input-toggle — fired when the element wordInput setting changes. Includes wordInput and oldWordInput.
  • physics-pause — fired when the element physicsPaused setting changes. Includes physicsPaused and oldPhysicsPaused.

The word-specific events carry a handle property: a live WordHandle for the affected word. The setting-change events instead carry their old and new values.

Listen using the string literal or the static .type property of the event classes:

wordCloud.addEventListener("word-add", (event) => {
  console.log(`added word: "${event.handle.word}" at ${event.handle.x}, ${event.handle.y}`)
})

wordCloud.addEventListener("word-change", (event) => {
  console.log(`renamed word: "${event.oldValue}" -> "${event.value}"`)
})

wordCloud.addEventListener("word-check", (event) => {
  console.log(`"${event.handle.word}" checked: ${event.checked}`)
})

wordCloud.addEventListener("word-delete", (event) => {
  console.log(`deleted word: "${event.handle.word}"`)
})

wordCloud.addEventListener("word-action-change", (event) => {
  console.log(`word action: ${event.oldWordAction} -> ${event.wordAction}`)
})

wordCloud.addEventListener("word-input-toggle", (event) => {
  console.log(`word-input: ${event.oldWordInput} -> ${event.wordInput}`)
})

wordCloud.addEventListener("physics-pause", (event) => {
  console.log(`physics-paused: ${event.oldPhysicsPaused} -> ${event.physicsPaused}`)
})

Styling

The component exposes CSS custom properties on the host. Example:

x-word-cloud {
  --font-family: "Georgia", serif;
  --font-size: 1.25rem;
  --input-padding-y: 0.4rem;
  --input-padding-x: 1rem;
  --word-padding-y: 0.35rem;
  --word-padding-x: 0.9rem;
  --line-width: 3px;
  --word-text-color: #1f2937;
  --word-background-color: #f3f4f6;
  --word-border-color: #d1d5db;
  --word-checked-text-color: #6b7280;
  --word-checked-background-color: #e5e7eb;
  --word-delete-hover-text-color: #991b1b;
  --word-delete-hover-background-color: #fee2e2;
  --word-dragged-background-color: #dbeafe;
  --word-dragged-border-color: #bfdbfe;
  --word-dragged-text-color: #1d4ed8;
  --word-dragged-shadow-blur: 8px;
  --word-dragged-shadow-color: rgba(0, 0, 0, 0.15);
  --word-dragged-scale-factor: 1.05;
  --word-dragged-scaling-duration: 80ms;
  --input-background-color: #ffffff;
  --input-text-color: #111827;
  --input-border-color: #9ca3af;
  --input-hover-text-color: #111827;
  --input-hover-border-color: #6b7280;
  --input-hover-background-color: #f9fafb;
  --input-hover-shadow-color: transparent;
  --input-focus-text-color: #0f172a;
  --input-focus-border-color: #2563eb;
  --input-focus-background-color: #eff6ff;
  --input-focus-shadow-color: #93c5fd;
  --word-focus-outline-color: #2563eb;
  --fast-animation: 50ms;
  --slow-animation: 150ms;
  --extra-slow-animation: 1s;
  --word-chip-fade-duration: 1s;
  --word-fade-in-duration: 0.5s;
  --word-fade-out-duration: 0.5s;
  --word-state-transition-duration: 150ms;
  --input-state-transition-duration: 150ms;
  width: 100%;
  height: 70vh;
}

Supported variables:

| Variable | Default | Used for | | -------------------------------------- | --------------------------------- | ---------------------------------------------------------------- | | --space-s | 0.5rem | Shared compact spacing token used by the default padding vars. | | --space-m | 1rem | Shared roomy spacing token used by the default padding vars. | | --input-padding-y | var(--space-s) | Input vertical padding. | | --input-padding-x | var(--space-m) | Input horizontal padding. | | --word-padding-y | var(--space-s) | Word vertical padding. | | --word-padding-x | var(--space-m) | Word horizontal padding. | | --fast-animation | 50ms | Shared fast timing token used by default animation durations. | | --slow-animation | 150ms | Shared medium timing token used by default animation durations. | | --extra-slow-animation | 1s | Shared long timing token used by the chip fade animation. | | --line-width | 2px | Border width and strike line thickness. | | --font-size | 1.5rem | Input and word font size. | | --font-family | Arial | Input and word font family. | | --input-text-color | black | Input text color. | | --input-background-color | hwb(0 93% 7%) | Input background while the built-in input is enabled. | | --input-border-color | hwb(0 27% 73%) | Input border color. | | --input-hover-text-color | var(--input-text-color) | Input text color while hovered. | | --input-hover-border-color | hwb(0 20% 66%) | Input border color while hovered. | | --input-hover-background-color | hwb(0 96% 4%) | Input background while hovered. | | --input-hover-shadow-color | transparent | Input hover drop-shadow color. | | --input-focus-text-color | hwb(212 2% 88%) | Input text color while focused. | | --input-focus-border-color | hwb(212 16% 22%) | Input border and default word focus outline color while focused. | | --input-focus-shadow-color | hwb(212 76% 0%) | Input focus drop-shadow color. | | --input-focus-background-color | hwb(212 95% 0%) | Input background while focused. | | --word-focus-outline-color | var(--input-focus-border-color) | Keyboard focus outline for words. | | --word-text-color | hwb(276 2% 80%) | Default word text color. | | --word-background-color | hwb(276 96% 0%) | Default word background. | | --word-border-color | var(--word-background-color) | Default word border color. | | --word-delete-hover-text-color | hwb(357 45% 11%) | Word text color on delete hover. | | --word-delete-hover-background-color | hwb(351 99% 0%) | Word background and border on delete hover. | | --word-checked-text-color | hwb(276 54% 31%) | Checked word text color. | | --word-checked-background-color | hwb(276 98% 0%) | Checked word background and border color. | | --word-checked-hover-text-color | hwb(276 21% 21%) | Word text color while hovered in check mode. | | --word-dragged-background-color | hwb(212 90% 0%) | Dragged word background. | | --word-dragged-border-color | hwb(212 76% 0%) | Dragged word border. | | --word-dragged-text-color | hwb(211 5% 70%) | Dragged word text color. | | --word-dragged-shadow-blur | 5px | Blur radius of the drop-shadow on a dragged word. | | --word-dragged-shadow-color | hwb(0 0% 100% / 0.05) | Drop-shadow color on a dragged word. | | --word-dragged-scale-factor | 1.1 | Scale applied to a word while it is being dragged. | | --word-dragged-scaling-duration | var(--fast-animation) | Transition duration for the drag scale-up / scale-down effect. | | --word-chip-fade-duration | var(--extra-slow-animation) | Chip color fade duration for words created with "chip-fade". | | --word-fade-in-duration | var(--slow-animation) | Opacity fade-in duration for newly created words. | | --word-fade-out-duration | var(--slow-animation) | Opacity fade-out duration for deleted words. | | --word-state-transition-duration | var(--slow-animation) | Checked and hover state transition duration for words. | | --input-state-transition-duration | var(--slow-animation) | Hover and focus transition duration for the built-in input. |

Notes

  • The library exports constructors and types, not a pre-registered tag name.
  • The host element needs a real size; if its height is 0, nothing useful will render.
  • Words are positioned using the host element’s content box, so restoring saved coordinates works best when the element has a stable size.

Local demo

pnpm install
pnpm dev

The demo lives in demo/ and is served by index.html during local development.

Build

pnpm build

This produces the publishable library in dist/.

Development

Running tests

pnpm test

This runs all unit and browser tests using Vitest.

Updating visual regression screenshots

Visual regression tests in Vitest compare rendered component screenshots against baseline images. If you make intentional visual changes that require updating these screenshots, follow these steps:

  1. Push your changes to a branch and open a pull request.
  2. Add the update screenshots label to that pull request.

This triggers the Update Visual Regression Screenshots workflow automatically. The workflow:

  • checks out the PR branch,
  • runs the browser visual regression tests with screenshot updates,
  • commits and pushes updated screenshots back to the same PR branch (only if files changed),
  • posts a PR comment with a summary,
  • removes the update screenshots label when done.

If no screenshots need updating, the workflow still posts a summary comment saying everything is already up to date.