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

@zhangt58/svelte-vtable

v0.3.3

Published

A Svelte 5 library for virtualized data tables with sorting, selection, and pagination controls.

Readme

@zhangt58/svelte-vtable

A Svelte 5 library providing virtualized data tables with sorting, selection, search, filtering, and pagination controls. Built with Svelte 5 runes for reactive state management.

Features

  • 🚀 Virtualized rendering - Efficiently handles large datasets
  • 🔄 Sorting - Click column headers to sort
  • 🔍 Search - Built-in search filtering
  • 🎯 Multi-select filters - Column-based filtering with OR/AND logic
  • 📄 Pagination - Configurable page controls with ellipsis navigation
  • Selection - Row selection with callback support
  • 🎨 Styling - Tailwind CSS based with light/dark mode support
  • 📱 Responsive - Flexible column widths and layouts

Installation

npm install @zhangt58/svelte-vtable

Peer Dependencies

This library requires the following peer dependencies:

npm install svelte@^5.0.0 tailwindcss@^4.0.0 svelte-virtuallists@^1.0.0

flowbite-svelte is optional — only needed if you use the Flowbite adapter (see Flowbite Adapter).

Quick Start

<script>
  import { DataTable, DataTableControls, DataTableFilters } from '@zhangt58/svelte-vtable';

  let items = $state([
    { id: 1, name: 'Alice', email: '[email protected]', dept: 'Engineering' },
    { id: 2, name: 'Bob', email: '[email protected]', dept: 'Sales' },
    // ... more items
  ]);

  const ui = $state({
    searchQuery: '',
    currentPage: 1,
    perPage: 25,
    sortKey: null,
    sortDir: 'asc',
  });

  const visibleKeys = ['name', 'email', 'dept'];
  const colWidths = { name: 1, email: 2, dept: 1 }; // stretch weights

  // For filtering
  let activeFilters = $state({});
  const columnFilters = [
    { key: 'dept', label: 'Department', uniqueValues: ['Engineering', 'Sales'] },
  ];
</script>

<!-- Multi-select filters -->
<DataTableFilters
  {columnFilters}
  {activeFilters}
  onfilter={({ allFilters }) => (activeFilters = allFilters)}
/>

<!-- Search and pagination controls -->
<DataTableControls
  search={ui.searchQuery}
  currentPage={ui.currentPage}
  perPage={ui.perPage}
  totalItems={items.length}
  onpage={({ page }) => (ui.currentPage = page)}
  onsearch={({ search }) => (ui.searchQuery = search)}
/>

<!-- Virtualized table -->
<DataTable
  {items}
  {visibleKeys}
  bind:sortKey={ui.sortKey}
  bind:sortDir={ui.sortDir}
  onsort={({ key, dir }) => {
    /* optionally trigger a server fetch here */
  }}
  onselect={({ item, index }) => {
    /* handle selection */
  }}
  {colWidths}
>
  {#snippet rowSnippet({ item, index, select, selected })}
    <tr onclick={select}>
      <td>{item.name}</td>
      <td>{item.email}</td>
      <td>{item.dept}</td>
    </tr>
  {/snippet}
</DataTable>

Flowbite Adapter

By default, DataTableControls uses zero external UI-framework dependencies — it renders using plain <input>, <select>, <button>, and <nav> elements with Tailwind utility classes.

If your project already uses Flowbite, you can opt in to the Flowbite-flavoured controls by importing from the /flowbite entrypoint:

# Install optional Flowbite peer dependencies
npm install flowbite-svelte@^1.0.0
<script>
  // Import Flowbite-flavoured DataTableControls (Search, Badge, Select, Modal)
  import { DataTable, DataTableControls, DataTableFilters } from '@zhangt58/svelte-vtable/flowbite';
</script>

<DataTableControls
  search={searchQuery}
  {currentPage}
  bind:perPage
  totalItems={items.length}
  onpage={({ page }) => (currentPage = page)}
  onsearch={({ search }) => (searchQuery = search)}
  {columnFilters}
  {activeFilters}
  onfilter={handleFilterChange}
/>

The Flowbite entrypoint re-exports everything from the default entrypoint, so you can use a single import for all components and utilities.

See examples/flowbite/App.svelte for a complete working example.

Components

DataTable (previously named VirtualDataTable)

A virtualized table component for efficient rendering of large datasets. The component was renamed to DataTable; VirtualDataTable is still exported for backward compatibility.

Props

| Prop | Type | Default | Description | | -------------- | ----------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | items | Array | [] | Array of data items to display | | columns | ColumnDef[] | undefined | Unified column definitions (recommended). When provided, visibleKeys, colWidths, and rowSnippet may be omitted. See ColumnDef below. | | visibleKeys | Array | [] | (Legacy) Array of keys to display as column headers. Ignored when columns is provided. | | sortKey | string \| null | null | Current sort key (column header); supports bind:sortKey | | sortDir | 'asc' \| 'desc' | 'asc' | Sort direction; supports bind:sortDir | | className | string | '' | Additional CSS classes | | style | string | '' | Inline styles | | emptyMessage | string | 'No items to display.' | Message when no items | | colWidths | object \| Array | {} | (Legacy) Column width configuration (stretch weights or pixel values). Ignored when columns is provided. | | selected | any | null | Currently selected item | | onselect | function | undefined | Callback when a row is selected: ({item, index}) => void | | onsort | function | undefined | Callback when sort changes: ({key, dir}) => void. When provided, local sorting is skipped (server-side sort pattern). Omit entirely (do not pass () => {}) to enable local sorting. | | rowSnippet | Snippet | undefined | Svelte 5 snippet for rendering rows. Optional when columns is provided (a default row is rendered using each column's cellSnippet or raw value). |

Row Snippet Parameters

The rowSnippet receives an object with:

  • item - The current row data
  • index - Row index in the current page
  • select - Function to call to select this row
  • selected - Currently selected item (for comparison)

ColumnDef

ColumnDef is the unified column configuration object. Import the JSDoc type via:

// @type {import('@zhangt58/svelte-vtable').ColumnDef}

| Field | Type | Default | Description | | --------------- | ----------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- | | key | string | required | Data key. Must match a property on the row item. | | label | string | key | Column header label. Defaults to key. | | width | number \| string | 1 | Stretch weight (number) or CSS value (e.g. '120px'). | | sortable | boolean | true | Whether clicking the header sorts the table. | | filterType | 'value' \| 'daterange' \| 'datetimerange' \| 'none' | 'value' | Filter UI for this column. 'none' excludes it from buildColumnFilters. | | headerSnippet | Snippet<[{key, label, sortKey, sortDir}]> | undefined | Custom Svelte 5 snippet rendered inside <th>. | | cellSnippet | Snippet<[{item, value, index}]> | undefined | Custom Svelte 5 snippet rendered inside each <td>. Used for default row rendering when rowSnippet is not provided. |

Minimal example using columns

<script>
  import { DataTable, buildColumnFilters, applyFilters } from '@zhangt58/svelte-vtable';

  const items = [...]; // your data

  /** @type {import('@zhangt58/svelte-vtable').ColumnDef[]} */
  const columns = [
    { key: 'id',         label: 'ID',         width: 1, filterType: 'none' },
    { key: 'name',       label: 'Name',        width: 3, filterType: 'none' },
    { key: 'department', label: 'Department',  width: 2, filterType: 'value' },
    { key: 'hireDate',   label: 'Hire Date',   width: 2, filterType: 'daterange' },
  ];

  // Build filters directly from columns — filterType:'none' columns are skipped automatically
  let columnFilters = buildColumnFilters(items, columns);
  let activeFilters = $state({});
  const filteredItems = $derived(applyFilters(items, activeFilters));
</script>

<!-- No visibleKeys, colWidths, or rowSnippet required -->
<DataTable items={filteredItems()} {columns} style="height: 400px" />

Filter example (filterCallback)

Below is a minimal example showing how to wire the filterCallback prop on VirtualDataTable to apply per-column inline filters emitted by the header inputs. The idea: keep a small filters map in your component, compute a derived filteredItems list, and update filters when the table emits a column filter change.

<script>
  import { DataTable } from '@zhangt58/svelte-vtable';

  // raw dataset you want to display/filter
  const rawItems = [
    { id: 1, name: 'Alice', email: '[email protected]' },
    { id: 2, name: 'Bob', email: '[email protected]' },
    { id: 3, name: 'Carol', email: '[email protected]' },
  ];

  // per-column filter values
  let filters = {};

  // derived, reactive filtered list
  $: filteredItems = rawItems.filter((item) =>
    Object.entries(filters).every(
      ([k, v]) =>
        !v ||
        String(item[k] ?? '')
          .toLowerCase()
          .includes(v.toLowerCase()),
    ),
  );

  // called when a ColumnHeader emits a filter event: { key, value }
  function handleFilter({ key, value }) {
    filters = { ...filters, [key]: value };
  }
</script>

<DataTable items={filteredItems} visibleKeys={['name', 'email']} filterCallback={handleFilter}>
  {#snippet rowSnippet({ item, index, select, selected })}
    <tr onclick={select}>
      <td>{item.name}</td>
      <td>{item.email}</td>
    </tr>
  {/snippet}
</DataTable>

Column Widths

Column widths can be specified as:

  • Stretch weights (numbers): Distributed proportionally, e.g., { name: 1, description: 3 }
  • Pixel values (strings): Fixed widths, e.g., { id: '80px', name: '200px' }

Row Snippet Parameters

The rowSnippet receives an object with:

  • item - The current row data
  • index - Row index in the current page
  • select - Function to call to select this row
  • selected - Currently selected item (for comparison)

DataTableControls

Controls component for search and pagination.

Props

| Prop | Type | Default | Description | | ----------------- | ---------- | ----------- | -------------------------------------------------------------------------------------------------------- | | search | string | '' | Current search query | | currentPage | number | 1 | Current page number | | perPage | number | 25 | Items per page | | totalItems | number | 0 | Total number of items | | onpage | function | undefined | Callback for page changes: ({page}) => void | | onsearch | function | undefined | Callback for search changes: ({search}) => void | | onfilterstoggle | function | undefined | Callback when filters panel toggled: ({visible}) => void | | onperpage | function | undefined | Callback when per-page changes: ({perPage}) => void | | onfilter | function | undefined | Callback when filters change (passed through to DataTableFilters): ({key, values, allFilters}) => void |

DataTableFilters

Multi-select filter component with flexible layout options. Implements OR logic within columns and AND logic across columns.

Props

| Prop | Type | Default | Description | | --------------- | ---------------------------- | -------------- | ------------------------------------------------------------------- | | columnFilters | Array | [] | Array of filter configurations (see below) | | direction | 'horizontal' \| 'vertical' | 'horizontal' | Layout direction for filter grid | | activeFilters | Object | {} | Current active filters { columnKey: [selectedValues] } | | onfilter | function | undefined | Callback when filters change: ({key, values, allFilters}) => void | | oncolumnsort | function | undefined | Callback when column sort changes: ({key, mode, dir}) => void | | className | string | '' | Additional CSS classes | | showCounts | boolean | true | Whether to show value counts |

columnFilters Structure

Each item in columnFilters should have:

{
  key: 'columnKey',           // Column identifier
  label: 'Column Label',      // Display label
  uniqueValues: [...],        // Array of unique values
  counts: { value: count }    // Optional: value frequency map
}

Filter Logic

  • OR within column: Selecting multiple values in one filter matches rows with ANY of those values
  • AND across columns: All active column filters must match for a row to pass

Example usage with data filtering:

<script>
  import { DataTableFilters } from '@zhangt58/svelte-vtable';

  const data = [
    { name: 'Alice', dept: 'Engineering', status: 'Active' },
    { name: 'Bob', dept: 'Sales', status: 'Active' },
    // ...
  ];

  let activeFilters = $state({});

  const columnFilters = [
    {
      key: 'dept',
      label: 'Department',
      uniqueValues: ['Engineering', 'Sales'],
      counts: { Engineering: 5, Sales: 3 },
    },
    {
      key: 'status',
      label: 'Status',
      uniqueValues: ['Active', 'Inactive'],
      counts: { Active: 7, Inactive: 1 },
    },
  ];

  // Apply filters
  const filteredData = $derived(() => {
    return data.filter((item) => {
      for (const [key, values] of Object.entries(activeFilters)) {
        if (values?.length > 0 && !values.includes(item[key])) {
          return false; // AND logic across columns
        }
      }
      return true;
    });
  });
</script>

<DataTableFilters
  {columnFilters}
  {activeFilters}
  onfilter={({ allFilters }) => (activeFilters = allFilters)}
  direction="horizontal"
  showCounts={true}
/>

For complete examples, see DATATABLEFILTERS_README.md.

Styling (important)

This library ships with built-in CSS to provide sensible default visuals (light/dark mode, striping, hover, selection, sticky headers, and pagination control styling). There are two important details to understand so the styles work correctly for both local development and package consumers:

  1. Source vs published CSS
  • The human-editable source is src/lib/styles.css. It is written using Svelte-style :global(...) wrappers so the selectors are clear and scoped intentionally when compiled inside Svelte components.
  • Bundlers and consumer projects do not process Svelte :global(...) tokens when they load plain .css files. For that reason the package publishes a compiled plain-CSS file at src/lib/dist/styles.css. This compiled file has the :global(...) wrappers removed so the selectors are normal CSS selectors and will match in any bundler.
  1. How you should import the styles as a consumer
  • Recommended (installed from npm):
import '@zhangt58/svelte-vtable/styles.css';

This import resolves to the precompiled src/lib/dist/styles.css via the package exports entry in package.json.

  • Working inside the repo or during local development (components import the file relatively):

Components inside this package import the compiled CSS via:

@import '../lib/dist/styles.css';

Do not import the raw src/lib/styles.css from components or consumers — the :global(...) wrappers will remain and selectors won't behave as intended outside the Svelte compiler.

  1. How :global works here and why we compile it
  • :global(.selector) is a Svelte compiler token used when writing styles in Svelte components. It tells the Svelte compiler to treat the selector as global instead of scoping it to the component.
  • When shipping plain .css files to consumers, those tokens must be converted into regular selectors. The repository contains a tiny build script (scripts/build-styles.cjs) that strips :global(...) wrappers and emits a compiled src/lib/dist/styles.css. This is what gets published and what consumers should import.
  1. Build scripts and publishing
  • The project has a build:styles script that generates src/lib/dist/styles.css from src/lib/styles.css:
npm run build:styles
  • package.json already runs npm run build:styles as part of prepublishOnly to ensure the compiled CSS is present before publishing.

Optional recommendation: add prepare to package.json so that npm install in development or certain CI flows will also generate the compiled CSS automatically:

"scripts": {
  "build:styles": "node ./scripts/build-styles.cjs",
  "prepare": "npm run build:styles",
  "prepublishOnly": "npm run build:styles && npm run check"
}

This is optional — the current prepublishOnly is sufficient for publishing.

Theming

svelte-vtable exposes a set of CSS custom properties (variables) as its theming contract. All accent-coloured elements (active pagination button, active filter badge, filter button active state, dual-range slider) read from these variables, so you can re-theme the library without forking source or reconfiguring Tailwind.

Default values

:root {
  --vtable-color-accent: #16a34a; /* green-600 — active elements */
  --vtable-color-accent-light: #dcfce7; /* green-100 — active element background */
  --vtable-color-accent-border: #22c55e; /* green-500 — active element border */
  --vtable-color-accent-text: #15803d; /* green-700 — active element text */

  --vtable-color-danger: #dc2626; /* red-600   — destructive actions */
  --vtable-color-info: #3b82f6; /* blue-500  — dual-range slider */

  --vtable-radius: 0.375rem; /* rounded-md */
  --vtable-row-height: 2.25rem; /* virtual-scroll row height hint */
}

Overriding the theme

Set any property on :root for a global override, or on a wrapper element for a scoped override:

/* app.css — global purple theme */
:root {
  --vtable-color-accent: #7c3aed;
  --vtable-color-accent-light: #ede9fe;
  --vtable-color-accent-border: #8b5cf6;
  --vtable-color-accent-text: #6d28d9;
}
<!-- Scoped to a single section of a page -->
<div
  style="--vtable-color-accent: #7c3aed; --vtable-color-accent-light: #ede9fe; --vtable-color-accent-border: #8b5cf6; --vtable-color-accent-text: #6d28d9;"
>
  <DataTableControls ... />
  <DataTable ... />
</div>

What changes with --vtable-color-accent

| Element | Before (default green) | After override | | ------------------------------------ | ----------------------------- | --------------------------------- | | Active pagination button | green-100 bg + green-600 text | accent-light bg + accent text | | Active filter count badge (controls) | green-600 background | accent background | | Active filter button border | green-500 | accent-border | | Active filter badge (count / range) | green-600 background | accent background | | "Show All" button | green-600 border + text | accent border + text |

The live example at examples/ThemedExample.svelte lets you switch between green, purple, orange, and rose themes at runtime.

TypeScript

The library is written in JavaScript with JSDoc annotations. Type checking is available via svelte-check.

License

MIT © zhangt58