Documentation / DOM

DOM

A jQuery-shaped $, typed. Around 35 chainable methods across traversal, classes, attributes, content, style, and events. Import from the root or from koyojs/DOM.

// Root import
import { $, ready, parseHTML, isElement } from "koyojs";

// Per-module import
import { $, ready } from "koyojs/DOM";

Every entry point needs a document. In Node, Bun, or an edge runtime they throw KoyoDOMError rather than failing with a bare ReferenceError, so a server-rendered path can catch and skip the DOM work instead of crashing the render.

$$(target: Selectable, context?: Selectable): KoyoCollection

Selects, wraps, or creates elements and returns a chainable collection. A string starting with `<` and ending with `>` is parsed as HTML; anything else is treated as a CSS selector. Also accepts an Element, Document, NodeList, array, or another collection. Passing `null` or `undefined` gives an empty collection rather than throwing, so optional lookups chain without guards. Requires a document — in Node, Bun, or an edge runtime it throws `KoyoDOMError` instead of failing with a bare `ReferenceError`.

ParameterTypeDescription
targetSelectableCSS selector, HTML string, Element, Document, NodeList, array, or collection.
contextSelectableOptional root to scope the search to.
returnsKoyoCollection
import { $ } from "koyojs/DOM";

// CSS selector
$("li.done").addClass("struck").css("opacity", 0.5);

// HTML string → a new, detached element
$("#log").append($("<p class='note'>saved</p>"));

// An existing node
$(event.target).closest("tr").remove();

// Scoped to a context
$(".cell", row).text("");

// null / undefined give an empty collection — no guard needed
$(document.querySelector("#maybe-missing")).addClass("active");  // no throw

// length tells you what matched
if ($(".error").length > 0) showBanner();

KoyoCollection

The chainable result of $(). Methods that read return a value from the first element; methods that write apply to every element and return the collection, so calls chain.

Traversal

Every traversal method returns a new collection — the original is never mutated, so you can branch off a base selection freely.

MethodReturnsDescription
lengthnumberHow many elements the collection holds.
toArray()Element[]The matched elements as a plain array.
get(index)Element | undefinedThe element at an index, or undefined.
each(fn)thisRuns fn per element. Returning false stops iteration, as in jQuery.
filter(selectorOrFn)KoyoCollectionKeeps elements matching a selector or predicate.
is(selector)booleanTrue when any element matches the selector.
find(selector)KoyoCollectionDescendants matching the selector.
closest(selector)KoyoCollectionNearest ancestor (or self) matching the selector.
parent()KoyoCollectionDirect parents.
children(selector?)KoyoCollectionDirect children, optionally filtered.
siblings(selector?)KoyoCollectionSiblings, optionally filtered.
add(target)KoyoCollectionUnion with another selection.
eq(index)KoyoCollectionA single-element collection at an index.
first()KoyoCollectionFirst element as a collection.
last()KoyoCollectionLast element as a collection.
import { $ } from "koyojs/DOM";

// Walk up from an event target and back down
$("#table").on("click", ".delete", function () {
  $(this).closest("tr").remove();
});

// Narrow a selection without re-querying the document
const rows = $("#table tr");
rows.filter(".selected").addClass("highlight");
rows.filter((el, i) => i % 2 === 0).addClass("even");

// each stops early when the callback returns false
$(".item").each((el, i) => {
  if (i >= 10) return false;
  el.setAttribute("data-index", String(i));
});

$(".tab").eq(2).addClass("active");
$(".field").first().is(":focus");

Classes

Thin wrappers over classList, applied across every element in the collection.

MethodReturnsDescription
addClass(...names)thisAdds one or more classes to every element.
removeClass(...names)thisRemoves one or more classes from every element.
toggleClass(name, force?)thisToggles a class, or forces it on/off with the second argument.
hasClass(name)booleanTrue when any element carries the class.
import { $ } from "koyojs/DOM";

$(".card").addClass("loaded", "visible");
$(".card").removeClass("skeleton");

// force the state instead of flipping it
$("#menu").toggleClass("open", isOpen);

if ($("#form").hasClass("dirty")) confirmLeave();

Attributes & properties

Getters read from the first element; setters write to every element. That split is what lets the same name serve both roles without an options object.

MethodReturnsDescription
attr(name)string | nullReads an attribute from the first element.
attr(name, value)thisSets an attribute on every element. null removes it.
attr(values)thisSets several attributes at once from an object.
removeAttr(...names)thisRemoves attributes from every element.
prop(name, value)thisSets a DOM property (checked, disabled, …) rather than an attribute.
data(key)string | undefinedReads a data-* attribute from the first element.
data(key, value)thisWrites a data-* attribute on every element.
val()stringReads the value of the first form control.
val(value)thisSets the value of every form control.
import { $ } from "koyojs/DOM";

$("#avatar").attr("src");                     // read from the first
$("img.lazy").attr("loading", "lazy");        // write to all
$("#link").attr({ href: "/docs", title: "Docs" });
$(".temp").removeAttr("data-temp", "hidden");

// prop for live DOM state, attr for markup
$("#agree").prop("checked", true);
$("#submit").prop("disabled", false);

$("#row").data("id");                          // "42"
$("#row").data("state", "editing");

const email = $("#email").val();
$("#search").val("");

Content & insertion

A plain string is treated as text and only parsed as markup when it looks like HTML, so `.append("5 < 6")` can never inject an element. With several targets, all but the last receive clones — a node can only live in one place, and that is what makes `$(".row").append(icon)` mean anything.

MethodReturnsDescription
text()stringText content of the first element.
text(value)thisSets text content on every element.
html()stringinnerHTML of the first element.
html(value)thisSets innerHTML on every element.
append(content)thisInserts as the last child.
prepend(content)thisInserts as the first child.
before(content)thisInserts as the previous sibling.
after(content)thisInserts as the next sibling.
remove()thisDetaches every element from the document.
empty()thisRemoves all children.
clone(deep?)KoyoCollectionCopies the elements. Deep by default.
import { $ } from "koyojs/DOM";

$("#title").text();                    // read
$("#title").text("Dashboard");         // write

// A plain string is text, never markup — this is safe with user input
$("#out").append(userSuppliedString);
$("#out").append("5 < 6");             // renders literally, injects nothing

// Markup only when it looks like HTML
$("#list").append("<li>New item</li>");

// Several targets: all but the last get clones
$(".row").append($("<span class='icon'>★</span>"));

$(".stale").remove();
$("#container").empty();

const template = $("#row-template").clone();

Style & visibility

`.css()` takes numbers and appends px, except on unitless properties (opacity, zIndex, lineHeight, flexGrow, …) and custom `--properties`. `.hide()` remembers the inline display it replaced so `.show()` restores it, falling back to block when a stylesheet is what hides the element.

MethodReturnsDescription
css(property)stringComputed value of a property on the first element.
css(property, value)thisSets one property on every element.
css(properties)thisSets several properties at once.
hide()thisHides elements, remembering the display value replaced.
show()thisRestores the remembered display, or falls back to block.
toggle(force?)thisFlips visibility, or forces it with the argument.
import { $ } from "koyojs/DOM";

$("#box").css("width");              // computed, from the first element
$("#box").css("width", 240);         // → "240px"
$("#box").css("opacity", 0.5);       // unitless — stays "0.5"
$("#box").css("--brand", "#71A6D1"); // custom property, no px

$(".panel").css({
  marginTop: 16,        // → "16px"
  zIndex: 10,           // unitless
  background: "white",
});

$("#modal").hide();
$("#modal").show();                  // restores what hide() replaced
$("#sidebar").toggle(isExpanded);

Events

on/once/off are overloaded on HTMLElementEventMap, so `on("click", e => …)` gives a MouseEvent rather than a bare Event. Listener bookkeeping lives in a WeakMap keyed by element, so `.off()` works from a different collection object than the one `.on()` was called on.

MethodReturnsDescription
on(type, handler, options?)thisBinds a handler to every element.
on(type, selector, handler, options?)thisDelegated: binds once on the root, fires for matching descendants.
once(type, handler, options?)thisBinds a handler that runs at most once.
once(type, selector, handler, options?)thisDelegated one-shot handler.
off()thisRemoves every handler bound through Koyo.
off(type, handler?)thisRemoves handlers for a type.
off(type, selector, handler?)thisRemoves delegated handlers.
trigger(type, detail?, options?)thisDispatches an event, with optional CustomEvent detail.
import { $ } from "koyojs/DOM";

// Typed events — e is a MouseEvent here, not a bare Event
$("#save").on("click", (e) => {
  e.preventDefault();
  save();
});

// Delegation: bound once on the root, fires for descendants added later.
// Inside the handler, `this` is the matched descendant.
$("#table").on("click", ".row", function (e) {
  $(this).toggleClass("selected");
});

// once() with a selector is implemented manually rather than with the
// native `once` option, which a first non-matching event would spend.
$(document).once("keydown", ".shortcut", handleShortcut);

// off() works from a different collection object than on() used
$("#table").off("click", ".row");
$("#widget").off();

$("#form").trigger("submit");
$("#cart").trigger("cart:updated", { items: 3 });

readyready(fn: () => void): void

Runs `fn` once the document has been parsed. Calls it synchronously when the document is already parsed, so a late registration is never dropped. Waits for `DOMContentLoaded`, not `load`, so it does not block on images and stylesheets.

ParameterTypeDescription
fn() => voidCallback to run when the document is ready.
returnsvoid
import { ready, $ } from "koyojs/DOM";

ready(() => {
  $("#app").text("ready");
  $("#menu").on("click", ".item", handleSelect);
});

// Registering after the document already parsed still runs — immediately
setTimeout(() => ready(() => console.log("not dropped")), 5000);

parseHTMLparseHTML(html: string): Element[]

Parses an HTML string into detached elements. Uses a `<template>`, so fragments that are invalid outside their parent — a bare `<tr>`, `<td>`, or `<option>` — survive instead of being stripped, which is what happens with `innerHTML` on a `<div>`. Text and comment nodes at the top level are dropped; only elements are returned, fully detached with a null `parentNode`.

ParameterTypeDescription
htmlstringThe markup to parse.
returnsElement[]
import { parseHTML, $ } from "koyojs/DOM";

parseHTML("<li>a</li><li>b</li>");    // [li, li]

// Table fragments survive — innerHTML on a div would strip these
const rows = parseHTML("<tr><td>1</td></tr>");
$("#table tbody").append(rows);

// Options too
const opts = parseHTML("<option value='1'>One</option>");
$("#select").append(opts);

// Elements come back detached
parseHTML("<p>x</p>")[0].parentNode;  // null

isElementisElement(value: unknown): value is Element

Duck-typed element check using `nodeType === 1`. `value instanceof Element` is false for a node from another realm — an iframe, a worker, or a second copy of the DOM shim in tests — so the nodeType check is used instead.

ParameterTypeDescription
valueunknownThe value to test.
returnsvalue is Element
import { isElement } from "koyojs/DOM";

isElement(document.body);           // true
isElement("<p>not a node</p>");     // false
isElement(null);                    // false

// Correct across realms, where instanceof is not
const node = iframe.contentDocument.body;
node instanceof Element;            // false — different realm
isElement(node);                    // true

function mount(target: unknown) {
  if (!isElement(target)) throw new TypeError("mount needs an element");
  target.append(view);              // narrowed to Element
}