Core API
The core module provides selectors, DOM manipulation, events, and utilities. The API mirrors jQuery’s ergonomics while staying explicit and debuggable. It is built on three building blocks:
$(selector)— wraps a single element in aBQueryElementand throws if the selector matches nothing. Use it when the absence of the element is a bug.$$(selector)— wraps any number of elements (including zero) in aBQueryCollection. Use it for lists, optional matches, or iteration.- Utilities — pure helpers (
debounce,throttle,merge,uid,template, type guards, …) exposed both as named exports and via theutilsnamespace for IntelliSense-friendly destructuring.
Mutating methods return this so they chain — with two exceptions: replaceWith() and clone() return a new wrapper around the replacement/copy, so the chain continues on the new node rather than the original. Read-only getters (text(), attr(name), css(prop), val(), etc.) return the underlying value. HTML insertion methods such as html(), append(), prepend(), before(), and after() sanitize string content by default, while setters like attr(), data(), and css() write directly; the Security guide covers how to opt in to raw HTML when you genuinely need it.
BQueryElementalways wraps exactly one element.BQueryCollectionwraps zero or more — its mutating methods apply to every element, and its getters read from the first element. Convert between them with$$(...).eq(0)orcollection.firstEl(), both of which returnBQueryElement | undefinedwhen the collection is empty.
Selectors
import { $, $$ } from '@bquery/bquery/core';
const button = $('#submit');
const items = $$('.list-item');$() is the strict variant: missing elements raise an Error with the message bQuery: element not found for selector "...", which surfaces typos and DOM-timing bugs at the call site instead of crashing later. $$() is the permissive variant: an empty collection is a valid result, and the chainable API on it becomes a safe no-op ($$('.missing').addClass('x') does nothing rather than throwing).
$ (single element)
- Accepts a selector string or an
Element. - Throws if a selector string matches no element.
const el = $('#app');
const wrap = $(document.body);$$ (collection)
- Accepts a selector string, an array of
Element, orNodeListOf<Element>. - Always returns a
BQueryCollection(empty if no matches).
const list = $$('.item');
const fromArray = $$([document.body]);BQueryElement (single element wrapper)
BQueryElement is the single-element wrapper returned by $(). Methods that change the element return this so calls can be chained fluently (except replaceWith()/clone(), which return a new wrapper — see above). Methods that read state return the underlying value: primitives for text()/attr()/css(), raw elements or arrays for traversal helpers (find(), closest(), contents()), and plain objects for measurements (offset(), position()). The underlying DOM node is always available via the .raw (alias .node) getter when you need to drop down to raw browser APIs.
Class & attribute helpers
These helpers mirror standard classList / setAttribute semantics but stay chainable. attr(name) (single argument) returns the current attribute value as a string, using '' when the attribute is missing; calling attr(name, value) sets it. prop() is the equivalent for IDL properties on the wrapped Element; in TypeScript, control-specific properties such as checked or value may require narrowing or casting the element type, or using the underlying .raw node directly. data() reads and writes data-* attributes with automatic key normalization.
addClass(...classNames)removeClass(...classNames)toggleClass(className, force?)hasClass(className)attr(name, value?)removeAttr(name)toggleAttr(name, force?)prop(name, value?)data(name, value?)
Content & HTML
text() reads or writes textContent, so any HTML in the input is rendered as plain text — this is the safest way to display user-supplied data. html(value) runs the input through the default sanitizer (see Security) before assignment, removing scripts and dangerous attributes. htmlUnsafe(value) skips sanitization and is the explicit opt-in for trusted markup. Insertion helpers (append, prepend, before, after) accept either sanitized strings or DOM nodes; passing nodes preserves listeners and references.
text(value?)html(value)– sanitized by defaulthtmlUnsafe(value)– bypasses sanitizationempty()append(content)prepend(content)before(content)after(content)
contentcan be a string (sanitized) orElement/Element[].
Style & visibility
css() is overloaded: with a single property name it acts as a getter that returns the resolved value from getComputedStyle() (always a string); with a property + value (or an object) it acts as a setter that writes to element.style and stays chainable. show() clears the inline display style (or sets the value you pass, e.g. show('flex')) and removes the hidden attribute; hide() only sets the inline display: none — it does not add hidden. toggle() decides based on the inline display value only, so an element hidden by a stylesheet rule, a class, or the hidden attribute counts as visible and gets hidden; pass a boolean force to set the state explicitly. toggle() exists on BQueryElement only — collections have show()/hide() but no toggle().
css(property)– getter: returns computed style value viagetComputedStyle()css(property, value)– setter: sets a single CSS propertycss(properties)– setter: sets multiple CSS properties from an objectshow(display?)– clears/sets inlinedisplayand removes thehiddenattributehide()– sets inlinedisplay: none(does not addhidden)toggle(force?)– flips based on the inlinedisplayvalue (BQueryElementonly)
Events (Element)
on() attaches a listener that fires for every dispatched event of that type, once() auto-removes itself after the first invocation, and off() removes a previously attached handler (the exact function reference must match). trigger() synthesises and dispatches a CustomEvent, with the optional detail payload available on event.detail. delegate() is the preferred way to react to events on dynamically inserted descendants: a single listener on the wrapper element matches the inner selector at dispatch time, so newly added children are picked up automatically.
on(event, handler)once(event, handler)off(event, handler)trigger(event, detail?)delegate(event, selector, handler)– event delegation for dynamic content
Event Delegation
Event delegation allows handling events on dynamically added elements:
// Handle clicks on .item elements, even if added later
$('#list').delegate('click', '.item', (event, target) => {
console.log('Clicked:', target.textContent);
});CSS Getter
The css() method works as a getter when called with a single property name:
// Get computed style value
const color = $('#box').css('color');
// Set styles (chainable)
$('#box').css('color', 'red');
$('#box').css({ color: 'red', 'font-size': '16px' });Selector Matching
if ($('#el').is('.active')) {
console.log('Element is active');
}
// Equivalent to
$('#el').matches('.active');Traversal & utilities
Traversal methods follow the DOM tree without mutating the receiver, but their return types mirror the underlying DOM operation. On BQueryElement, find() returns all matching descendants as Element[], findOne() returns the first match as Element | null, and helpers like closest(), parent(), next(), and prev() return raw Element | null values. On BQueryCollection, traversal helpers return new BQueryCollection wrappers so you can keep chaining across multiple matches. closest() walks up the tree (including the element itself when it matches), which is the canonical pattern for resolving the target of a delegate('click', selector, …) callback to a known ancestor.
Layout getters come in two flavours: innerWidth/innerHeight measure the content + padding box (matching clientWidth/clientHeight), while outerWidth/outerHeight measure the border box and accept an optional includeMargin boolean. rect() returns a static DOMRect snapshot from getBoundingClientRect() (it does not update as layout changes), offset() returns offsetWidth/offsetHeight plus offsetTop/offsetLeft (relative to the offset parent), and position() returns coordinates relative to the offset parent.
find(selector)findOne(selector)closest(selector)parent()children()siblings()next()prev()matches(selector)is(selector)– alias formatches()clone(deep?)– returns a newBQueryElementwrapping the copyval(newValue?)rect()offset()index()contents()offsetParent()position()innerWidth()– content + padding width (clientWidth)innerHeight()– content + padding height (clientHeight)outerWidth(includeMargin?)– border-box width, optionally with marginsouterHeight(includeMargin?)– border-box height, optionally with marginsfocus()/blur()raw(getter) /node(getter)
DOM Manipulation
wrap(wrapper)– wrap element with new parent (accepts tag name or Element)unwrap()– remove parent, keeping only this element; sibling nodes are discarded with the parent (use$$(…).unwrap()to keep all children)replaceWith(content)– replace element with new content; returns a newBQueryElementwrapping the replacementdetach()– remove element from DOM without discarding the wrapperscrollTo(options?)– scroll element into view
// Wrap element with a div
$('#content').wrap('div');
// Wrap with an existing element
const wrapper = document.createElement('section');
wrapper.className = 'wrapper';
$('#content').wrap(wrapper);
// Unwrap (remove parent — siblings inside the parent are removed too)
$('#content').unwrap();
// Replace element
$('#old').replaceWith('<div id="new">New content</div>');
// Detach and reinsert later
const item = $('#item').detach();
document.body.appendChild(item.raw);
// Smooth scroll to element
$('#section').scrollTo({ behavior: 'smooth', block: 'center' });Form Serialization
serialize() collects every named, non-disabled control inside the wrapped <form> into an object — input values, selected option(s), checked checkboxes, radio groups, multi-select arrays, and <textarea> content are all supported. The returned object has a null prototype (security hardening), so inherited members like hasOwnProperty or toString are not available on it — use Object.keys(data) or Object.prototype.hasOwnProperty.call(data, key) instead. serializeString() returns the same data already URL-encoded so it can be appended to a query string or sent as application/x-www-form-urlencoded body. Both helpers respect the standard form rules: unnamed fields, disabled controls, and type="submit" buttons are skipped, and unchecked checkboxes / radios are excluded rather than serialized as false.
serialize()– returns form data as objectserializeString()– returns URL-encoded string
// Get form data as object
const data = $('form').serialize();
// { name: 'John', email: 'john@example.com' }
// Get as query string
const query = $('form').serializeString();
// 'name=John&email=john%40example.com'BQueryCollection (multi-element wrapper)
BQueryCollection is the zero-or-more-element wrapper returned by $$() (and by collection-returning methods like find(), siblings(), children()). It flattens common operations: writing through the collection iterates every element, while reading returns the value from the first element only — matching jQuery's "first-wins" convention. For per-element access, note the callback shapes: .each() and .toArray() yield BQueryElement wrappers, while .map() / .filter() / .reduce() hand their callbacks the raw Element. Use .eq(n) to narrow down to a single BQueryElement (undefined when the index is out of range).
Mutating methods apply to every element and return this for chaining — except replaceWith(), which returns a new BQueryCollection of the replacement nodes, so further chained calls target the replacements. Getter methods return values from the first element.
Collection helpers
length(getter)eq(index)firstEl()lastEl()each(callback)– callback receives aBQueryElementmap(callback)– callback receives the rawElementfilter(predicate)– predicate receives the rawElementreduce(callback, initialValue)– callback receives the rawElementtoArray()– returnsBQueryElement[]find(selector)– query all descendant elements matching a selector across all collection elements (deduplicates shared descendants)
DOM traversal
closest(selector)– closest matching element, including the element itself when it matches (deduplicated)parent()– unique parent elementschildren()– all child elements across the collectionsiblings()– all siblings excluding collection elementsnext()– next sibling of each elementprev()– previous sibling of each element
// Navigate the DOM from a collection
$$('.item').parent().addClass('has-items');
$$('.active').siblings().removeClass('active');
$$('.current').next().addClass('upcoming');// Find all .item descendants across multiple containers
$$('.container').find('.item').addClass('highlight');DOM & class helpers
addClass(...classNames)removeClass(...classNames)toggleClass(className, force?)attr(name, value?)removeAttr(name)toggleAttr(name, force?)text(value?)html(value?)– sanitized by defaulthtmlUnsafe(value)append(content)prepend(content)before(content)after(content)wrap(wrapper)unwrap()replaceWith(content)detach()index()contents()offsetParent()position()innerWidth()– content + padding width (first element)innerHeight()– content + padding height (first element)outerWidth(includeMargin?)– border-box width (first element)outerHeight(includeMargin?)– border-box height (first element)css(property)– getter: returns computed style value (first element)css(property, value)– setter: sets a single CSS propertycss(properties)– setter: sets an object of propertiesshow(display?)hide()remove()empty()
Events
on(event, handler)once(event, handler)off(event, handler)trigger(event, detail?)delegate(event, selector, handler)– event delegation
Utilities
The core module also exposes a large set of pure, tree-shakeable utility functions — covering objects, functions, strings, numbers, arrays, type guards, and miscellaneous helpers (UID, UUID, polling, retry, …). Two access styles are supported and equivalent:
- Named imports (best for tree-shaking with a bundler):
import { debounce, merge, uid } from '@bquery/bquery/core'; - Namespace import (best for editor autocomplete and IntelliSense):
import { utils } from '@bquery/bquery/core';thenutils.debounce(...).
Function wrappers (debounce, throttle, memoize) return a callable with extra control methods such as .cancel(), .flush(), and .clear() — see each entry below for specifics. retry is not a wrapper: it invokes the function immediately and returns a Promise; cancel it via its signal option. Most utilities do not mutate their arguments — the deliberate exceptions are set(), defaults(), and freeze(), which modify (or freeze) the object you pass in and return that same reference. Deep object helpers (get, set, merge, defaults) refuse to traverse prototype keys (__proto__, constructor, prototype), preventing prototype-pollution exploits.
import { debounce, throttle, merge, uid, utils } from '@bquery/bquery/core';
const id = uid();
const merged = merge({ a: 1 }, { b: 2 });
const delayed = debounce(() => console.log('Saved'), 200);
delayed.cancel(); // Cancel pending invocation
const scrollHandler = throttle(() => console.log('Scroll'), 100);
scrollHandler.cancel(); // Reset throttle, next call executes immediately
const legacyId = utils.uid();utils is also explicitly typed as BQueryUtils, which makes namespace-style access play nicely with editor IntelliSense when you prefer utils.debounce(...) over named imports.
import { utils, type BQueryUtils } from '@bquery/bquery/core';
const typedUtils: BQueryUtils = utils;
const later = typedUtils.debounce(() => console.log('Saved'), 200);
later.cancel();Utility list
Object
clone(value)merge(...sources)pick(obj, keys)omit(obj, keys)hasOwn(obj, key)isPlainObject(value)get(obj, path, default?)/set(obj, path, value)/has(obj, path)– prototype-pollution-safe deep accessors ('a.b.c'or'list[0].name')mapValues(obj, fn)/mapKeys(obj, fn)/invert(obj)deepEqual(a, b)(aliasisEqual)freeze(obj)– deepObject.freezedefaults(target, ...sources)– fillundefinedkeysentriesTyped(obj)/keysTyped(obj)– key-preserving typed wrappers
Function
debounce(fn, delayMs, options?)–DebouncedFnwith.cancel(),.flush(). Options:{ leading?, trailing?, maxWait? }throttle(fn, intervalMs, options?)–ThrottledFnwith.cancel(),.flush(). Options:{ leading?, trailing? }once(fn)/noop()memoize(fn, keyFn?)–MemoizedFnwith.clear(),.delete(key)compose(...fns)/pipe(...fns)curry(fn)/partial(fn, ...preset)retry(fn, opts?)– exponential backoff with jitter andAbortSignal
Misc
uid(prefix?)– short hash-style iduuid()– RFC 4122 v4 (usescrypto.randomUUID()/getRandomValues()when available)isEmpty(value)/parseJson(json, fallback)/sleep(ms)tryCatch(fn)– Go-style[error, value]for sync or async functionstimes(n, fn)/pollUntil(predicate, opts?)/nextFrame()/nextTick()
Type guards
isElement/isCollection/isFunction/isString/isNumber/isBoolean/isArray/isDate/isPromise/isObjectisError/isMap/isSet/isRegExp/isSymbol/isBigIntisAsyncFunction/isIterable/isAsyncIterableisNullish(value)/isDefined(value)
Number
randomInt(min, max)/randomFloat(min, max)clamp(value, min, max)/inRange(value, min, max, inclusive?)/toNumber(value, fallback?)round(value, precision?)/roundTo(value, step)lerp(a, b, t)/inverseLerp(a, b, value)/mapRange(value, inMin, inMax, outMin, outMax)formatBytes(bytes, opts?)– decimal/binary, locale-aware viaIntl.NumberFormatsum(items)/average(items)/median(items)degToRad(deg)/radToDeg(rad)
The locale-aware
formatNumberis exposed by@bquery/bquery/i18n.utils.formatBytes()accepts the samelocaleoption for consistent localized output without pulling in i18n.
String
capitalize/toKebabCase/toCamelCase/toSnakeCase/toPascalCase/toTitleCasetruncate(str, maxLength, suffix?)/slugify(str)/escapeRegExp(str)pad(str, length, char?)/padStart(str, length, char?)/padEnd(str, length, char?)wordCount(str)/lines(str)template(str, vars)– safe${name}interpolation (noeval)stripHtml(str)– DOM-free tag removal (not a sanitizer; usesanitizeHtml()from@bquery/bquery/securityfor untrusted input)randomString(length, charset?)– crypto-backed when available
Array
ensureArray(value)/unique(items)/uniqueBy(items, fn)chunk(items, size)/chunkBy(items, predicate)/compact(items)flatten(items)/flattenDeep(items)groupBy(items, key|fn)/keyBy(items, key|fn)partition(items, pred)/zip(...arrays)range(start, end, step?)first(items)/last(items)/take(items, n)/drop(items, n)sample(items)/shuffle(items)(Fisher–Yates)sortBy(items, fn|fns)intersection(a, b)/difference(a, b)move(items, from, to)
Pitfalls and gotchas
$(selector)throws if no element matches — use$$(selector)(collection, never throws) for optional queries.- Mutating wrappers return
thisfor chaining; non-mutating getters (.text(),.attr()without args) do not. .text()/.attr()/.css()are overloaded as getter and setter — pass a value to mutate.- HTML-writing APIs (
.html(value)) sanitize untrusted content by default; use.raw.innerHTMLonly for trusted strings. - Utility helpers like
deepEqual,clone, andfreezeoperate by structure.clone()delegates tostructuredClone(), which throws on functions and symbols rather than skipping them (the legacy JSON fallback silently drops them).
Performance notes
- Cache wrappers when re-using them in tight loops — each
$()call performs aquerySelector. - Prefer
.find()/.children()over re-querying fromdocumentto keep selector scope narrow. - For very large lists, split the work into batches with
chunk()from@bquery/bquery/coreand yield between batches (nextFrame()/nextTick()). All array helpers return materialized arrays — there are no streaming or generator variants.
Testing this module
- DOM tests use
happy-domviatests/setup.ts. Inline DOM creation, assertion, then.remove()is the preferred pattern (seetests/core.test.ts). - Utility helpers are pure functions — assert with
bun:test'sexpect.
Related modules
- Reactive — combine signals with DOM updates.
- View — declarative bindings on top of the core wrappers.
- Security — sanitizer used by
.html()and related APIs.
Version history
- 1.16.0 —
undelegate()works across wrapper instances (module-level delegation registry, one counted listener perdelegate()target);wrap()clone correctness over multi-element collections; cheaperreplaceWith(string),empty(),children()/siblings()/index(),unwrap(),css(object), form serialization, anddata(). - 1.13.0 — major
core/utilsexpansion (array / function / object / string / number / misc helpers, additional type guards). - 1.0.0 — original chainable jQuery-inspired wrappers shipped.