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

vanjs-jsf

v0.4.0

Published

A JSON Schema Form UI Library for VanJS

Downloads

250

Readme

VanJS-JSF — a JSON Schema Form Library for VanJS

Generate dynamic UI forms effortlessly with TypeScript and JSON Schema, powered by the JSON Schema Form Headless UI framework.

Description

This library provides a robust and flexible solution for dynamically generating user interface (UI) forms using JSON Schema definitions. It is built on TypeScript for type safety and leverages the powerful JSON Schema Form Headless UI framework, which you can find documented here. It ensures a lightweight, modern, accessible, and customizable form-building experience.

Features

  • [x] Dynamic Form Generation: Create forms directly from JSON Schema, reducing repetitive coding tasks.
  • [x] Theming API: Inject CSS classes for every form element via a theme object — works with Tailwind, Bootstrap, or any CSS framework.
  • [x] Layout Map: Control per-field spatial distribution (e.g. grid column spans) without coupling CSS classes to JSON Schema.
  • [x] Customizable: Tailor form styles, layouts, and behaviours to meet specific UI/UX requirements.
  • [x] Headless UI Integration: Utilize Headless UI's components for accessible and modern interfaces.
  • [x] TypeScript-first Approach: Enjoy strong typing and enhanced developer experience with TypeScript.
  • [x] Validation Support: Easily integrate JSON Schema-based validation for seamless user input handling.
  • [ ] Extensible Architecture: Add custom widgets, field types, and behaviours as needed.

Available components

The currently supported form element types are:

  • text = "text"
  • number = "number"
  • textarea = "textarea"
  • select = "select"
  • radio = "radio"
  • date = "date" (Pikaday)
  • code = "code" (CodeMirror with JSON, JavaScript, TypeScript support)
  • fieldset = "fieldset"
  • file = "file" (drag & drop, with configurable readAs mode and size validation)

Getting Started

  1. Install the library:
npm install vanjs-jsf
  1. Import and define your JSON Schema with x-jsf-presentation hints:
import van from "vanjs-core";
import { jsform } from "vanjs-jsf";

const { div, h1, p, button } = van.tags;

const schema = {
  type: "object",
  properties: {
    userName: {
      type: "string",
      title: "Name",
      "x-jsf-presentation": { inputType: "text" },
    },
    age: {
      type: "number",
      title: "Age",
      "x-jsf-presentation": { inputType: "number" },
    },
  },
  required: ["userName"],
  "x-jsf-order": ["userName", "age"],
};
  1. Create a config with initial values and render the form:
const initialValues = { userName: "Simon" };
const config = {
  strictInputType: false,
  initialValues: initialValues,
  formValues: initialValues,
};

const handleOnSubmit = (e: Event) => {
  e.preventDefault();
  const values = config.formValues;
  alert(`Submitted: ${JSON.stringify(values, null, 2)}`);
};

van.add(
  document.body,
  div(
    h1("json-schema-form + VanJS"),
    p("Dynamic form generated from JSON Schema."),
    jsform(
      {
        name: "my-jsf-form",
        schema: schema,
        config: config,
        onsubmit: handleOnSubmit,
      },
      button({ type: "submit" }, "Submit"),
    ),
  ),
);

File Upload Field

The file field type renders a drag & drop zone with file info display. Define it in your schema:

const schema = {
  type: "object",
  properties: {
    document: {
      type: "string",
      title: "Upload Document",
      description: "PDF or image, max 5 MB",
      "x-jsf-presentation": {
        inputType: "file",
        accept: ".pdf,.png,.jpg",
        maxSizeMB: 5,
        readAs: "auto",
      },
    },
  },
  "x-jsf-order": ["document"],
};

readAs modes

| Value | Behaviour | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | "auto" (default) | Text extensions (json, csv, txt, xml, yaml, md, html, css, js, ts, sql, etc.) are read as text. All other files are read as binary and returned as base64. | | "text" | Always reads the file as text (readAsText). | | "dataURL" | Returns a data URL string (readAsDataURL). | | "arrayBuffer" | Reads as ArrayBuffer and returns base64 (readAsArrayBuffer). |

The file content is stored in formValues under the field name. Additional metadata is stored as <fieldName>__fileName, <fieldName>__fileSize, and <fieldName>__fileType.

Theming

By default, vanjs-jsf applies minimal semantic CSS classes (e.g. jsf-dropzone, jsf-file-info). You can import the default styles:

import "vanjs-jsf/dist/jsf-defaults.css";

To fully customize the appearance, pass a theme object to jsform(). Theme classes apply to every element type in the form. Per-field classes defined in x-jsf-presentation (e.g. class, titleClass, containerClass) take priority over theme classes.

import { jsform, JsfTheme } from "vanjs-jsf";

const myTheme: JsfTheme = {
  // Structure
  container: "mb-4",
  label: "block text-sm font-medium text-gray-700 mb-1",
  description: "text-gray-500 text-xs mb-1",
  error: "text-red-500 text-xs mt-1",
  requiredIndicator: "text-red-500 ml-1",

  // Inputs
  input: "w-full border border-gray-300 rounded px-3 py-2",
  textarea: "w-full border border-gray-300 rounded px-3 py-2 resize-y",
  select: "w-full border border-gray-300 rounded px-3 py-2",
  option: "bg-white",

  // Radio
  radioGroup: "flex flex-col gap-2",
  radioLabel: "flex items-center gap-2 cursor-pointer",
  radioInput: "accent-blue-500",

  // Fieldset
  fieldset: "border border-gray-200 rounded p-4",
  legend: "text-sm font-semibold text-gray-700",

  // File upload
  dropZone:
    "border-2 border-dashed border-gray-300 rounded-lg p-6 text-center cursor-pointer hover:border-blue-400 transition-colors",
  dropZoneActive:
    "border-2 border-dashed border-blue-500 rounded-lg p-6 text-center cursor-pointer bg-blue-50",
  dropZoneText: "text-gray-500 text-sm m-0",
  fileInfoBar: "mt-2 flex items-center gap-2",
  fileName: "font-semibold",
  fileSize: "text-gray-400 text-sm",
  fileClearButton:
    "text-sm border border-gray-300 rounded px-2 py-0.5 hover:bg-gray-100",
  fileReading: "mt-2 text-gray-400 text-sm",
};

const formEl = jsform({
  schema: mySchema,
  config: { initialValues: {}, formValues: {} },
  theme: myTheme,
  onsubmit: handleSubmit,
});

JsfTheme properties

| Property | Applies to | | ------------------- | ------------------------------------------------------ | | container | Wrapper <div> of each field | | label | <label> elements | | description | Description <div> below the label | | error | Error <p> below the input | | requiredIndicator | <span> with "*" next to required field labels | | input | <input> for text, number, and date fields | | textarea | <textarea> elements | | select | <select> elements | | option | <option> elements inside selects | | radioGroup | Container <div> for radio options | | radioLabel | <label> wrapping each radio option | | radioInput | <input type="radio"> elements | | fieldset | <fieldset> elements | | legend | <legend> elements (falls back to label if not set) | | dropZone | File drop zone container (normal state) | | dropZoneActive | File drop zone during dragover | | dropZoneText | Text inside the drop zone | | fileInfoBar | Container for uploaded file info | | fileName | File name <strong> | | fileSize | File size <small> | | fileClearButton | "Clear" button | | fileReading | "Reading file..." indicator |

Layout

The layout attribute lets you assign extra CSS classes to specific field containers by name, without modifying the JSON Schema. This is useful for controlling column spans in CSS grid layouts.

import { jsform, JsfTheme, JsfLayout } from "vanjs-jsf";

const theme: JsfTheme = {
  container: "mb-4",
  input: "w-full border rounded px-3 py-2",
  label: "block text-sm font-medium mb-1",
};

const layout: JsfLayout = {
  host: "col-span-2",    // full row
  dbname: "col-span-2",  // full row
  // port, user, password, schema → no entry → default 1 column
};

const formEl = jsform({
  schema: connectionSchema,
  config: { initialValues: {}, formValues: {} },
  theme,
  layout,
});

Wrap the form in a grid container to activate the column distribution:

.jsf-grid > form {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 1rem;
}
const { div } = van.tags;
van.add(document.body, div({ class: "jsf-grid" }, formEl));

This produces a layout like:

┌─────────────── host ────────────────┐
│ 192.168.1.176                       │
├────── port ──────┬──── user ────────┤
│ 5432             │ wolfops          │
├──── password ────┬──── schema ──────┤
│ ••••••••         │ public           │
├─────────────── dbname ──────────────┤
│ wolfops_dev                         │
└─────────────────────────────────────┘

Nested fields

For fields inside a fieldset, layout supports both the full path and the short name:

const layout: JsfLayout = {
  "connection.host": "col-span-2",  // by full path
  port: "col-span-1",              // by short name (also works for nested)
};

The full path (fieldset.fieldName) is checked first, then the short name.

Class resolution order

For each element, the resolved class follows this priority:

  1. Per-field class from x-jsf-presentation (e.g. class, titleClass, containerClass)
  2. Theme class from the theme object
  3. Empty string (no class applied)

Layout classes are appended after the resolved container class, so they combine with both per-field and theme classes.

Visibility

Hidden fields receive the jsf-hidden CSS class instead of inline display: none. Make sure your CSS includes:

.jsf-hidden {
  display: none;
}

This is included in jsf-defaults.css.

Development

npm install          # Install dependencies
npm run dev          # Start Vite dev server (port 3030)
npm run build        # Bundle with esbuild → dist/index.js
npm run types        # Generate type declarations → dist/*.d.ts
npm run lint         # Run ESLint
npm run lint:fix     # Run ESLint with auto-fix

Publishing

  1. Update the version in package.json
  2. Run the publish script with your npm OTP:
./publish.sh <otp>

This cleans dist/, rebuilds the bundle and type declarations, copies jsf-defaults.css to dist/, and publishes to npm.

The package is available at: https://www.npmjs.com/package/vanjs-jsf

Contributing

Contributions are welcome! Please submit a pull request if you have ideas, feedback, or improvements. Your contributions will help make this library more robust and useful for the community.

License

This project is licensed under the Apache 2 License. For details, see the LICENSE file.