Lit, Alpine and HTMX
None of these needs an adapter. <oklch-picker> is a custom element, so it already works in all three. What follows is how to wire it to each one's state.
Install the no-framework package and register the tag once, anywhere in your bundle:
npm install oklch-pickerimport "oklch-picker/register";
import "@oklch-picker/core/styles.css";Lit
Lit binds properties with a leading dot, which is exactly what this element wants. gamut, presets and parts are objects and arrays, so they can only ever be properties. An attribute would stringify them.
import { LitElement, html } from "lit";
import { P3 } from "@oklch-picker/core/gamuts";
import "oklch-picker/register";
class ColourField extends LitElement {
static properties = { colour: { state: true } };
constructor() {
super();
this.colour = "oklch(0.7 0.15 255)";
}
// Render into the light DOM. The stylesheet is a document-level sheet, so a
// shadow root would leave the picker unstyled.
createRenderRoot() {
return this;
}
render() {
return html`
<oklch-picker
.value=${this.colour}
.gamut=${P3}
.presets=${["oklch(0.75 0.16 145)"]}
@change=${(e) => (this.colour = e.detail.colour)}
></oklch-picker>
`;
}
}
customElements.define("colour-field", ColourField);Assign reactive state in the constructor rather than as a class field. A native class field overwrites the accessor Lit installs to detect changes, and updates stop firing with no error to explain it. Lit warns about this in the console, which is easy to miss.
Types come with the package. Importing either oklch-picker oroklch-picker/register teaches TypeScript the tag, sopicker.value is a string and event.detail.colourresolves in a listener.
Alpine
Alpine reads and writes the DOM directly, so the element's property and itschange event are all it needs. There is no adapter layer to sit between them.
<div x-data="{ colour: 'oklch(0.7 0.15 255)' }">
<oklch-picker
x-effect="$el.value = colour"
@change="colour = $event.detail.colour"
></oklch-picker>
<p x-text="colour"></p>
</div>x-effect rather than x-model. That is not a style preference: x-model recognises built-in form controls, and on a custom element it sets the initial value and then never updates. The binding looks right and silently stops tracking after the first drag. Setting$el.value in an effect keeps the picker in step with Alpine's state, and the change handler carries the colour back.
For objects, bind the property in x-init, since an attribute would stringify it:
<oklch-picker
x-init="$el.gamut = P3; $el.parts = { hexInput: true }"
@change="colour = $event.detail.colour"
></oklch-picker>HTMX
HTMX posts forms, so what matters is that the picker submits like a field. Give it a name and it does: the element is form-associated, so its value is included in the submission with no hidden input to keep in sync.
<form hx-post="/theme" hx-trigger="change from:#brand">
<oklch-picker id="brand" name="brand" value="oklch(0.7 0.15 255)"></oklch-picker>
</form>The request carries brand=oklch(0.7 0.15 255), the same canonical string every adapter emits. A form reset restores the value the server rendered, as a built-in input would.
hx-trigger="change from:#brand" fires on every commit, which for a colour picker means every slider release. If that is more traffic than you want, add delay:500ms to coalesce a burst of adjustments into one request.
Why no packages
Each of these was considered as an adapter and none earned one. Lit's property binding is already the thing a wrapper would add, and Alpine and HTMX work at the DOM level where the element already lives. A package would add a version to keep in lockstep and a second API to learn, in exchange for syntax you can write today.
The no-framework examples cover the element itself in more detail: every property, every event, and the form association these recipes lean on.