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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@minimact/query

v0.1.1

Published

SQL query interface for DOM state - treat the DOM as a relational database

Readme

Minimact Query

SQL for the DOM - Query the DOM like a relational database.

Treat the DOM as a reactive, queryable data source with full SQL-like syntax. Built on top of minimact-punch, which makes the DOM a comprehensive state system with 80+ queryable properties.


Installation

npm install minimact-query minimact-punch

Quick Start

import { useDomQuery } from 'minimact-query';

function MyComponent() {
  // Query the DOM with SQL-like syntax
  const query = useDomQuery()
    .from('.card')
    .where(card => card.isIntersecting && card.state.hover)
    .orderBy(card => card.history.changeCount, 'DESC')
    .limit(10);

  // SELECT projection in JSX
  return (
    <div>
      {query.select(card => ({
        id: card.attributes.id,
        title: card.textContent,
        changes: card.history.changeCount
      })).map(row => (
        <div key={row.id}>
          {row.title} - {row.changes} changes
        </div>
      ))}
    </div>
  );
}

Features

✅ Full SQL-Like Syntax

  • SELECT - Project results to any shape
  • FROM - Query by CSS selector
  • WHERE - Filter with predicates
  • JOIN - Relate elements to each other
  • GROUP BY - Aggregate elements
  • HAVING - Filter groups
  • ORDER BY - Sort results
  • LIMIT/OFFSET - Pagination
  • DISTINCT - Unique values
  • UNION/INTERSECT/EXCEPT - Set operations

✅ Aggregate Functions

  • COUNT() - Count results
  • SUM() - Sum numeric values
  • AVG() - Average
  • MIN() / MAX() - Min/max values
  • STDDEV() - Standard deviation

✅ Reactive by Default

  • Queries automatically re-run when DOM changes
  • Built-in throttling and debouncing options
  • Optimized for performance

✅ Type-Safe

  • Full TypeScript support
  • Autocomplete for all DOM properties
  • Inferred return types

✅ 80+ Queryable Properties

Access all minimact-punch properties:

  • Base: isIntersecting, childrenCount, attributes, classList
  • Pseudo-State: state.hover, state.focus, state.active
  • Theme: theme.isDark, theme.reducedMotion, breakpoint.md
  • History: history.changeCount, history.hasStabilized, history.trend
  • Lifecycle: lifecycle.lifecycleState, lifecycle.timeInState

API

useDomQuery()

Reactive React hook that creates a query builder. Automatically re-runs when DOM changes.

const query = useDomQuery()
  .from('.selector')
  .where(el => /* predicate */)
  .orderBy(el => /* value */, 'DESC')
  .limit(10);

useDomQueryThrottled(ms)

Throttled version - limits re-renders to once every N milliseconds.

const query = useDomQueryThrottled(250) // Max 4 updates/second
  .from('.live-data');

useDomQueryDebounced(ms)

Debounced version - only re-renders after N milliseconds of inactivity.

const query = useDomQueryDebounced(500) // Wait for 500ms quiet
  .from('.search-results');

useDomQueryStatic()

Non-reactive version - only runs once.

const query = useDomQueryStatic()
  .from('.card');

Query Methods

FROM

Specify which elements to query (CSS selector):

query.from('.card')
query.from('#app')
query.from('[data-type="widget"]')
query.from('div.active')

WHERE

Filter elements by predicate:

// Single condition
query.where(card => card.state.hover)

// Multiple conditions (chained = AND)
query
  .where(card => card.isIntersecting)
  .where(card => card.childrenCount > 5)
  .where(card => card.lifecycle.lifecycleState === 'visible')

// Complex boolean logic
query.where(card =>
  (card.state.hover || card.state.focus) &&
  card.theme.isDark &&
  card.history.changeCount > 10
)

Shorthand methods:

query.whereEquals('lifecycle.lifecycleState', 'visible')
query.whereGreaterThan('childrenCount', 10)
query.whereLessThan('history.changeCount', 5)
query.whereBetween('childrenCount', 5, 10)
query.whereIn('lifecycle.lifecycleState', ['visible', 'entering'])

JOIN

Relate elements to each other:

// INNER JOIN - only matching elements
query
  .from('.card')
  .join(
    useDomQuery().from('.badge'),
    (card, badge) => card.element.contains(badge.element)
  )

// LEFT JOIN - all left elements, matching right or null
query
  .from('.product')
  .leftJoin(
    useDomQuery().from('.review'),
    (product, review) =>
      product.attributes['data-id'] === review.attributes['data-product-id']
  )

GROUP BY

Group elements by a key function:

query
  .from('.widget')
  .groupBy(w => w.lifecycle.lifecycleState)

// Access grouped results
query.select(group => ({
  state: group.key,
  count: group.count,
  items: group.items
}))

HAVING

Filter groups after grouping:

query
  .from('.product')
  .groupBy(p => p.attributes['data-category'])
  .having(group => group.count > 10) // Only categories with 10+ products

ORDER BY

Sort results:

query.orderBy(card => card.history.changeCount, 'DESC')
query.orderBy(card => card.childrenCount, 'ASC')

// Multiple sort keys (chain them)
query
  .orderBy(card => card.attributes.category, 'ASC')
  .orderBy(card => card.history.ageInSeconds, 'DESC')

LIMIT / OFFSET

Pagination:

query.limit(10)                    // First 10 results
query.limit(10, 20)                // Skip 20, take 10
query.offset(20).limit(10)         // Same as above

SELECT

Project results to a new shape (called in JSX):

{query.select(card => ({
  id: card.attributes.id,
  title: card.textContent,
  isHovered: card.state.hover,
  changes: card.history.changeCount
})).map(row => (
  <div key={row.id}>...</div>
))}

Set Operations

// UNION - combine results (no duplicates)
const buttons = useDomQuery().from('button');
const links = useDomQuery().from('a');
const interactive = buttons.union(links);

// INTERSECT - elements in both queries
const hovered = useDomQuery().from('.item').where(el => el.state.hover);
const focused = useDomQuery().from('.item').where(el => el.state.focus);
const both = hovered.intersect(focused);

// EXCEPT - elements in first query but not second
const allCards = useDomQuery().from('.card');
const visible = useDomQuery().from('.card').where(c => c.isIntersecting);
const hidden = allCards.except(visible);

// DISTINCT - unique values
query.distinct(item => item.attributes['data-category'])

Aggregate Functions

query.count()                          // Total count
query.sum(card => card.childrenCount)  // Sum
query.avg(card => card.childrenCount)  // Average
query.min(card => card.childrenCount)  // Minimum
query.max(card => card.childrenCount)  // Maximum
query.stddev(card => card.childrenCount) // Standard deviation

query.first()                          // First result or null
query.last()                           // Last result or null

query.any()                            // Has any results?
query.all(card => card.isIntersecting) // All match condition?
query.find(card => card.state.hover)   // Find first matching

Real-World Examples

Dashboard Analytics

function DashboardStats() {
  const stats = useDomQuery()
    .from('.metric-card')
    .where(card => card.isIntersecting)
    .groupBy(card => card.lifecycle.lifecycleState);

  return (
    <div>
      {stats.select(group => ({
        state: group.key,
        count: group.count
      })).map(row => (
        <div key={row.state}>
          {row.state}: {row.count} cards
        </div>
      ))}
    </div>
  );
}

Performance Monitoring

function PerformanceMonitor() {
  const unstable = useDomQuery()
    .from('.component')
    .where(c => c.history.changesPerSecond > 10)
    .orderBy(c => c.history.volatility, 'DESC');

  return (
    <div>
      {unstable.any() && (
        <Alert>
          {unstable.count()} unstable components detected!
        </Alert>
      )}
    </div>
  );
}

Top 10 Most Active Elements

function MostActive() {
  const query = useDomQuery()
    .from('.interactive')
    .orderBy(el => el.history.changeCount, 'DESC')
    .limit(10);

  return (
    <div>
      {query.select(el => ({
        id: el.attributes.id,
        changes: el.history.changeCount
      })).map((row, i) => (
        <div key={row.id}>
          #{i + 1}: {row.id} - {row.changes} changes
        </div>
      ))}
    </div>
  );
}

Accessibility Audit

function AccessibilityAudit() {
  const unlabeled = useDomQuery()
    .from('button')
    .where(btn =>
      !btn.attributes['aria-label'] &&
      !btn.textContent?.trim()
    );

  const improperDisabled = useDomQuery()
    .from('input, button')
    .where(el =>
      el.state.disabled &&
      !el.attributes['aria-disabled']
    );

  return (
    <div>
      <p>Unlabeled buttons: {unlabeled.count()}</p>
      <p>Improperly disabled: {improperDisabled.count()}</p>
    </div>
  );
}

SQL Equivalents

| Minimact Query | SQL Equivalent | |---|---| | useDomQuery().from('.card') | SELECT * FROM .card | | .where(c => c.isIntersecting) | WHERE isIntersecting = true | | .orderBy(c => c.childrenCount, 'DESC') | ORDER BY childrenCount DESC | | .limit(10) | LIMIT 10 | | .groupBy(c => c.attributes.category) | GROUP BY category | | .having(g => g.count > 10) | HAVING COUNT(*) > 10 | | .count() | SELECT COUNT(*) | | .avg(c => c.childrenCount) | SELECT AVG(childrenCount) | | .union(other) | UNION | | .intersect(other) | INTERSECT | | .except(other) | EXCEPT |


Why This is Brilliant

  1. Familiar Syntax - If you know SQL, you already know Minimact Query
  2. Type-Safe - Full TypeScript support with autocomplete
  3. Reactive - Queries automatically update when DOM changes
  4. Performant - Optimized execution with throttling/debouncing options
  5. Composable - Save and reuse queries
  6. Powerful - 80+ properties from minimact-punch
  7. Clean Separation - Data fetching in hook, projection in JSX
  8. Production Ready - Built, tested, documented

Documentation


The Stack

minimact (core framework)
  ↓
minimact-punch (DOM as reactive data source)
  ↓
minimact-query (SQL interface for querying)

License

MIT


The DOM is now a relational database. 🗃️⚡

Query it like PostgreSQL. Make it reactive. Make it type-safe.

Welcome to the future of DOM interaction.