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

@sp-treeview/element

v4.1.0

Published

Native Web Components <sp-tree> and <sp-tree-select>: WAI-ARIA tree, checkbox/radio selection with cascade, lazy loading, search, dropdown field with chips. Works in React, Vue, Svelte, Angular, plain HTML.

Readme

@sp-treeview/element

Native Web Components for tree selection, built on Lit over the framework-agnostic @sp-treeview/core state engine. Two custom elements:

  • <sp-tree> — an inline, WAI-ARIA tree with checkbox/radio selection, lazy loading, and a built-in search box.
  • <sp-tree-select> — a form field (dropdown or overlay) whose value is a tree selection, with removable chips.

Because they are standard custom elements, they work in React, Vue, Svelte, Angular, plain HTML/JS — no per-framework wrapper.

Website, live demos & guides: https://srjn45.github.io/sp-treeview-v2/

npm install @sp-treeview/element
# peer/runtime deps: lit, @floating-ui/dom (installed automatically)
// Importing the package registers <sp-tree> and <sp-tree-select> as a side effect.
import '@sp-treeview/element';

Version 4.1.0. Node ≥ 20. Ships ESM only.


<sp-tree>

An inline tree. Renders store.rows() as a flat, indented list — collapsed and filtered-out rows are not in the DOM.

Properties & attributes

Object/function inputs (data, loadChildren, renderNode, store) are DOM properties — set them via JS/framework binding, not string attributes. Primitive inputs also have a reflected attribute.

| Property | Attribute | Type | Default | Description | |---|---|---|---|---| | data | — | TreeNodeData[] | [] | The tree data (see core data model). | | selection | selection | 'none' \| 'single' \| 'multi' | 'none' | Selection mode. | | cascade | cascade | boolean | true | Parent↔child propagation (multi). | | loadOnce | load-once | boolean | true | Cache lazily-loaded children. | | loadChildren | — | (node) => Promise<TreeNodeData[]> | — | Lazy loader for hasChildren nodes. | | searchable | searchable | boolean | false | Render the built-in search box. | | showAllNode | show-all-node | boolean | false | Render a top "All" row wired to setAllChecked. | | renderNode | — | (node, ctx) => TemplateResult | — | Custom row content (Lit template). | | store | — | TreeStore | — | Use an existing store instead of building one from data/selection/… | | editable | editable | boolean | false | Show per-node create/rename/delete actions + an "Add item" root button (see Editable mode). | | name | name | string | '' | Form field name — when set, the tree JSON is submitted under this name. | | idFactory | — | () => string | crypto.randomUUID | Id generator for nodes created in editable mode. |

cascade and loadOnce default to true; because they are boolean attributes, set them to false through the property (el.loadOnce = false / .loadOnce=${false}), not by omitting an attribute.

Events

All events are composed and bubbling (CustomEvent).

| Event | detail | Fires | |---|---|---| | sp-change | multi: { checked: TreeNodeData[], allSelected: boolean }; single: { selected: TreeNodeData \| null } | On any selection change. | | sp-expand | { node: TreeNodeData } | A row expands. | | sp-collapse | { node: TreeNodeData } | A row collapses. | | sp-load-error | { node: TreeNodeData, error: Error } | A lazy load rejects. | | sp-node-create | { node: TreeNodeData, parentId: string \| null } | A node is created (editable mode). | | sp-node-update | { node: TreeNodeData, previousLabel: string } | A node is renamed (editable mode). | | sp-node-delete | { node: TreeNodeData } | A node (and its subtree) is deleted (editable mode). | | sp-tree-change | { data: TreeNodeData[] } | Any structural edit — carries the full current tree. |

The multi-mode sp-change reports the concrete checked nodes plus allSelected — there is no sentinel "ALL" node in the payload.

::part() names

Style internals from outside the shadow DOM via parts (full list and the CSS custom-property tokens are in the theming guide):

tree, row, all-row, toggle, checkbox, radio, label, match, spinner, error, retry, search, search-input, search-clear, empty.

Editable mode adds: node-actions, action-edit, action-add, action-delete, edit-input, edit-submit, edit-cancel, confirm, confirm-text, confirm-yes, confirm-no, add-root-bar, add-root.

sp-tree::part(row) { padding-block: 2px; }
sp-tree::part(match) { background: gold; }

Editable mode

Set editable to turn the tree into a hierarchy editor. Each row reveals three actions on hover/focus, and an Add item button below the tree creates roots:

  • ✎ edit — replaces the label with an inline text field; Enter or the ✓ button commits (updateNode), Escape or ✕ cancels.
  • + add child — expands the node and opens an inline field for a new child (addNode); the new node's id comes from idFactory (default crypto.randomUUID).
  • 🗑 delete — swaps the row actions for an inline "Delete "X" + N items?" confirmation (N = descendant count); only ✓ actually removes the subtree.

Because it's form-associated, an editable tree doubles as a form field that submits the whole hierarchy as JSON:

const tree = document.createElement('sp-tree') as SpTree;
tree.editable = true;
tree.name = 'taxonomy';
tree.data = data;

tree.addEventListener('sp-tree-change', (e) => {
  console.log((e as CustomEvent).detail.data); // current TreeNodeData[]
});

tree.value;              // '[{"id":"…","label":"…","children":[…]}]'
new FormData(form).get('taxonomy'); // same JSON string on submit

<sp-tree> is formAssociated: its value (getter/setter) is the tree JSON, setFormValue runs on every change, and formResetCallback restores the data the tree was first built from. Setting value to a JSON string replaces the tree.


<sp-tree-select>

A form field whose value is a tree selection. The trigger shows selected leaves as removable chips (wrapping to 2 rows, then a +N overflow chip); activating it opens a panel that embeds an <sp-tree>. The panel — and every node in it — is removed from the DOM while closed, so a closed field leaves no hidden tab/click targets.

Additional properties & attributes

Everything from <sp-tree> is forwarded, plus:

| Property | Attribute | Type | Default | Description | |---|---|---|---|---| | variant | variant | 'dropdown' \| 'overlay' | 'dropdown' | Floating panel vs. full-viewport modal sheet. | | placeholder | placeholder | string | 'Select…' | Shown when nothing is selected. | | selection | selection | 'none' \| 'single' \| 'multi' | 'multi' | Note: defaults to multi here (<sp-tree> defaults to none). |

editable and idFactory are also forwarded — set editable to allow creating/renaming/deleting nodes from inside the dropdown panel.

Methods

open(), close(), toggle() — imperative panel control (the field, Escape, click-outside, and the panel's Done button also drive it). On open, focus moves into the panel; on close it returns to the trigger.

getData() — the current tree structure (reflecting any in-panel edits) as TreeNodeData[].

Events

Emits the same sp-change as <sp-tree> (re-dispatched from the embedded tree); sp-expand / sp-collapse / sp-load-error, and — in editable mode — sp-node-create / sp-node-update / sp-node-delete / sp-tree-change from the inner tree also bubble out.

The select's form value stays a JSON array of checked node ids (the field's purpose is selection). To persist an edited hierarchy, read getData() or listen for sp-tree-change.

Form participation

static formAssociated = true: the field sets its form value (via ElementInternals) to a JSON array of checked node ids, so it works in plain <form>s and framework form libraries.

<form>
  <sp-tree-select name="regions"></sp-tree-select>
</form>
const fd = new FormData(form);
fd.get('regions'); // e.g. '["mumbai","pune"]'

Additional ::part() names

Plus the forwarded <sp-tree> parts above:

field, placeholder, chip, chip-overflow, chip-remove, backdrop, panel, done.


Usage

The data + lazy loader used below:

import type { TreeNodeData } from '@sp-treeview/core';

const data: TreeNodeData[] = [
  { id: 'india', label: 'India', children: [
    { id: 'mh', label: 'Maharashtra', children: [{ id: 'mumbai', label: 'Mumbai' }] },
  ] },
  { id: 'usa', label: 'USA', hasChildren: true },
];

const loadChildren = (node: TreeNodeData): Promise<TreeNodeData[]> =>
  new Promise((resolve) =>
    setTimeout(() => resolve(node.id === 'usa' ? [{ id: 'ca', label: 'California' }] : []), 300),
  );

interface MultiChangeDetail { checked: TreeNodeData[]; allSelected: boolean }

Plain JS / TypeScript

import { SpTree } from '@sp-treeview/element';

const tree = document.createElement('sp-tree') as SpTree;
// Object/function inputs are DOM *properties*, never attributes.
tree.data = data;
tree.loadChildren = loadChildren;
// Primitive inputs may be attributes or properties.
tree.selection = 'multi';
tree.cascade = true;
tree.searchable = true;

tree.addEventListener('sp-change', (e) => {
  const detail = (e as CustomEvent<MultiChangeDetail>).detail;
  console.log('checked:', detail.checked.map((n) => n.label), 'all:', detail.allSelected);
});

document.body.append(tree);

The dropdown/overlay field is wired the same way:

import { SpTreeSelect } from '@sp-treeview/element';

const select = document.createElement('sp-tree-select') as SpTreeSelect;
select.data = data;
select.loadChildren = loadChildren;
select.variant = 'dropdown';        // or 'overlay'
select.selection = 'multi';
select.searchable = true;
select.placeholder = 'Pick regions…';

select.addEventListener('sp-change', (e) => {
  const detail = (e as CustomEvent<MultiChangeDetail>).detail;
  console.log('selected leaves:', detail.checked.map((n) => n.label));
});

document.body.append(select);

Plain HTML with an import map (no bundler):

<script type="importmap">
  { "imports": {
    "lit": "https://esm.sh/lit@3",
    "@floating-ui/dom": "https://esm.sh/@floating-ui/dom@1",
    "@sp-treeview/core": "https://esm.sh/@sp-treeview/[email protected]",
    "@sp-treeview/element": "https://esm.sh/@sp-treeview/[email protected]"
  } }
</script>
<sp-tree-select id="picker" selection="multi" searchable></sp-tree-select>
<script type="module">
  import '@sp-treeview/element';
  document.getElementById('picker').data = [/* … */];
</script>

React

<sp-tree> / <sp-tree-select> are custom elements, so React uses them directly — no wrapper package. Set object/function inputs through a ref; listen for sp-change with addEventListener.

import { useEffect, useRef, useState } from 'react';
import type { TreeNodeData } from '@sp-treeview/core';
import type { SpTreeSelect } from '@sp-treeview/element';
import '@sp-treeview/element';

// JSX typings for the custom elements (no wrapper package needed).
type CustomElementProps<E> = React.HTMLAttributes<E> & { ref?: React.Ref<E>; key?: React.Key; class?: string };
type SpTreeSelectProps = CustomElementProps<SpTreeSelect> & {
  variant?: 'dropdown' | 'overlay';
  selection?: 'none' | 'single' | 'multi';
  searchable?: boolean;
  placeholder?: string;
};
declare module 'react' {
  namespace JSX {
    interface IntrinsicElements { 'sp-tree-select': SpTreeSelectProps }
  }
}

export function RegionPicker() {
  const ref = useRef<SpTreeSelect>(null);
  const [labels, setLabels] = useState<string[]>([]);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    el.data = data;                 // object/function inputs → properties
    el.loadChildren = loadChildren;
    const onChange = (e: Event) => {
      const detail = (e as CustomEvent<{ checked: TreeNodeData[] }>).detail;
      setLabels(detail.checked.map((n) => n.label));
    };
    el.addEventListener('sp-change', onChange);
    return () => el.removeEventListener('sp-change', onChange);
  }, []);

  return (
    <div>
      <sp-tree-select ref={ref} variant="dropdown" selection="multi" searchable placeholder="Pick regions…" />
      <p>Selected: {labels.length ? labels.join(', ') : '(none)'}</p>
    </div>
  );
}

Vue 3

Tell Vue that sp-* are custom elements (in your build config: compilerOptions.isCustomElement = (tag) => tag.startsWith('sp-'), or the Vite @vitejs/plugin-vue template.compilerOptions.isCustomElement option). Then bind object/function inputs imperatively via a template ref.

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import type { TreeNodeData } from '@sp-treeview/core';
import type { SpTreeSelect } from '@sp-treeview/element';
import '@sp-treeview/element';

interface MultiChangeDetail { checked: TreeNodeData[]; allSelected: boolean }

const picker = ref<SpTreeSelect | null>(null);
const labels = ref<string[]>([]);

function onChange(e: Event) {
  const detail = (e as CustomEvent<MultiChangeDetail>).detail;
  labels.value = detail.checked.map((n) => n.label);
}

onMounted(() => {
  const el = picker.value;
  if (!el) return;
  el.data = data;                 // object/function inputs → properties
  el.loadChildren = loadChildren;
  el.addEventListener('sp-change', onChange);
});

onBeforeUnmount(() => {
  picker.value?.removeEventListener('sp-change', onChange);
});
</script>

<template>
  <sp-tree-select ref="picker" variant="dropdown" selection="multi" searchable placeholder="Pick regions…" />
  <p>Selected: {{ labels.length ? labels.join(', ') : '(none)' }}</p>
</template>

Theming

Every visual value flows through --sp-tree-* CSS custom properties (light + dark defaults built in); structural restyling uses ::part(). See the theming guide for the full token table and a worked dark-theme override.

Accessibility

Implements the WAI-ARIA tree pattern: role="tree"/treeitem, aria-level, aria-setsize, aria-posinset, aria-expanded, aria-checked (incl. "mixed"), aria-disabled, aria-busy, roving tabindex, arrow-key navigation, Home/End, Enter/Space to toggle, and type-ahead.

License

MIT. Part of sp-treeview v4.