oklch-pickerPlaygroundGitHub

Server rendering

Every adapter renders on a server. The markup it sends is the finished picker, sliders and chart and value, rather than an empty shell that fills in once JavaScript arrives.

That matters because the chart is the expensive part. A picker that renders blank and paints on hydration flashes on every page load, which is most of the reason to server-render it at all.

Nothing to configure

There is no server entry point and no client:only to remember. The colour maths is framework-free and touches no DOM, so it runs the same on a server as in a browser, and each adapter renders through its own framework's usual server renderer.

import { renderToString } from "react-dom/server";
import { ColourPicker } from "@oklch-picker/react";

// Nothing special. The picker is a component like any other.
renderToString(<ColourPicker value={colour} onChange={setColour} />);
import { renderToString } from "vue/server-renderer";
import { ColourPicker } from "@oklch-picker/vue";

await renderToString(createSSRApp({ /* ... */ }));
import { render } from "svelte/server";
import { ColourPicker } from "@oklch-picker/svelte";

const { body } = render(ColourPicker, { props: { value: colour } });
import { renderToString } from "solid-js/web";
import { ColourPicker } from "@oklch-picker/solid";

renderToString(() => <ColourPicker value={colour()} onChange={setColour} />);
import { renderApplication } from "@angular/platform-server";

// Angular renders on a server through its own platform, as any
// standalone component does. The picker needs nothing extra.
await renderApplication(bootstrap, { document });
import { renderToString } from "@builder.io/qwik/server";
import { ColourPicker } from "@oklch-picker/qwik";

// Resumability is the point: the server sends the finished picker and
// the client resumes it rather than re-running the component.
await renderToString(<Example />);

The custom element is different

<oklch-picker> is a custom element, so it upgrades in the browser rather than being rendered to HTML on a server. Server-render the tag and the element fills it in once its module loads:

---
import "@oklch-picker/core/styles.css";
---

<oklch-picker value="oklch(0.7 0.15 255)"></oklch-picker>

<script>
  // A <script> block, so Astro bundles this for the browser. The element
  // needs a DOM to upgrade into.
  import "oklch-picker/register";
</script>
"use client";
import { useEffect } from "react";

export function Picker() {
  // The import runs in the browser, where customElements exists.
  useEffect(() => {
    import("oklch-picker/register");
  }, []);
  return <oklch-picker value="oklch(0.7 0.15 255)" />;
}
<script setup>
import { onMounted } from "vue";

onMounted(() => import("oklch-picker/register"));
</script>

<template>
  <oklch-picker value="oklch(0.7 0.15 255)" />
</template>

Importing oklch-picker/register on a server is safe: the module loads and register() returns without doing anything, since there is nothing to upgrade. Earlier versions threwHTMLElement is not defined and took the render down with them, so the client-only import above was mandatory rather than merely tidy.

Template languages were never affected

Rails, Laravel, Django, PHP and WordPress serve the tag as text and never import the module on the server, so none of the above applies. Write the tag, ship the script, done.

Hydration

What the server sends has to match what the client renders first, or the framework throws the markup away and renders again, costing exactly the flash the server render was meant to avoid.

The picker holds to that. pickerModel() is a pure function of the current colour, so two renders of the same value are identical: the same chart path, the same gradient stops, the same crosshair position. Nothing is randomised, and the draft state that only exists after a drag resolves to the stored value on both sides.

The one thing that has to be unique per picker is the SVG gradient id, since those share a document-wide namespace. Each adapter takes it from its own framework's id hook, which is stable across the server render and the hydration that follows.

What is tested

Every adapter has a server-rendering suite running in a real Node environment with no DOM, asserting the markup is the finished picker rather than an empty shell. That part is a tested promise rather than a hope.

Hydration itself is covered less evenly, because the frameworks differ in what they will tell you. Vue and Svelte hydrate over real server markup and genuinely detect a mismatch, which the suite checks by feeding them markup that is wrong on purpose. React hydrates too, but Preact neither warns on a mismatched value nor repairs it, so that test records the behaviour instead of asserting a guarantee it cannot make. Solid matches server nodes by hydration keys that only line up inside a real page, so its suite compares the server and client trees rather than calling hydrate(). Angular renders through its own server platform, which its suite covers, and its hydration is Angular's to verify rather than ours.

What holds across every adapter is the property underneath:pickerModel() is pure, so the server and the client compute the same picker from the same value. That is what the trees and the determinism tests pin down. A browser is the honest place to verify the rest, the same way form association is.