ForgeSprinkle

A tiny library of high-power behaviors for HTML that your app can add to any page. Write normal HTML, add one attribute, and a native element quietly gains polish — without a framework, without replacing your markup, and without breaking when JavaScript doesn't load.

Overview

ForgeSprinkle ships as the ForgeSprinkle capability, but at its core it's a standalone, dependency-free script (sprinkle.js) plus a stylesheet (sprinkle.css). You keep writing ordinary HTML — a <select>, a <details>, a file <input>, a <dialog> — and enhance it with a single attribute:

<!-- a native select becomes a searchable combobox -->
<select name="team" combo-box searchable>
    <option value="a">Alpha</option>
    <option value="b">Beta</option>
</select>

<!-- a plain textarea grows as you type -->
<textarea name="bio" autosize></textarea>

<!-- a button copies a value to the clipboard -->
<button copy="#email">Copy</button>

Everything is progressive: the element you write is the element that ships. The attribute only adds behavior on top.

What it gives you

  • Attribute-driven behavior, no JS to write
  • Native HTML preserved underneath
  • Degrades gracefully when JS fails
  • Zero dependencies, no build step
  • Tiny footprint (~47KB minified total)
  • Auto-injected by the capability

Not a Framework

It's worth being precise about what ForgeSprinkle is not, because that's exactly what makes it different from a UI library or a frontend framework:

  • Not a UI component library — it doesn't hand you prebuilt <sprinkle-date-picker>-style elements to drop in. You write the HTML; it enhances it.
  • Not a replacement for HTML — it never tells you "stop using <select> and use this instead." The native element stays the source of truth.
  • Not a framework — no runtime to learn, no component model, no build, no JSX. It's a few dozen behaviors keyed off plain attributes.

Where a framework owns the page and renders it, ForgeSprinkle sits quietly on top of markup you already wrote — the web platform stays in charge.

Progressive Enhancement

The core promise is that the un-enhanced HTML is always functional on its own. Every directive builds on a native element that already works — and the enhancement is a layer on top:

  • Native first. A <select combo-box> is still a real, submitting <select>. A <dialog> is still a native dialog. A <details> accordion is still a native disclosure widget.
  • Enhancement is additive. Attributes map to single behaviors — autosize, copy, character-count, zoom, switch — each of which could be removed without losing the element's meaning.
  • Degrade gracefully. If JavaScript fails to load, errors, or is disabled, the page keeps working with the browser's default behavior. A robust app that happens to feel more polished when the script runs.

The library relies on modern browser features it detects and tolerates — MutationObserver, IntersectionObserver, the native <dialog> element, and CSS like @starting-style — rather than shipping a bulky fallback runtime.

Installation

php forge.php package:install-module --module=ForgeSprinkle

Installing links the module's assets into your app's public directory. From then on, the capability adds its stylesheet to the <head> and its deferred script before </body> on your HTML responses automatically — no per-page wiring needed.

Non-HTML responses (JSON, plain text) are left untouched, and the assets are injected only once per page. Drop the attribute on any element and it starts working; nothing gets rewritten in your markup.

Form Fields

A large group of directives upgrades native inputs and textareas without changing what they are. A few examples:

<!-- auto-grow as you type -->
<textarea autosize></textarea>

<!-- show a live "n / max" counter -->
<input maxlength="20" character-count />

<!-- leading icon + password visibility toggle on click -->
<input type="password" leading="lock" suffix="eye" />

<!-- a real checkbox, styled as a switch -->
<input type="checkbox" switch />

<!-- select-all on focus; submit on Ctrl/Cmd+Enter -->
<input auto-select />
<textarea enter-submit></textarea>

<!-- clamp text to 3 lines with a Show more toggle -->
<div truncate="3">&hellip;long content&hellip;</div>

File uploads, URL/phone inputs, and one-time codes get the same treatment:

<!-- drag-and-drop upload (still a native file input) -->
<label drop-zone>
    <span>Drop files or click to browse</span>
    <input type="file" name="docs" multiple />
</label>

<!-- auto url prefix, masked phone, single-char-advance OTP -->
<input type="url" prefix="www.example" />
<input type="tel" mask />                          <!-- (000) 000-0000 -->
<input type="tel" max="6" otp />                    <!-- 6 bit boxes -->

Each still submits as the native input it is: a checkable checkbox, a real <input type="file">, a single hidden-ish text field that feeds a segmented OTP display.

Validation

Inline messages are driven by the error-message attribute and the browser's own validity state — so a field's constraints stay native:

<form enhance>
    <input name="email" type="email" required
           error-message="A valid email is required."
           error-message-type="That address doesn't look right."
           allowed-domains="yourco.com" />
    <button type="submit">Subscribe</button>
</form>

Per-state messages let you customize required, type, pattern, minlength, maxlength, min, max, and step failures. allowed-domains restricts email or URL hosts; no-past, no-future, disable-days, and business-hours add smart constraints to date and time inputs.

A form marked enhance opts into richer behavior — validating before submit, showing inline and form-level messages, disabling its buttons while pending, and posting with fetch while keeping JSON errors readable. Crucially, it's the one directive you opt into: unenhanced, a form with plain error-message attributes still submits with the browser's normal native validation.

Dialogs & Menus

Drawers, modals, dropdowns, and pickers build on native building blocks — <dialog> and <details> — rather than invented overlays:

<!-- a centered modal; buttons drive it with command-for -->
<dialog modal close-outside id="confirm">
    <p>Delete this item?</p>
    <button command-for="confirm" command="close">Cancel</button>
</dialog>
<button command-for="confirm" command="show-modal">Delete</button>

<!-- a slide-in drawer from the right -->
<dialog drawer="right" close-outside id="nav"> &hellip; </dialog>

<!-- a keyboard-navigable dropdown (native details/summary) -->
<details dropdown>
    <summary>Actions</summary>
    <a href="#">Edit</a>
    <a href="#">Duplicate</a>
</details>

The command-for / command pair drives native dialogs with show-modal and close, a small layer that works even where the compact `commandfor` spelling isn't supported yet. Date-picker (date-input) and range pickers (date-range) likewise sit on top of native date inputs.

Layout & Nav

Whole-page structure — sidebars, topbars, and navigation — is described in markup, not JS:

<div shell>
    <aside sidebar="left"> &hellip; </aside>
    <main content> &hellip; </main>
</div>

<ul nav>
    <li><a href="/">Home</a></li>
    <li><a href="/billing" active>Billing</a></li>
</ul>

<button theme-toggle></button>

shell lays out left/right/top/bottom sidebars around a content region with CSS grid and adds a sidebar toggle on narrow screens. The nav directive turns an <a active> into the page's aria-current link and keeps nav-groups mutually exclusive. theme-toggle flips between light and dark, honoring the system preference and remembering the choice.

UX Polish

Smaller touches round out the set, each additive to an element that already works:

  • [sticky] — smart sticky behavior with a stuck-state class and attribute.
  • [copy] — clipboard copy with "Copied" feedback and a live region for screen readers.
  • img[zoomable] — click an image to view it enlarged in an overlay.
  • details[accordion] — animated accordions, optionally grouped to open exclusively.
  • [tooltip] — CSS tooltips that flip position to stay on screen (and expose a role="tooltip").
  • fieldset[card] — a clickable card that triggers an inner .card-link.
  • [count-up] — animate a number (or a <progress>) when it scrolls into view.
  • form[confirm-leave] — warn before leaving a form with unsaved changes.
  • button[loading] — disable and mark a submit button while its form is processing.

Accessibility is treated as part of the UX, not an afterthought: live regions on counters and drop zones, expanded states on truncates and accordions, roles and checked states on switches, and the full WAI-ARIA combobox pattern on [combo-box].

Extending

A directive is just a selector plus a function. Register your own against the global ForgeSprinkle namespace — the handler runs on matching elements at startup and on any elements added to the page afterwards:

ForgeSprinkle.register('my-attr', function (el) {
    el.classList.add('is-enhanced')
})

Register before the document finishes loading so the library can index your attribute and pick up both existing and dynamically inserted elements. The built-ins are written the same way, which keeps the whole thing small and understandable.

Configuration

There's almost nothing to set up. Icons referenced by name (for example leading="search") are looked up in /assets/svg/{name}.svg, which you can point elsewhere with a single meta tag:

<meta name="sprinkle-svg-path" content="/custom/path/to/icons">

Beyond that, the capability injects its assets on your HTML responses for you. Want the raw library in your own frontend build instead of the injected tags? The files ship in the module's asset folder so you can link them directly and skip the automatic injection.