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

@renderorange/alanbradley

v2.0.1

Published

lightweight table filter, sorting, and pagination library, with optional background chunking

Downloads

372

Readme

@renderorange/alanbradley

Lightweight table filter, sorting, and pagination library, with optional background chunking.

alanbradley fights for the user on the grid.

Features

  • zero dependencies
  • progressive background chunking for large datasets
  • client-side sort (numeric, date, string)
  • global text search across configurable fields (including nested data)
  • per-column dropdown filters
  • pagination with configurable page sizes
  • expandable subtable rows
  • fully themable via CSS custom properties

Installation

npm install @renderorange/alanbradley

Or copy src/alanbradley.js and src/alanbradley.css into your project.

Usage

<link rel="stylesheet" href="alanbradley.css" />

<table id="my-table">
    <tbody></tbody>
</table>
<div class="alanbradley-status"></div>

<script src="alanbradley.js"></script>
<script>
    new AlanBradley("#my-table", {
        api: "/api/items",
        columns: [
            { key: "id", label: "ID", sortable: true },
            { key: "name", label: "Name", sortable: true },
            { key: "status", label: "Status", sortable: true },

            // add an empty column definition for the edit button below
            { key: "", label: "", sortable: false },
        ],
        filters: [
            { key: "status", label: "Status", options: ["active", "closed"] },
        ],
        search_fields: ["name", "status"],
        render_row: function (item) {
            return (
                '<tr>' +
                '<td data-label="ID">' + item.id + '</td>' +
                '<td data-label="Name">' + item.name + '</td>' +
                '<td data-label="Status">' + item.status + '</td>' +
                '<td data-label=""><a href="/item/' +
                item.id +
                '/edit" class="btn btn-sm">Edit</a></td>' +
                '</tr>'
            );
        },
    });
</script>

API Contract

Your API endpoint must accept:

GET /api/items?chunk=1&page_size=500

And return:

{
    "data": [{ "id": 1, "name": "Item 1" }],
    "total": 150,
    "chunk": 1,
    "page_size": 500
}

| Field | Type | Description | | ----------- | ------ | ---------------------------- | | data | array | Array of row objects | | total | number | Total records in the dataset | | chunk | number | Current chunk number | | page_size | number | Records per chunk |

Options

| Option | Type | Default | Description | | -------------------- | -------- | --------------------- | -------------------------------------------- | | api | string | required | API endpoint URL | | columns | array | required | Column definitions | | columns[].key | string | required | Data field name (also used as sort key) | | columns[].label | string | required | Header display text | | columns[].sortable | boolean | false | Whether column is sortable | | filters | array | [] | Dropdown filter definitions | | filters[].key | string | required | Data field to filter on | | filters[].label | string | required | Display label | | filters[].options | array | required | Values (strings or {value, label} objects) | | search_fields | array | [] | Field names to search across (supports dot-notation for nested data) | | render_row | function | required | Returns HTML string for a data row | | render_expanded | function | null | Returns HTML string for expanded row content | | on_expand | function | null | Callback fired when a row is expanded | | on_collapse | function | null | Callback fired when a row is collapsed | | page_size | number | 50 | Rows per page | | page_size_options | array | [25, 50, 100] | Available page sizes | | chunk | boolean | true | Enable progressive chunked loading | | chunk_size | number | 500 | Records per chunk | | search_placeholder | string | 'Search...' | Search input placeholder | | empty_message | string | 'No records found.' | Message when no data |

Public Methods

| Method | Description | | ----------------------------- | ---------------------------------------------- | | refresh() | Re-fetch all data | | go_to_page(n) | Navigate to page n | | set_sort(column, direction) | Set sort programmatically | | set_filter(key, value) | Set a filter value | | clear_filters() | Reset all filters and search | | search(term) | Set search term programmatically | | toggle_row(index) | Toggle expanded state of row at index | | expand_row(index) | Expand row at index | | collapse_row(index) | Collapse row at index | | collapse_all() | Collapse all expanded rows | | destroy() | Remove all generated DOM elements |

HTML Structure

<!-- Controls row: search + filters -->
<table class="alanbradley">
    <thead>
        <!-- generated by JS -->
    </thead>
    <tbody>
        <!-- generated by JS -->
        <!-- with render_expanded, each row gets a toggle cell, -->
        <!-- and expanded rows get a colspan td with your content -->
    </tbody>
</table>

<!-- Status bar: page size, pagination, count -->
<div class="alanbradley-status">
    <select class="alanbradley-page-size">...</select>
    <span class="alanbradley-page-size-label">per page</span>
    <div class="alanbradley-pagination">...</div>
    <span class="alanbradley-status-text">Showing 1-50 of 200</span>
</div>

Expandable Rows

When render_expanded is provided, each row gets a toggle button in the first column. Clicking it reveals the content returned by render_expanded:

new AlanBradley("#my-table", {
    api: "/api/items",
    columns: [
        { key: "id", label: "ID", sortable: true },
        { key: "name", label: "Name", sortable: true },
        { key: "status", label: "Status", sortable: true },
    ],
    search_fields: ["name", "items.description"],
    render_row: function (item) {
        return (
            "<tr>" +
            '<td data-label="ID">' + item.id + "</td>" +
            '<td data-label="Name">' + item.name + "</td>" +
            '<td data-label="Status">' + item.status + "</td>" +
            "</tr>"
        );
    },
    render_expanded: function (item) {
        if (!item.items || item.items.length === 0) {
            return "<p>No items found.</p>";
        }
        var html = '<dl>';
        for (var i = 0; i < item.items.length; i++) {
            html += '<dt>' + item.items[i].label + '</dt>';
            html += '<dd>' + item.items[i].description + '</dd>';
        }
        html += '</dl>';
        return html;
    },
});

Expanded rows are collapsed automatically when sorting, filtering, searching, or changing pages.

Nested Search

search_fields supports dot-notation paths for searching nested data:

search_fields: ["name", "items.description"]

When a path resolves to an array (e.g., items), each item's field (e.g., description) is searched. A match on any nested item includes the parent row in results.

CSS Customization

All styling uses CSS custom properties. Override :root or target a specific container:

:root {
    --alanbradley-sort-arrow-color: #6c757d;
    --alanbradley-active-sort-color: #0d6efd;
    --alanbradley-row-hover-bg: #f1f3f5;
    --alanbradley-pagination-active-bg: #0d6efd;
    --alanbradley-pagination-active-color: #fff;
    --alanbradley-pagination-hover-bg: #e9ecef;
    --alanbradley-loading-opacity: 0.5;
    --alanbradley-filter-bg: #fff;
    --alanbradley-filter-border: #ced4da;
    --alanbradley-filter-border-radius: 0.375rem;
    --alanbradley-search-bg: #fff;
    --alanbradley-search-border: #ced4da;
    --alanbradley-search-border-radius: 0.375rem;
    --alanbradley-status-color: #6c757d;
    --alanbradley-empty-color: #6c757d;
    --alanbradley-toggle-color: #6c757d;
    --alanbradley-toggle-hover-color: #0d6efd;
    --alanbradley-expanded-bg: #f8f9fa;
    --alanbradley-expanded-border: #dee2e6;
}

Dark mode

[data-theme="dark"] {
    --alanbradley-row-hover-bg: #363636;
    --alanbradley-filter-bg: #333;
    --alanbradley-filter-border: #555;
    --alanbradley-search-bg: #333;
    --alanbradley-search-border: #555;
    --alanbradley-status-color: #999;
    --alanbradley-empty-color: #999;
    --alanbradley-toggle-color: #999;
    --alanbradley-toggle-hover-color: #6c9eff;
    --alanbradley-expanded-bg: #2b2b2b;
    --alanbradley-expanded-border: #444;
}

Bootstrap 5

Add Bootstrap's .table class alongside .alanbradley for base table styling:

<table id="my-table" class="table alanbradley"></table>

License

MIT