DEV Community

Cover image for bQuery.js ๐Ÿฅ‚ The jQuery for the Modern Web Platform
Jonas Pfalzgraf
Jonas Pfalzgraf

Posted on

bQuery.js ๐Ÿฅ‚ The jQuery for the Modern Web Platform

A deep-dive into the modular, zero-build frontend framework that bridges the gap between vanilla JavaScript and full-blown frameworks


Introduction

Remember jQuery? That legendary library that made DOM manipulation actually enjoyable back in the day? Well, times have changed, browsers became smarter, the web platform grew up, and build toolchains ballooned into something that requires a PhD to configure properly.

But here's the thing: sometimes you just want to grab an element, wire up some reactive state, and get on with your life. No Vite config, no node_modules rabbit hole, no framework-specific mental model to internalize. Just... JavaScript. On the web. Like the good ol' days, but modern.

That's exactly where bQuery.js comes in.

bQuery (v1.7.0 as of this writing) describes itself as "the jQuery for the modern web platform" and it earns that title. It takes the directness and ergonomics of jQuery and layers on signals-based reactivity, async data composables, native Web Components, motion, forms, i18n, accessibility primitives, drag-and-drop, SSR, and a whole lot more. All of it modular. All of it progressively adoptable.

Let's break it down.


Table of Contents

  1. What Is bQuery?
  2. Getting Started Zero Build, No Excuses
  3. The Core API Good Old DOM Manipulation
  4. Reactive Primitives Signals All the Way Down
  5. Async Data & Fetching
  6. Building Web Components with bQuery
  7. @bquery/ui The Default Component Library
  8. The Broader Ecosystem at a Glance
  9. When Should You Reach for bQuery?
  10. Conclusion

1. What Is bQuery?

bQuery is a modular JavaScript/TypeScript library published under @bquery/bquery on npm. Its philosophy can be summed up in three bullet points:

  • Zero build required works via CDN or ES modules straight in the browser; Vite is optional
  • Secure by default sanitized DOM operations and Trusted Types compatibility out of the box
  • Progressive import only what you need, add complexity only where you need it

The package is split into focused submodules so you never pay for what you don't use:

Module What it does
core Selectors, DOM manipulation, events, utilities
reactive Signals, computed, effects, async composables
component Typed Web Components with Shadow DOM control
motion Transitions, FLIP, springs, parallax, typewriter
security Sanitization, Trusted Types, CSP helpers
platform Storage, cookies, cache, page meta, announcer
router SPA routing with guards and declarative links
store Signal-based global state with persistence
forms Reactive form state and validators
i18n Locale, translations, pluralization, Intl formatting
a11y Focus traps, skip links, live regions, media audits
dnd Draggable, drop zones, sortable lists
media Viewport, network, battery, clipboard wrappers
plugin Custom directive and component registration
devtools Signal/store/component inspection at runtime
testing Component mounts, mock signals, async assertions
ssr Server-side rendering with hydration

That's a lot of ground covered and yet the entry point stays clean because you only import what you actually touch.


2. Getting Started Zero Build, No Excuses

The fastest way to try bQuery is dropping a <script type="module"> into an HTML file:

<!DOCTYPE html>
<html>
  <head>
    <title>bQuery Demo</title>
  </head>
  <body>
    <button id="counter">Count: 0</button>

    <script type="module">
      import { $, signal, effect } from 'https://unpkg.com/@bquery/bquery@1/dist/full.es.mjs';

      const count = signal(0);

      effect(() => {
        $('#counter').text(`Count: ${count.value}`);
      });

      $('#counter').on('click', () => {
        count.value++;
      });
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

No build step. No config. Reactive state, DOM manipulation, and event handling in ~10 lines. If that doesn't put a smile on your face, I don't know what will.

For project-based setups you install it the usual way:

npm install @bquery/bquery
# or
pnpm add @bquery/bquery
# or
bun add @bquery/bquery
Enter fullscreen mode Exit fullscreen mode

And then import from the main entry point or directly from individual submodules:

// Everything from one place
import { $, signal, effect, component } from '@bquery/bquery';

// Or surgically pick submodules
import { $, $$ } from '@bquery/bquery/core';
import { signal, computed, effect, useFetch } from '@bquery/bquery/reactive';
import { component, html, registerDefaultComponents } from '@bquery/bquery/component';
Enter fullscreen mode Exit fullscreen mode

3. The Core API Good Old DOM Manipulation

The core module is the jQuery-familiar part of bQuery. You get $ for single elements and $$ for collections. Both return wrapper objects with a chainable API.

import { $, $$ } from '@bquery/bquery/core';

// Single element throws if not found
$('#app')
  .addClass('loaded')
  .css({ color: 'rebeccapurple', fontSize: '1.2rem' })
  .text('Hello, bQuery!');

// Multiple elements
$$('.card').each((el) => {
  el.toggleClass('visible');
});
Enter fullscreen mode Exit fullscreen mode

The single-element wrapper (BQueryElement) covers:

  • Class/attribute helpers: addClass, removeClass, toggleClass, attr, removeAttr, data, prop
  • Content: text, html (sanitized by default!), htmlUnsafe, append, prepend, before, after
  • Visibility: show, hide, toggle, css
  • Events: on, once, off, trigger, delegate
  • Traversal: find, closest, parent, children, siblings, next, prev
  • DOM manipulation: wrap, unwrap, replaceWith, detach, scrollTo
  • Form helpers: serialize, serializeString, val
  • Dimensions: rect, offset, outerWidth, outerHeight, position

Notice that html() is sanitized by default. That's the "secure by default" principle in practice you have to explicitly call htmlUnsafe() to bypass it. A small thing that prevents a whole class of XSS bugs.

The core module also ships a solid utility belt:

import { debounce, throttle, merge, uid, utils } from '@bquery/bquery/core';

const save = debounce(() => console.log('saved!'), 300);
save.cancel(); // cancelable

const id = uid('component'); // "component-xyz123"

const merged = merge({ a: 1 }, { b: 2 }); // { a: 1, b: 2 }
Enter fullscreen mode Exit fullscreen mode

Utilities include clone, pick, omit, slugify, truncate, chunk, flatten, compact, unique, randomInt, clamp, and a full suite of type guards (isString, isElement, isPromise, etc.). It's the kind of utility layer that means you actually don't need lodash.


4. Reactive Primitives Signals All the Way Down

This is where bQuery steps firmly into modern territory. The reactive module gives you fine-grained reactivity through signals the same primitive that's now baked into Angular, Solid, and Preact signals.

import { signal, computed, effect, batch, watch } from '@bquery/bquery/reactive';

const firstName = signal('John');
const lastName = signal('Doe');

// Computed values are lazy and cached
const fullName = computed(() => `${firstName.value} ${lastName.value}`);

// Effects run immediately, re-run on dependency change
effect(() => {
  document.title = fullName.value;
});

// Batch multiple updates into a single notification pass
batch(() => {
  firstName.value = 'Jane';
  lastName.value = 'Smith';
});

// Watch with old/new value comparison
const stop = watch(firstName, (newVal, oldVal) => {
  console.log(`Changed: ${oldVal} รขโ€ โ€™ ${newVal}`);
});

stop(); // unsubscribe
Enter fullscreen mode Exit fullscreen mode

A few things worth highlighting:

signal.peek() reads the value without creating a reactive dependency. Useful when you need to read inside an effect without it re-subscribing.

signal.update(fn) updates based on the current value handy for immutable patterns.

signal.dispose() removes all subscribers and prevents memory leaks. Important for long-lived apps.

readonly(signal) creates a read-only view. Great for exposing reactive state from a store without allowing external mutation.

untrack(() => ...) reads signals inside an effect without tracking them as dependencies.

persistedSignal syncs a signal to localStorage automatically, with graceful fallbacks for SSR and Safari private mode:

import { persistedSignal } from '@bquery/bquery/reactive';

const theme = persistedSignal('theme', 'light');
theme.value = 'dark'; // Saved to localStorage automatically
Enter fullscreen mode Exit fullscreen mode

linkedSignal creates a writable computed you provide both a getter and a setter, so writes can fan out to multiple underlying signals:

import { linkedSignal, signal } from '@bquery/bquery/reactive';

const first = signal('Ada');
const last = signal('Lovelace');

const fullName = linkedSignal(
  () => `${first.value} ${last.value}`,
  (next) => {
    const [nextFirst, nextLast] = next.split(' ');
    first.value = nextFirst ?? '';
    last.value = nextLast ?? '';
  }
);

fullName.value = 'Grace Hopper'; // Fans out to first and last
Enter fullscreen mode Exit fullscreen mode

Errors inside effects are caught and logged rather than crashing the reactive system subsequent updates keep working. That's a nice resilience property you don't always get for free.


5. Async Data & Fetching

Managing loading states, errors, and async lifecycles is boilerplate-heavy in vanilla JS. bQuery abstracts all of that into two composables.

useAsyncData wraps any async function in a signal-based lifecycle:

import { signal, useAsyncData } from '@bquery/bquery/reactive';

const userId = signal(1);
const user = useAsyncData(
  () => fetch(`/api/users/${userId.value}`).then(r => r.json()),
  {
    watch: [userId],        // re-run when userId changes
    defaultValue: null,
    onError: (err) => console.error('Failed:', err),
  }
);

// Reactive state you can bind directly to the DOM
console.log(user.status.value);  // 'idle' | 'pending' | 'success' | 'error'
console.log(user.pending.value); // boolean
console.log(user.data.value);    // the resolved data
console.log(user.error.value);   // Error | null

await user.refresh(); // manually trigger
user.clear();         // reset everything
user.dispose();       // stop watchers
Enter fullscreen mode Exit fullscreen mode

useFetch builds on top of that and adds HTTP niceties base URLs, query params, custom headers, automatic JSON serialization, and pluggable response parsers (json, text, blob, arrayBuffer, formData, response):

import { useFetch } from '@bquery/bquery/reactive';

const users = useFetch('/users', {
  baseUrl: 'https://api.example.com',
  query: { page: 1, include: 'profile' },
  headers: { authorization: 'Bearer my-token' },
});
Enter fullscreen mode Exit fullscreen mode

For shared defaults across multiple requests, createUseFetch acts as a factory:

import { createUseFetch } from '@bquery/bquery/reactive';

const useApi = createUseFetch({
  baseUrl: 'https://api.example.com',
  headers: { 'x-client': 'my-app' },
});

const profile = useApi('/profile');
const posts = useApi('/posts', { query: { page: 2 } });
Enter fullscreen mode Exit fullscreen mode

This pattern is really clean for larger apps where you want a pre-configured fetch instance rather than repeating base URLs everywhere.


6. Building Web Components with bQuery

The component module is where bQuery really shines for component-driven architectures. It wraps the native Custom Elements API with typed props, optional internal state, scoped reactivity, and a sanitized render function.

import { component, html, bool } from '@bquery/bquery/component';

component('user-card', {
  props: {
    username: { type: String, required: true },
    avatar: { type: String, default: '/default-avatar.png' },
    active: { type: Boolean, default: false },
  },
  state: { clicks: 0 },
  styles: `
    .card { display: grid; gap: 0.5rem; padding: 1rem; border-radius: 8px; }
    .active { border: 2px solid #4f46e5; }
  `,
  connected() {
    console.log('user-card mounted');
  },
  disconnected() {
    console.log('user-card removed');
  },
  render({ props, state, emit }) {
    return html`
      <button
        class="card ${props.active ? 'active' : ''}"
        ${bool('disabled', !props.active)}
        @click=${() => {
          this.setState('clicks', state.clicks + 1);
          emit('card-clicked', { username: props.username });
        }}
      >
        <img src="${props.avatar}" alt="${props.username}" />
        <strong>${props.username}</strong>
        <span>Clicked ${state.clicks} times</span>
      </button>
    `;
  },
});
Enter fullscreen mode Exit fullscreen mode
<!-- Usage -->
<user-card username="Jonas" active></user-card>
Enter fullscreen mode Exit fullscreen mode

A few things to appreciate here:

Props are typed and coerced automatically. Strings stay strings, numbers get Number() called on them, booleans understand 'true', '', '1', 'false', '0'. Objects get JSON.parsed. You can also add a validator function to enforce invariants at runtime.

The render output is sanitized before being written to the Shadow DOM. You get security by default with an explicit opt-in mechanism (safeHtml, trusted) when you need to pass sanitized fragments through.

Shadow DOM mode is configurable. Open shadow root by default, but you can go closed or render directly into light DOM:

component('inline-banner', {
  shadow: false, // renders in light DOM
  render: () => html`<p class="banner">No shadow needed here</p>`,
});
Enter fullscreen mode Exit fullscreen mode

Lifecycle hooks cover everything you need: beforeMount, connected, beforeUpdate (return false to cancel a re-render), updated, disconnected, onError, onAdopted, and onAttributeChanged.

Scoped reactive helpers (useSignal, useComputed, useEffect) create component-local reactive resources that are cleaned up automatically on disconnect no manual cleanup needed:

component('live-timer', {
  state: { seconds: 0 },
  connected() {
    const tick = useSignal(0);
    const interval = setInterval(() => tick.value++, 1000);

    useEffect(() => {
      this.setState('seconds', tick.value);
    });

    this.disconnected = () => clearInterval(interval);
  },
  render({ state }) {
    return html`<p>Elapsed: ${state.seconds}s</p>`;
  },
});
Enter fullscreen mode Exit fullscreen mode

External signals can drive re-renders via the signals option, keeping component updates predictable:

import { signal, computed } from '@bquery/bquery/reactive';

const theme = signal<'light' | 'dark'>('light');
const themeClass = computed(() => `theme-${theme.value}`);

component('theme-badge', {
  props: {},
  signals: { themeClass },
  render({ signals }) {
    return html`<span class="${signals.themeClass.value}">Current theme</span>`;
  },
});
Enter fullscreen mode Exit fullscreen mode

7. @bquery/ui The Default Component Library

bQuery ships a companion component library that's registered through registerDefaultComponents(). It's a small, zero-dependency set of native UI primitives no external CSS framework required.

import { defineBqueryConfig } from '@bquery/bquery/platform';
import { registerDefaultComponents } from '@bquery/bquery/component';

// Configure a custom prefix (default is 'ui')
defineBqueryConfig({
  components: { prefix: 'ui' },
  fetch: { baseUrl: 'https://api.example.com' },
  transitions: { skipOnReducedMotion: true },
});

const tags = registerDefaultComponents();

console.log(tags);
// {
//   button: 'ui-button',
//   card: 'ui-card',
//   input: 'ui-input',
//   textarea: 'ui-textarea',
//   checkbox: 'ui-checkbox'
// }
Enter fullscreen mode Exit fullscreen mode

The available primitives:

  • ui-button pill-shaped button with variant, size, type, and disabled props
  • ui-card container with optional title, footer, and elevated props
  • ui-input labeled text input that emits input events with { value }
  • ui-textarea labeled textarea, same event contract as ui-input
  • ui-checkbox labeled checkbox that emits change events with { checked }

These components use regular HTML slots and bubble custom events, so they play nicely with forms, routers, and shadow DOM boundaries. You can compose them directly in your markup:

<ui-card title="Create Account">
  <ui-input label="Name"></ui-input>
  <ui-input label="Email"></ui-input>
  <ui-checkbox label="Accept terms"></ui-checkbox>
  <ui-button variant="primary" type="submit">Sign Up</ui-button>
</ui-card>
Enter fullscreen mode Exit fullscreen mode

And wire them up reactively:

import { $, signal } from '@bquery/bquery';

const name = signal('');
const email = signal('');

document.querySelector('ui-input[label="Name"]')
  ?.addEventListener('input', (e) => {
    name.value = (e as CustomEvent<{ value: string }>).detail.value;
  });
Enter fullscreen mode Exit fullscreen mode

The prefix system via defineBqueryConfig is a nice touch for teams with strict naming conventions, or for avoiding collisions when integrating bQuery components into an existing design system.


8. The Broader Ecosystem at a Glance

bQuery v1.7.0 covers a surprising amount of ground beyond what we've walked through. Here's a quick tour of the other modules:

Router full SPA routing with constrained params, guards, redirects, and declarative <bq-link> elements.

Store signal-based global state with persistence, migrations, and action lifecycle hooks. Think a lightweight Pinia, but framework-agnostic.

View declarative bq-* attribute directives (bq-text, bq-show, bq-class, etc.) for template-style binding without a full component.

Motion transitions, FLIP animations, springs, parallax, typewriter effects, and scroll-linked animations. Respects prefers-reduced-motion by default when configured.

i18n reactive locale state, nested translation keys, pluralization rules, and Intl-based date/number/relative-time formatting.

a11y focus traps, skip navigation links, live region announcers, and audit helpers that flag missing ARIA attributes at runtime.

DnD make any element draggable, define drop zones, build sortable lists without reaching for a third-party library.

Testing renderComponent, fireEvent, waitFor utilities that mirror what you'd expect from Testing Library.

SSR renderToString for server-side HTML generation and hydrateMount for seamlessly picking up where the server left off.

Storybook helpers storyHtml() and when() for writing safe Storybook stories with boolean attribute shorthand (?disabled=${true}).


9. When Should You Reach for bQuery?

bQuery isn't trying to replace React, Vue, or Svelte for large-scale applications with complex component trees and heavy state management. It's solving a different problem.

Reach for bQuery when:

  • You want reactivity and component primitives without a build pipeline prototypes, experiments, browser extensions, internal tools
  • You're writing vanilla JS/TS and want jQuery's ergonomics plus modern signal-based reactivity
  • You need native Web Components with typed props and a sane lifecycle, but don't want to set up Lit or Stencil
  • You're building progressively enhanced pages where a CDN import is all you need
  • You want to ship accessible, secure-by-default UI without bolting on extra libraries for sanitization, focus management, and ARIA
  • You're working on a small-to-medium project where a full SPA framework would be overkill

It's also genuinely useful as a companion in larger apps you could use bQuery's reactive core alongside an existing codebase for specific interactive islands without committing to a full rewrite.


10. Conclusion

bQuery v1.7.0 is one of those rare libraries that manages to feel both nostalgic and completely modern at the same time. It channels the simplicity of jQuery while embracing everything the web platform has become signals, Web Components, Trusted Types, fetch, Shadow DOM, the whole lot.

The zero-build path alone makes it worth knowing about. Being able to drop a single CDN import into an HTML file and immediately have signals, reactive DOM manipulation, typed components, and async data composables is genuinely impressive.

If you've been eyeing signals-based reactivity but felt the existing frameworks were too opinionated or too heavyweight for your use case, bQuery is absolutely worth exploring.

Give it a spin:


Thanks for reading! If you have questions or feedback, drop them in the comments. And if you're using bQuery in a project, I'd love to hear about it.

Top comments (0)