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.
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.
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:
<sprinkle-date-picker>-style
elements to drop in. You write the HTML; it
enhances it.
<select> and use this
instead." The native element stays the source
of truth.
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.
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:
<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.
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.
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.
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">…long content…</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.
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.
Whole-page structure — sidebars, topbars, and navigation — is described in markup, not JS:
<div shell>
<aside sidebar="left"> … </aside>
<main content> … </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.
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].
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.
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.