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

vanilla-ssr

v0.4.1

Published

SSR-first admin UI toolkit for vanilla JavaScript with server-rendered markup and client hydration.

Downloads

52

Readme

Vanilla SSR (npm: vanilla-ssr)

npm version npm downloads bundle size GitHub Stars CI TypeScript License: MIT

Vanilla Components is an SSR-first UI toolkit for vanilla JavaScript dashboards and multi-framework sites. Render accessible markup on the server (Blade/Twig, Laravel, Razor, Go templates, Express, Next.js) and hydrate behaviour on the client without adopting a SPA framework. The library ships with battle-tested admin widgets, shadcn/ui-inspired styling, and TypeScript definitions for predictable integrations.

If you find this useful, please consider starring the repository to support the project.

👉 Star on GitHub

English | 한국어

📚 Documentation & Playground | 📦 npm Package

The package was renamed from vanila-components to vanilla-ssr. Most users only need to update import paths. See MIGRATION.md.

Highlights

  • 🎨 shadcn/ui Design (v0.3.1) – Professional, refined styling with subtle shadows, HSL colors, and perfect dark mode.
  • SSR-first – Every component exposes render*Markup and hydrate* helpers so you can generate HTML on the server and attach behaviour on the client.
  • Admin-ready widgets – Filter bars, data tables, metric cards, accordions, toasts, modals, bottom sheets, cards, input/select/date pickers, pagination, banners, and file uploaders.
  • ** Utility components** (v0.3.0) – Pre-styled badges, chips, status dots, and table helpers to eliminate repetitive CSS.
  • ** Theme system** (v0.3.0) – Built-in light/dark mode support with customizable HSL color tokens.
  • Accessible by default – Focus ring states, keyboard-friendly interactions, ARIA attributes, and consistent design tokens.
  • Styling options – Import the bundled CSS, inject at runtime, or pull the raw stylesheet string for custom pipelines.
  • Consistent DX – Mount helpers share optional target, id, and className props so you can wire components without guessing argument order.
  • TypeScript + tests – Rich typings and Vitest coverage for critical behaviours (SSR markup, hydration, event APIs, custom delete flows, etc.).
  • Docs & playground – A Next.js documentation site with interactive previews and a live dashboard playground (npm run docs:dev).

Installation

npm install vanilla-ssr

Quick Start

import {
  hydrateAllVanilaComponents,
  showModal,
  showToast,
} from "vanilla-ssr";

// 1) inject styles once + hydrate any SSR markup
hydrateAllVanilaComponents();

showModal({
  title: "Welcome",
  message: "Vanilla Components ready",
  primaryButtonText: "Close",
});

showToast({
  message: "Saved",
  type: "success",
});

hydrateAllVanilaComponents() accepts the same root selector as hydrateVanilaComponents() and adds injectStyles, styleTarget flags if you need to control where the CSS string is mounted.

Lazy/Conditional Hydration

import {
  hydrateOnVisible,
  hydrateOnInteraction,
  hydrateOnIdle,
} from "vanilla-ssr/client";

// When visible (IntersectionObserver)
hydrateOnVisible("[data-vanila-component='data-table']", (el) => {
  import("vanilla-ssr/components/data-table").then(({ hydrateDataTable }) => {
    hydrateDataTable(el as HTMLDivElement);
  });
});

// On first interaction (click/focus/keydown)
hydrateOnInteraction("[data-vanila-component='modal']", (el) => {
  import("vanilla-ssr/components/modal").then(({ hydrateModal }) => {
    hydrateModal(el as HTMLDivElement);
  });
});

// When the browser is idle (fallback timeout 1s)
hydrateOnIdle(() => {
  // pre-hydrate low-priority widgets
});

Global Configuration (target, styles, CSP nonce)

import { configure, hydrateAllVanilaComponents } from "vanilla-ssr";

configure({
  // default container for overlays (modal/toast/bottom sheet)
  defaultTarget: "#modal-root",
  // where to inject the CSS when using hydrateAll*
  styleTarget: document,
  // add CSP nonce to injected <style>
  csp: { nonce: (window as any).__CSP_NONCE__ },
  // enable hydration debug logs
  debug: process.env.NODE_ENV === "development",
});

hydrateAllVanilaComponents();

How it fits into your stack

  1. npm install vanilla-ssr
  2. Import the CSS bundle, call hydrateAllVanilaComponents() for auto-injection, or use injectVanilaStyles() (Shadow DOM compatible)
  3. Server-side: generate HTML via render*Markup() helpers
  4. Client-side: call the matching hydrate*() or hydrateVanilaComponents() to attach events
  5. Pure browser usage: use the create*() helpers instead of SSR

Shared component options

To keep APIs predictable, most helpers accept a couple of optional props:

  • target: where to append overlays (showModal, showBottomSheet, showToast, renderCards); accepts a selector or an HTMLElement.
  • id: sets the root element ID (mirrors SSR markup).
  • className: appends custom classes without clobbering defaults.
  • clearContainer: opt-out of clearing the host before renderCards.

These props are fully typed and produce descriptive runtime errors when misused.

SSR-friendly example

// Server (Razor/PHP/Go/etc.)
import { renderInputFieldMarkup } from "vanilla-ssr";

const markup = renderInputFieldMarkup({
  name: "project",
  label: "Project name",
  placeholder: "Internal dashboard",
});
// -> render markup string inside your template

// Client
import { injectVanilaStyles, hydrateInputField } from "vanilla-ssr";

injectVanilaStyles();
document
  .querySelectorAll('[data-vanila-component="input-field"]')
  .forEach((element) => {
    hydrateInputField(element as HTMLDivElement, {
      onChange: (value) => console.log(value),
    });
  });

Loading styles

// Option 1: bundler import
import "vanilla-ssr/styles.css";

// Option 2: runtime injection
import { injectVanilaStyles } from "vanilla-ssr";

injectVanilaStyles();

Component snippets

Below are concise usage examples. Visit the docs (npm run docs:dev) for live previews, API tables, and recipes.

Modal

import { showModal } from "vanilla-ssr";

showModal({
  id: "confirm-delete-modal",
  className: "dashboard-modal",
  target: "#modal-root",
  title: "Delete confirmation",
  message: "Are you sure you want to remove this item?",
  primaryButtonText: "Delete",
  secondaryButtonText: "Cancel",
  onPrimaryAction: () => console.log("confirmed"),
  onSecondaryAction: () => console.log("cancelled"),
});

// Promise-based confirm helper (reduces boilerplate)
import { confirm } from "vanilla-ssr";
const accepted = await confirm({
  title: "Delete confirmation",
  message: "Are you sure?",
  confirmText: "Delete",
  cancelText: "Cancel",
});
if (accepted) {
  // proceed with deletion
}

Toast

import { showToast } from "vanilla-ssr";

showToast({
  message: "Saved successfully",
  type: "success",
  duration: 2500,
  dismissible: true,
  position: "top",
});

Accordion (custom delete flow)

import { createAccordion } from "vanilla-ssr";

const accordion = createAccordion({
  title: "FAQ",
  content: "Click to edit this content inline.",
  onDeleteRequest: ({ defaultHandler }) => defaultHandler(),
  onContentChange: (value) => console.log("new content", value),
});

document.getElementById("accordion-wrapper")?.append(accordion);

Bottom Sheet

import { showBottomSheet } from "vanilla-ssr";

showBottomSheet({
  id: "filters-bottom-sheet",
  className: "dashboard-bottom-sheet",
  target: document.body,
  title: "Filters",
  content: "Customise your dashboard filters.",
  primaryButtonText: "Apply",
  onPrimaryAction: () => console.log("applied"),
});

Card

import { renderCards, bindCardClickEvents } from "vanilla-ssr";

renderCards({
  container: "#card-container",
  cards: [
    {
      id: "project-alpha",
      className: "project-card",
      title: "Card 1",
      description: "Internal note",
      imageUrl: "/card.png",
    },
  ],
});

bindCardClickEvents("#card-container", (title) => {
  console.log(`${title} clicked`);
});

Input Field

import { createInputField } from "vanilla-ssr";

const field = createInputField({
  name: "project-name",
  label: "Project",
  helperText: "Displayed inside the dashboard",
  prefix: "#",
  onChange: (value) => console.log(value),
});

field.setValue("Refactor billing");
field.setError("Required field");

Select Field

import { createSelectField } from "vanilla-ssr";

const select = createSelectField({
  name: "status",
  label: "Status",
  placeholder: "All",
  options: [
    { label: "Pending", value: "pending" },
    { label: "In progress", value: "progress" },
    { label: "Done", value: "done" },
  ],
  onChange: (value) => console.log(value),
});

select.setValue("progress");

Filter Bar

import { createFilterBar } from "vanilla-ssr";

const filterBar = createFilterBar({
  fields: [
    {
      type: "search",
      name: "query",
      label: "Search",
      placeholder: "Project or owner",
    },
    {
      type: "select",
      name: "status",
      label: "Status",
      placeholder: "All",
      options: [
        { label: "Pending", value: "pending" },
        { label: "In progress", value: "progress" },
        { label: "Done", value: "done" },
      ],
    },
  ],
  autoSubmit: true,
  onSubmit: (values) => console.log(values),
});

Date & Time Picker

import { createDatePicker } from "vanilla-ssr";

const createdRange = createDatePicker({
  mode: "date-range",
  name: "createdAt",
  label: "Created between",
  defaultValue: {
    start: "2024-01-01",
    end: "2024-01-07",
  },
  onChange: (value) => console.log(value),
});

createdRange.setMin("2023-12-01");
document.body.append(createdRange);

Pagination

import { createPagination } from "vanilla-ssr";

const pagination = createPagination({
  totalPages: 12,
  currentPage: 4,
  showFirstLast: true,
  onPageChange: (page) => console.log("Page moved to", page),
});

pagination.setCurrentPage(6);
document.body.append(pagination);

Banner

import { createBanner } from "vanilla-ssr";

const banner = createBanner({
  message: "A new version has been released.",
  variant: "success",
  dismissible: true,
  actions: [{ label: "View changelog", href: "/changelog" }],
  onAction: (action) => console.log(action.label, "clicked"),
});

document.body.prepend(banner);

File Uploader

import { createFileUploader } from "vanilla-ssr";

const uploader = createFileUploader({
  name: "attachments",
  label: "Attachments",
  multiple: true,
  maxFiles: 5,
  onFilesChange: (files) => console.log(files.length, "file(s) selected"),
});

document.body.append(uploader);

Data Table

import { createDataTable } from "vanilla-ssr";

const table = createDataTable({
  columns: [
    { key: "name", header: "Project", width: "34%" },
    { key: "owner", header: "Owner", width: "18%" },
    { key: "status", header: "Status", width: "16%" },
    {
      key: "progress",
      header: "Progress",
      align: "right",
      render: (value) => `${value}%`,
    },
  ],
  data: rows,
  caption: "Active projects",
  onRowClick: (row) => console.log(row),
});

Metric Card

import { createMetricCard } from "vanilla-ssr";

const metric = createMetricCard({
  label: "Active projects",
  value: 12,
  description: "Updated every hour",
  variant: "success",
  trend: { direction: "up", label: "+2.1%" },
});

metric.update({ value: 14, trend: { direction: "up", label: "+16%" } });

🆕 Utility Components (v0.3.0)

Pre-built, styled components for common UI patterns. No custom CSS required!

Badge

import { renderBadge, TableHelpers } from "vanilla-ssr";

// Standalone usage
const badge = renderBadge({
  label: "Active",
  variant: "success",
  dot: true,
});

// In DataTable
createDataTable({
  columns: [
    {
      key: "status",
      header: "Status",
      ...TableHelpers.statusColumn({
        active: "success",
        pending: "warning",
        failed: "danger",
      }),
    },
  ],
  data: rows,
});

Result: Eliminates 400+ lines of custom CSS for status badges!

Chip

import { createChip, renderChips } from "vanilla-ssr";

// Interactive chip
const chip = createChip({
  label: "TypeScript",
  value: "ts",
  removable: true,
  onRemove: (value) => console.log("Removed:", value),
});

// Multiple chips
const html = renderChips(["React", "Vue", "Angular"], { removable: true });

Status Dot

import { renderStatusDot, StatusPresets } from "vanilla-ssr";

// Custom status
renderStatusDot({ label: "Online", color: "green", pulse: true });

// Quick presets
StatusPresets.online();
StatusPresets.busy();
StatusPresets.away();

Tooltip

// Attribute-based hydration (works with SSR markup)
// <button data-vanila-tooltip="More info" data-placement="top">?</button>
import { hydrateAllVanilaComponents } from "vanilla-ssr";
hydrateAllVanilaComponents();

// Programmatic attach
import { attachTooltip } from "vanilla-ssr";
attachTooltip(document.getElementById("help")!, { content: "More info", placement: "right" });

Table Helpers

Pre-configured column renderers for DataTable:

import { createDataTable, TableHelpers } from "vanilla-ssr";

createDataTable({
  columns: [
    {
      key: "status",
      header: "Status",
      ...TableHelpers.statusBadgeWithDot({
        healthy: "success",
        warning: "warning",
        critical: "danger",
      }),
    },
    {
      key: "progress",
      header: "Progress",
      ...TableHelpers.progressColumn(), // Visual progress bar
    },
    {
      key: "health",
      header: "Health",
      ...TableHelpers.healthColumn(), // Colored status dots
    },
    {
      key: "createdAt",
      header: "Created",
      ...TableHelpers.dateColumn("relative"), // "2h ago"
    },
    {
      key: "revenue",
      header: "Revenue",
      ...TableHelpers.numberColumn({ prefix: "$", decimals: 2 }),
    },
    {
      key: "tags",
      header: "Tags",
      ...TableHelpers.tagsColumn(), // Multiple badges
    },
  ],
  data: rows,
});

Available helpers:

  • statusColumn() – Status badges
  • statusBadgeWithDot() – Status badges with indicator dot
  • progressColumn() – Visual progress bars
  • healthColumn() – Health status dots
  • booleanColumn() – Checkmark/cross indicators
  • numberColumn() – Formatted numbers with prefix/suffix
  • dateColumn() – Date formatting (short/long/relative)
  • tagsColumn() – Multiple badges
  • truncateColumn() – Ellipsis for long text

🎨 Theme System (v0.3.0)

Built-in dark mode and color customization.

Quick Start

import { applyThemeMode, toggleTheme } from "vanilla-ssr";

// Apply dark mode
applyThemeMode("dark");

// Toggle theme
document.getElementById("theme-toggle")?.addEventListener("click", () => {
  toggleTheme();
});

Custom Colors

import { applyThemeMode } from "vanilla-ssr";

applyThemeMode("light", {
  "--vanila-theme-primary": "#8b5cf6", // Purple
  "--vanila-theme-success": "#10b981", // Custom green
});

Advanced Usage

import { applyTheme, lightTheme, darkTheme } from "vanilla-ssr";

// Full control
const customTheme = {
  ...darkTheme,
  "--vanila-theme-primary": "#ff6b6b",
  "--vanila-theme-bg": "#1a1a2e",
};

applyTheme(customTheme);

// Apply to specific element (Shadow DOM support)
applyTheme(darkTheme, shadowRoot);

Supported tokens:

  • Background: --vanila-theme-bg, --vanila-theme-bg-secondary
  • Foreground: --vanila-theme-fg, --vanila-theme-fg-muted
  • Border: --vanila-theme-border
  • Semantic: --vanila-theme-primary, --vanila-theme-success, --vanila-theme-warning, --vanila-theme-danger, --vanila-theme-info

Scripts

| Command | Description | | -------------------- | ------------------------------------------------------------------------ | | npm run dev | Start the Vite dev server and playground (src/demo). | | npm run build | Generate bundled outputs (ESM/CJS/UMD) and declaration files in dist/. | | npm run preview | Preview the built bundle locally. | | npm run test:run | Run the Vitest suite. | | npm run docs:dev | Start the Next.js docs site (docs/). | | npm run docs:build | Build the documentation site for production deployment. |

Development workflow

  1. npm install
  2. Modify components under src/lib, update demos in src/demo
  3. npm run build to produce publishable output
  4. npm run test:run to make sure behaviour and hydration flows are intact

Roadmap & Adoption

  • See ROADMAP.md for priorities focused on DX, performance, and documentation that drive real-world adoption.
  • Contributions welcome — bugs/docs/examples/starter templates via Issues/PRs.

License

MIT