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

frameset

v0.17.1

Published

Frameset is a tool for designing and specifying UI using templates. Each frame is one self-contained file — YAML frontmatter over a Liquid template — that takes parameters, renders as its own document, and can be embedded inside other frames. Nest them an

Readme

Frameset

Frameset is a tool for designing and specifying UI using templates. Each frame is one self-contained file — YAML frontmatter over a Liquid template — that takes parameters, renders as its own document, and can be embedded inside other frames. Nest them and you get a digital twin of an application: the same structure and the same states, with none of its data, routing, or machinery.

Storybook targets isolated component stories. Figma targets visual design. Frameset targets the space between: a design surface made of real markup. One file can be a sketch of a screen that has no routes or data yet, a prototype you click through, a gallery of every state it can be in, and the specification an implementation is held to — each state adjusted through controls derived from its parameters, and linked to.

The templates are deliberately dumb — interpolation, conditionals, loops, and composition, with no arbitrary JavaScript — so a frame stays something to read and reason about rather than another program to debug.

A frame at a glance

---
title: Status badge
params:
  tone: { enum: [neutral, success], default: neutral }
  label: { type: string, default: Ready }
imports:
  - ./status.css
---
<span class="status status--{{ tone }}">{{ label }}</span>

The frontmatter is optional. params supplies the render interface and control metadata. imports is an ordered dependency list: strings are side-effect imports, while a single-key mapping binds a default export into the Liquid scope. data binds inline values into the scope the same way. style holds the frame's own CSS and script its own behavior, each delivered to the document once however many instances render. adoptedStyles is the frame's own CSS for the declarative shadow roots the body writes, per host tag, adopted into each such root. The body is Liquid and may use interpolation, conditionals, loops, and frame composition — but no style, script, or document-defining elements: the body is a fragment, and styles and behavior belong in style, script and imports.

Frontmatter is a strict JSON-compatible subset of YAML, and those eight keys — title, description, params, imports, data, style, script, adoptedStyles — are the only ones allowed. Duplicate keys are rejected, and param, import, and data names must be identifiers: makeRoom, not make-room or 10.

Quick start

Install Frameset in the project and add frameset.json:

npm install --save-dev frameset
{
  "root": ".",
  "imports": { "design/**/*.frame": ["design/foundation.ts"] },
  "sidebar": [
    { "type": "frame", "path": "design/dashboard.frame", "title": "Dashboard" },
    {
      "type": "folder",
      "title": "Account",
      "children": [
        { "type": "frame", "path": "design/account/sign-in.frame" }
      ]
    }
  ]
}

Run the command from the directory containing the manifest — that exact directory, since parent directories are not searched:

npx frameset

Frameset starts at http://127.0.0.1:4400/ and opens the viewer. Use --port, --host, and --allowed-hosts to change the server settings. Sidebar entries are explicit and retain manifest order. Every .frame under root is still addressable and composable, whether or not the sidebar lists it. imports maps a glob to modules every matching frame's document loads before the frame itself — the place for a foundation stylesheet or the element definitions frames rely on to draw.

Params and controls

Controls derive from the frame's params; frame code does not register them at runtime. An enum renders a dropdown, type: number a number input, type: boolean a checkbox, and type: string a text input. Object and array params are structured fixtures and do not render controls.

The viewer URL is the selected frame id plus its arguments:

/design/status?tone=success&label=Deployed

Defaults seed the render and the controls. Changing a control rewrites the query and re-renders the document in place, so the URL is also the shareable state. The viewer renders controls for the root frame; <frameset-frame> renders them for an embedded frame.

A frame's id is its path under root without the suffix, so design/status.frame has the id design/status. That id answers at three addresses, which is worth knowing when something looks wrong:

/#design/status         the viewer, with that frame selected
/design/status          the frame document by itself
/design/status.frame    the compiled module

Fetching the last one is what proves the frame compiles.

Composition

Liquid render inlines another frame's output into the current document:

{% render './status', tone: 'success', label: 'Deployed' %}

<frameset-frame> embeds another frame as its own document and viewport:

<frameset-frame
  src="./status?tone=success"
  label="Status"
  width="320"
  height="80"
  background="#fff"
  border
></frameset-frame>

render invokes the child in isolation: only the arguments you name cross the boundary. Surrounding scope does not — including a loop's forloop.index, which has to be passed explicitly as an argument.

An embed's label and control strip render outside its declared box, so they never change its size, and content that overflows the box is clipped (overflow="visible" to let it escape).

Use render when the child belongs to the same page and style context. Use <frameset-frame> for document isolation, a fixed viewport, and embedded controls. Its src may be relative to the current frame or absolute from the project root.

Importing frames

The viewer is not the only way to use a frame. Add the package's Vite plugin to a project's own config, and .frame files become importable modules anywhere that build reaches — application code, scripts, or tests:

import { defineConfig } from "vite";
import frameset from "frameset/vite";

export default defineConfig({
  plugins: [frameset()],
});

The Frameset server installs this plugin itself, so the config entry is only for your own build.

An import returns a frozen module — render plus the frontmatter:

import status from "./design/status.frame";

status.render({ tone: "success" }); // → markup string, synchronously
status.params;                      // → the declared schema fragments
status.imports;                     // → ["./status.css"]

Importing also delivers the frame's dependencies to the document, so a specimen's styles are present exactly as they are in the viewer, once however many times it renders.

This is what makes a frame usable as a specification: the frame renders the intended result, and a test holds the implementation against it. The test runner needs the plugin above so .frame imports compile, and a real browser so the frame's styles apply:

import spec from "../design/status.frame"; // delivers the spec's CSS here
import "../src/status-badge"; // defines <status-badge>

// the frame declares which tones exist, so the suite covers whatever it declares
test.each(spec.params.tone.enum as string[])("tone %s matches the spec", (tone) => {
  document.body.innerHTML = spec.render({ tone, label: "Ready" });
  const reference = document.body.firstElementChild!;

  const actual = document.createElement("status-badge");
  actual.setAttribute("tone", tone);
  document.body.append(actual);

  expect(getComputedStyle(actual).backgroundColor).toBe(
    getComputedStyle(reference).backgroundColor,
  );
});

How Frameset itself is specified

The specs/ folder in the repository holds the full specification of the format, server, viewer, and integration contracts.