<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Chroma Panel - React Color Picker]]></title><description><![CDATA[Learn how to use Chroma Panel - React Color Picker in your React projects. Explore simple tutorials, practical examples, new features, and tips for working with colors.]]></description><link>https://chroma-panel.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ab0f13c1a95a677dadec704/c0147354-65e1-4c56-9cea-8c980826992b.png</url><title>Chroma Panel - React Color Picker</title><link>https://chroma-panel.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 05 Oct 2026 14:41:58 GMT</lastBuildDate><atom:link href="https://chroma-panel.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why Fabric.js `loadFromJSON` Can Leave Your Editor Half-Loaded]]></title><description><![CDATA[A saved Fabric.js document can reopen with missing content when an image URL fails and the loading path allows failed objects to be omitted. fabricjs-document-engine checks external images before load]]></description><link>https://chroma-panel.hashnode.dev/fabricjs-loadfromjson-failed-image</link><guid isPermaLink="true">https://chroma-panel.hashnode.dev/fabricjs-loadfromjson-failed-image</guid><category><![CDATA[fabricjs]]></category><category><![CDATA[React]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[debugging]]></category><dc:creator><![CDATA[Sohail Khan]]></dc:creator><pubDate>Thu, 01 Oct 2026 06:46:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ab0f13c1a95a677dadec704/46232d6b-aee1-4aa8-9e9d-d264c52a7b74.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A saved Fabric.js document can reopen with missing content when an image URL fails and the loading path allows failed objects to be omitted. <a href="https://www.npmjs.com/package/fabricjs-document-engine">fabricjs-document-engine</a> checks external images before loading content into the canvas and rejects unresolved images with <code>MISSING_ASSETS</code>, including the affected URLs and object IDs.</p>
<p>The practical goal is simple: keep the current drawing visible, explain why the next document could not open, and let the user fix the missing asset.</p>
<p>This guide uses React and the public API in <strong>fabricjs-document-engine 1.0.2</strong>, checked on <strong>October 1, 2026</strong>. The native Fabric.js behavior discussed below is version-specific, with <strong>Fabric.js 7.4.0</strong> as the concrete example. The engine supports Fabric.js <strong>6 and 7</strong>. The <a href="https://fabricjs-document-engine.jscrate.dev/docs/overview/quick-start">quick-start guide</a> covers installation and engine setup.</p>
<h2>Does a failed image always leave Fabric.js partially loaded?</h2>
<p>No. A failed image is not a universal explanation for every blank or incomplete canvas, and Fabric.js versions do not all handle failed object creation identically.</p>
<p>Fabric.js 7.4.0 creates objects before <code>loadFromJSON</code> clears and replaces the canvas. Its object-enlivening code collects failed object results and calls the reviver with the error. If the reviver supplies no replacement and throws no error, a failed object can be omitted while successful objects are installed.</p>
<p>That can produce a document which looks partly restored: text and shapes appear, but an image is gone. By contrast, Fabric.js 6 uses a different object-enlivening failure path; do not assume a Fabric 7 example describes every Fabric 6 failure.</p>
<p>The relevant primary sources are Fabric.js's <a href="https://www.fabricjs.com/api/classes/staticcanvas/#loadfromjson">loadFromJSON API</a>, <a href="https://github.com/fabricjs/fabric.js/blob/ce64f450bad811750cb5a75aa749fc1502c644be/src/canvas/StaticCanvas.ts">canvas implementation</a>, and <a href="https://github.com/fabricjs/fabric.js/blob/ce64f450bad811750cb5a75aa749fc1502c644be/src/util/misc/objectEnlive.ts">object-enlivening implementation</a>. These two source links are pinned to the commit recorded for Fabric.js 7.4.0. Compare that implementation with the version installed in your app.</p>
<p>The document engine adds two relevant checks:</p>
<ul>
<li><p><strong>Check external assets.</strong> Fail with a missing-image report before invoking the canvas loader.</p>
</li>
<li><p><strong>Refuse dropped objects.</strong> Use a strict reviver when Fabric.js creates document objects.</p>
</li>
</ul>
<p>The package's <a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/src/fabric/fabric-adapter.ts">Fabric adapter source</a> shows the strict reviver. Its <a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/src/assets/asset-pipeline.ts">asset preparation source</a> shows where missing-image checks run.</p>
<h2>Why can a valid canvas JSON file still fail to reopen?</h2>
<p>Valid JSON describes the document. It does not guarantee that every dependency still exists.</p>
<p>An image object may contain a <code>src</code> pointing to a deleted file, an expired signed URL, an unavailable host, or a <code>blob:</code> URL from a previous browser session. The JSON can parse successfully while the browser cannot load that image.</p>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>What to check</th>
</tr>
</thead>
<tbody><tr>
<td>Shapes load but a picture disappears</td>
<td>Failed image request; omitted object in the loading path</td>
</tr>
<tr>
<td>A document works until refresh</td>
<td>A tab-only <code>blob:</code> image was saved without durable upload</td>
</tr>
<tr>
<td>An image displays but PNG export fails</td>
<td>Canvas tainting and image CORS settings</td>
</tr>
<tr>
<td>Text uses the wrong face or layout</td>
<td>Font availability before text creation</td>
</tr>
<tr>
<td>An older document replaces a newer selection</td>
<td>Overlapping requests and loads</td>
</tr>
<tr>
<td>Objects appear only after clicking the canvas</td>
<td>Rendering after the asynchronous load</td>
</tr>
</tbody></table>
<p>A missing image and a tainted canvas need different fixes. A missing image cannot be loaded. A tainted canvas may display an image while the browser blocks reading the canvas pixels for export.</p>
<p>The package's <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts">images and fonts guide</a> separates asset loading, font fallback, and CORS/export problems. The <a href="https://fabricjs-document-engine.jscrate.dev/docs/production/troubleshooting">troubleshooting guide</a> covers related editor symptoms.</p>
<h2>Why does clearing the canvas before loading make failures worse?</h2>
<p>This code discards the current drawing before the next document has opened:</p>
<pre><code class="language-ts">// Fragile app code: the old drawing is removed before the load succeeds.
canvas.clear();
await canvas.loadFromJSON(savedJson);
canvas.requestRenderAll();
</code></pre>
<p>If loading fails, the app has already removed the old objects. Catching the error afterward does not reconstruct those objects.</p>
<p>Do not clear the canvas before asking the engine to open a document. Let the engine validate and prepare the incoming content first:</p>
<pre><code class="language-ts">await engine.loadDocument(savedDocument);
</code></pre>
<p>If a storage adapter is configured, open a saved record by its document ID:</p>
<pre><code class="language-ts">await engine.load("design-42");
</code></pre>
<p><code>loadDocument</code> accepts a document value. <code>load(id)</code> first asks the storage adapter for that value. Both use the engine's document-loading path.</p>
<p>See the <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-and-load">save and load guide</a> and <a href="https://fabricjs-document-engine.jscrate.dev/docs/api/document-engine">document engine API</a> for the method signatures.</p>
<h2>What happens before the engine mutates the canvas content?</h2>
<p>The engine prepares the incoming document before passing it to Fabric.js. With image checks enabled, the loading path does the following:</p>
<ol>
<li><p><strong>Check the input.</strong> Apply content limits, migrate supported formats, and validate the document.</p>
</li>
<li><p><strong>Check object types.</strong> Refuse classes that have not been registered.</p>
</li>
<li><p><strong>Resolve asset URLs.</strong> Run an optional <code>assets.resolveUrl</code> handler.</p>
</li>
<li><p><strong>Check dependencies.</strong> Check external images and font availability.</p>
</li>
<li><p><strong>Try replacements.</strong> Run an optional <code>assets.replaceMissingImage</code> handler for missing images.</p>
</li>
<li><p><strong>Reject missing images.</strong> Throw <code>MISSING_ASSETS</code> if unresolved images remain.</p>
</li>
<li><p><strong>Load the content.</strong> Invoke Fabric.js with the engine's strict reviver.</p>
</li>
</ol>
<p>For the <code>MISSING_ASSETS</code> failure in step 6, the canvas content has not been replaced. The error carries a list shaped like this:</p>
<pre><code class="language-ts">error.missingAssets;
// [
//   { url: "/uploads/deleted-logo.png", objectIds: ["logo-image"] }
// ]
</code></pre>
<p>This guarantee is specific to failures detected before canvas loading. It is not a claim that every possible exception, custom-class side effect, or later Fabric.js failure can be rolled back. Embedded <code>data:</code> images are not fetched by the external-image preflight; malformed embedded data can fail during object creation instead.</p>
<p>Keep image checks enabled when you want this preflight behavior. Setting <code>assets.checkImages: false</code> skips the external-image check and removes the early missing-asset report.</p>
<p>The exact order is visible in the <a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/src/engine/create-document-engine.ts">engine loading source</a> and the <a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/src/assets/asset-pipeline.ts">asset preparation source</a>. The <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts#when-a-document-is-opened">images and fonts documentation</a> explains the public behavior.</p>
<h2>How do you prove that a failed image leaves the current drawing intact?</h2>
<p>Use a controlled missing-image document and compare the canvas objects before and after the rejected load. Serve the app locally and ensure the test image address returns a real failure; a development server that substitutes a valid image would defeat the test.</p>
<pre><code class="language-ts">import { Canvas, Textbox } from "fabric";
import {
  createDocumentEngine,
  isDocumentEngineError,
  type FabricDocument,
} from "fabricjs-document-engine";

export async function demonstrateMissingImage(element: HTMLCanvasElement) {
  const canvas = new Canvas(element, { width: 800, height: 500 });
  // No storage is configured in this isolated loading demonstration.
  const engine = createDocumentEngine({ canvas });
  canvas.add(new Textbox("Keep the current drawing", { left: 40, top: 40 }));

  const currentObjects = canvas.getObjects().slice();
  const timestamp = new Date().toISOString();
  const incoming: FabricDocument = {
    schemaVersion: 1,
    id: "broken-design",
    createdAt: timestamp,
    updatedAt: timestamp,
    revision: 1,
    canvas: { width: 800, height: 500 },
    objects: [{
      type: "Image",
      id: "logo-image",
      src: "/uploads/intentionally-missing-logo.png",
      width: 120,
      height: 120,
    }],
    metadata: {},
  };

  try {
    await engine.loadDocument(incoming);
    throw new Error("The test URL unexpectedly loaded.");
  } catch (error) {
    if (!isDocumentEngineError(error) || error.code !== "MISSING_ASSETS") {
      throw error;
    }
    const after = canvas.getObjects();
    if (after.length !== currentObjects.length ||
        after.some((object, index) =&gt; object !== currentObjects[index])) {
      throw new Error("The current drawing was changed.");
    }
    return { code: error.code, missingAssets: error.missingAssets };
  } finally {
    engine.destroy();
    await canvas.dispose();
  }
}
</code></pre>
<p>Use a dedicated canvas element for this helper. It creates and disposes its own canvas; do not pass the DOM element of an editor that already owns a Fabric.js instance.</p>
<p>The expected result is <code>MISSING_ASSETS</code>, the failed URL, and <code>logo-image</code>. The object-reference check confirms that the existing drawing survived the rejected load. The package repository contains a related <a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/tests/assets.browser.test.ts">browser test for missing images before canvas mutation</a>.</p>
<p>This helper was run in Chromium against package 1.0.2 and Fabric.js 7.4.0. It returned <code>MISSING_ASSETS</code> with the expected URL and object ID, and the original canvas object references remained unchanged. A separate native Fabric.js load with one rectangle and one unavailable image restored the rectangle and omitted the image. The complete TypeScript examples in this article also passed type checking.</p>
<p>The example intentionally has no storage adapter so it tests image preflight directly. A persistent editor should also protect unsaved changes, as the React example below does.</p>
<h2>How do you show the engine error in a React editor?</h2>
<p>Catch the rejected operation and branch on the stable error code. Show <code>missingAssets</code> to the user instead of silently logging a generic failure.</p>
<p>Install the packages:</p>
<pre><code class="language-bash">npm install fabric fabricjs-document-engine
</code></pre>
<p>This component is a complete local demonstration. It uses the built-in memory adapter, so saved records last only while that adapter remains in memory. Add text, save it, and try opening the broken document.</p>
<pre><code class="language-tsx">"use client";

import { Canvas, Textbox } from "fabric";
import {
  isDocumentEngineError,
  type DocumentEngineError,
  type FabricDocument,
} from "fabricjs-document-engine";
import { createMemoryStorage } from "fabricjs-document-engine/storage";
import {
  useDocumentEngine,
  useDocumentState,
} from "fabricjs-document-engine/react";
import { useEffect, useRef, useState } from "react";

const storage = createMemoryStorage();
const timestamp = new Date().toISOString();
const brokenDocument: FabricDocument = {
  schemaVersion: 1,
  id: "broken-design",
  createdAt: timestamp,
  updatedAt: timestamp,
  revision: 1,
  canvas: { width: 800, height: 500 },
  objects: [{
    type: "Image",
    id: "logo-image",
    src: "/uploads/intentionally-missing-logo.png",
    width: 120,
    height: 120,
  }],
  metadata: {},
};

export function SafeLoadEditor() {
  const elementRef = useRef&lt;HTMLCanvasElement&gt;(null);
  const [canvas, setCanvas] = useState&lt;Canvas | null&gt;(null);
  const [problem, setProblem] = useState&lt;DocumentEngineError | null&gt;(null);
  const [message, setMessage] = useState("");
  const [busy, setBusy] = useState(false);

  useEffect(() =&gt; {
    if (!elementRef.current) return;
    const created = new Canvas(elementRef.current, { width: 800, height: 500 });
    setCanvas(created);
    return () =&gt; { void created.dispose().catch(() =&gt; undefined); };
  }, []);

  const engine = useDocumentEngine(canvas, {
    storage,
    document: { id: "working-design" },
  });
  const state = useDocumentState(engine);

  async function run(action: () =&gt; Promise&lt;unknown&gt;, success: string) {
    setBusy(true);
    setProblem(null);
    setMessage("");
    try {
      await action();
      setMessage(success);
    } catch (error) {
      if (isDocumentEngineError(error)) {
        if (error.code === "LOAD_ABORTED") return;
        setProblem(error);
      } else {
        setMessage(error instanceof Error ? error.message : "Opening failed.");
      }
    } finally {
      setBusy(false);
    }
  }

  const disabled = !engine || busy || Boolean(state?.isSaving || state?.isLoading);

  return (
    &lt;section&gt;
      &lt;button disabled={disabled} onClick={() =&gt; canvas?.add(
        new Textbox("Keep the current drawing", { left: 40, top: 40 })
      )}&gt;Add text&lt;/button&gt;
      &lt;button disabled={disabled} onClick={() =&gt; {
        if (engine) void run(() =&gt; engine.save(), "Current drawing saved.");
      }}&gt;Save current drawing&lt;/button&gt;
      &lt;button disabled={disabled} onClick={() =&gt; {
        if (engine) void run(
          () =&gt; engine.loadDocument(brokenDocument),
          "Document opened."
        );
      }}&gt;Try opening the broken document&lt;/button&gt;

      {problem?.code === "MISSING_ASSETS" ? (
        &lt;div role="alert"&gt;
          &lt;p&gt;The document could not open. These images are unavailable:&lt;/p&gt;
          &lt;ul&gt;
            {problem.missingAssets.map((asset) =&gt; (
              &lt;li key={asset.url}&gt;
                &lt;code&gt;{asset.url}&lt;/code&gt;
                {" — Objects: "}{asset.objectIds.join(", ")}
              &lt;/li&gt;
            ))}
          &lt;/ul&gt;
          &lt;p&gt;Your current drawing is still visible.&lt;/p&gt;
        &lt;/div&gt;
      ) : problem?.code === "UNSAVED_CHANGES" ? (
        &lt;p role="alert"&gt;Save the current drawing before opening another document.&lt;/p&gt;
      ) : problem ? (
        &lt;p role="alert"&gt;{problem.code}: {problem.message}&lt;/p&gt;
      ) : null}

      &lt;p role="status"&gt;{message}&lt;/p&gt;
      &lt;p aria-live="polite"&gt;{state?.saveStatus ?? "Preparing editor"}&lt;/p&gt;
      &lt;canvas ref={elementRef} aria-label="Drawing preserved after a failed load" /&gt;
    &lt;/section&gt;
  );
}
</code></pre>
<p>Clicking <strong>Try opening the broken document</strong> before saving an edit should show <code>UNSAVED_CHANGES</code>. After saving, the same operation reaches image preflight and should show <code>MISSING_ASSETS</code>. The app never passes <code>discardUnsavedChanges: true</code> automatically.</p>
<p>For an API-backed editor, replace the memory adapter with your HTTP adapter and call <code>engine.load(documentId)</code> inside the same handler. The companion guide, <a href="./save-fabricjs-canvas-json-to-api.md">Save Fabric.js Canvas JSON to an API and Open It by Document ID</a>, includes the adapter and app-owned endpoint.</p>
<p>The <a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/react">React guide</a>, <a href="https://fabricjs-document-engine.jscrate.dev/docs/api/react-hooks">React hooks reference</a>, and <a href="https://fabricjs-document-engine.jscrate.dev/docs/api/error-codes">error code reference</a> document the hooks and error fields used here.</p>
<h2>Should you catch errors or subscribe to <code>load:error</code>?</h2>
<p>Use the operation's <code>catch</code> when the result belongs to a particular button, route, or document selection. Use <code>load:error</code> when a shared panel or logging system needs to observe engine loading failures.</p>
<pre><code class="language-tsx">import { useDocumentEvent } from "fabricjs-document-engine/react";

// Inside a React component that already has an engine.
useDocumentEvent(engine, "load:error", ({ error }) =&gt; {
  if (error.code === "LOAD_ABORTED") return;
  console.error("Document loading failed", error.code, error);
});
</code></pre>
<p>The event observes an error; it does not consume the promise rejection. Still handle the promise returned by <code>engine.load()</code> or <code>engine.loadDocument()</code>. If both paths show notifications, one failure can produce duplicate messages.</p>
<p>An unsaved-change guard can reject before the loading sequence starts. Catching the requested operation also covers that case. <code>useDocumentState(engine)</code> exposes <code>loadError</code> for failures tracked by the state store, but a persistent guard message should not depend only on that field.</p>
<p>The event payload is <code>{ error }</code>, not the error directly. See the <a href="https://fabricjs-document-engine.jscrate.dev/docs/api/events">events API</a> for the exact contract.</p>
<h2>How do you repair a missing image without removing the object?</h2>
<p>Use <code>assets.replaceMissingImage</code> when your app has a known replacement. The hook receives the missing URL and the object IDs that refer to it.</p>
<pre><code class="language-ts">const engine = createDocumentEngine({
  canvas,
  storage,
  assets: {
    replaceMissingImage(image) {
      if (image.url === "/uploads/deleted-logo.png") {
        return "/assets/replacement-logo.png";
      }
      return null;
    },
  },
});
</code></pre>
<p>This is an engine-options fragment; <code>canvas</code> and <code>storage</code> are the instances already owned by your app, and <code>createDocumentEngine</code> is imported from the package.</p>
<p>The engine checks the replacement URL too. If the replacement is also unavailable, the load still fails. If the replacement works, loading can continue and the engine reports an <code>IMAGE_REPLACED</code> warning.</p>
<p>A replacement policy changes document content. Make that policy clear to the user. For client artwork, returning <code>null</code> and asking for a replacement may be preferable to inserting a generic placeholder.</p>
<p>Save deliberately after a repaired load if the new address should be retained. A successful load begins a saved session; do not assume the replacement alone has marked the document dirty or written a new record to your backend.</p>
<p>See the <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts">asset options guide</a> and <a href="https://fabricjs-document-engine.jscrate.dev/docs/api/create-document-engine">createDocumentEngine API</a>. <code>engine.replaceImage(oldUrl, newUrl)</code> repairs images already present in the current canvas; it does not modify an incoming document that preflight rejected.</p>
<h2>What if the image URL expires instead of disappearing?</h2>
<p>Resolve the stored asset address into a currently accessible URL with <code>assets.resolveUrl</code>. Your app can use that handler to request a fresh signed URL from its backend before the engine checks and loads the image.</p>
<pre><code class="language-ts">const assets = {
  async resolveUrl(url: string) {
    if (!url.startsWith("asset://")) return url;
    const assetId = url.slice("asset://".length);
    const response = await fetch(
      `/api/assets/${encodeURIComponent(assetId)}/url`
    );
    if (!response.ok) throw new Error("The asset address could not be resolved.");
    const result = (await response.json()) as { url: string };
    return result.url;
  },
};
</code></pre>
<p>The asset-resolution endpoint is <strong>app-owned backend code</strong>. The package does not implement asset permissions or signed-URL generation.</p>
<p>Also plan the return path when saving. The engine rewrites the incoming image address to the resolved URL; saving that content can persist the signed URL. Keep durable asset identity in app-managed data and normalize addresses in your storage layer if your API requires stable identifiers.</p>
<p>For tab-only <code>blob:</code> images, configure <code>assets.upload</code> while saving so reopening does not depend on the previous tab. Both hooks are described in the <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts">images and fonts guide</a>.</p>
<h2>Can you fix an image CORS failure from React alone?</h2>
<p>The image host must permit the cross-origin request when the image is loaded with CORS enabled. Setting <code>crossOrigin: "anonymous"</code> in React or Fabric.js does not create permission on the remote server.</p>
<p>Check both the image request and response headers. For a canvas that displays correctly but cannot export, inspect whether the image was loaded in a way that tainted the canvas.</p>
<p>The engine's <code>IMAGE_CROSS_ORIGIN</code> warning helps identify risky image references. Export preflight can reject a raster export with <code>EXPORT_BLOCKED</code>; that is a different error from loading a document with <code>MISSING_ASSETS</code>.</p>
<p>Read the <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts#cors-and-export">CORS guidance</a> and <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/export">export guide</a>. The engine reports the problem; your app and image host control how the asset is served.</p>
<h2>What should the editor do with each loading error?</h2>
<p>Use error codes to decide what action to offer. Do not match error-message wording.</p>
<table>
<thead>
<tr>
<th>Engine code</th>
<th>Useful editor response</th>
</tr>
</thead>
<tbody><tr>
<td><code>MISSING_ASSETS</code></td>
<td>List missing image URLs and affected object IDs; offer repair or retry</td>
</tr>
<tr>
<td><code>MISSING_FONTS</code></td>
<td>Load the required fonts or explain why the document cannot open</td>
</tr>
<tr>
<td><code>UNKNOWN_OBJECT_TYPE</code></td>
<td>Register the missing custom class before retrying</td>
</tr>
<tr>
<td><code>INVALID_DOCUMENT</code></td>
<td>Show document validation details; request a valid saved record</td>
</tr>
<tr>
<td><code>UNSAVED_CHANGES</code></td>
<td>Offer save, cancel, or an explicit discard decision</td>
</tr>
<tr>
<td><code>DOCUMENT_NOT_FOUND</code></td>
<td>Explain that the requested stored document is unavailable</td>
</tr>
<tr>
<td><code>LOAD_ABORTED</code></td>
<td>Usually keep quiet when a newer document selection superseded the request</td>
</tr>
<tr>
<td><code>LOAD_FAILED</code></td>
<td>Inspect the storage or Fabric.js failure and the underlying <code>cause</code></td>
</tr>
</tbody></table>
<p>Missing fonts normally produce <code>FONT_UNAVAILABLE</code> warnings and fallback text. Set <code>assets.requireFonts: true</code> when the document must not open with fallback fonts; unavailable fonts then produce <code>MISSING_FONTS</code>.</p>
<p>The package's <a href="https://fabricjs-document-engine.jscrate.dev/docs/api/error-codes">error code reference</a> and <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/custom-objects">custom object guide</a> describe these cases.</p>
<h2>Will awaiting <code>loadFromJSON</code> solve every loading problem?</h2>
<p>Awaiting the load is necessary for code that depends on loaded objects. It does not make deleted images available or register a missing custom class.</p>
<p>For a direct Fabric.js load, a basic successful path is:</p>
<pre><code class="language-ts">await canvas.loadFromJSON(savedJson);
canvas.requestRenderAll();
</code></pre>
<p>That fixes the timing of rendering after the promise settles. With Fabric.js versions that permit failed objects to be omitted, a fulfilled promise alone does not prove every expected object was restored.</p>
<p>When using the document engine, wait for its load method. The engine requests rendering after its successful load.</p>
<h2>How do you keep an older load from replacing a newer document?</h2>
<p>Use <code>engine.load(id)</code> for the complete storage-and-loading operation. The engine claims the load before reading storage, so a slower response for an older selection cannot replace a newer completed load. A superseded request rejects with <code>LOAD_ABORTED</code>.</p>
<p>If your app separately fetches JSON and then calls <code>loadDocument</code>, your app must also prevent older fetch responses from being submitted after newer ones. The engine cannot know that a later <code>loadDocument</code> call came from an earlier UI selection.</p>
<p>The <a href="https://fabricjs-document-engine.jscrate.dev/docs/api/document-engine">loading API</a> documents the entry points, and the <a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/src/engine/create-document-engine.ts">engine source</a> shows the storage-response guard. Fabric.js itself also warns about overlapping loads in its <a href="https://www.fabricjs.com/api/classes/staticcanvas/#loadfromjson">loadFromJSON reference</a>.</p>
<p>Start with one deliberately missing image. Confirm that the old drawing stays visible, the error contains the image URL and object IDs, and the React UI explains the failure. Then test expired URLs, missing fonts, and overlapping document selections against your own storage. The <a href="https://www.npmjs.com/package/fabricjs-document-engine">npm package</a>, <a href="https://fabricjs-document-engine.jscrate.dev/docs/overview/quick-start">quick start</a>, and <a href="https://github.com/re-sohail/fabricjs-document-engine">GitHub repository</a> provide the source and further examples.</p>
]]></content:encoded></item><item><title><![CDATA[Save Fabric.js Canvas JSON to an API and Open It by Document ID]]></title><description><![CDATA[To save a Fabric.js canvas to a database, send its editable JSON through an API and retrieve that JSON when the user opens the document. With fabricjs-document-engine on npm, a storage adapter connect]]></description><link>https://chroma-panel.hashnode.dev/save-fabricjs-canvas-to-database</link><guid isPermaLink="true">https://chroma-panel.hashnode.dev/save-fabricjs-canvas-to-database</guid><category><![CDATA[fabricjs]]></category><category><![CDATA[React]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[database]]></category><category><![CDATA[fabricjs documents engine]]></category><dc:creator><![CDATA[Sohail Khan]]></dc:creator><pubDate>Thu, 01 Oct 2026 05:24:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ab0f13c1a95a677dadec704/beb0963d-d303-4d20-be15-dcd48531c281.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>To save a Fabric.js canvas to a database, send its editable JSON through an API and retrieve that JSON when the user opens the document. With <a href="https://www.npmjs.com/package/fabricjs-document-engine">fabricjs-document-engine on npm</a>, a storage adapter connects <code>engine.save()</code> and <code>engine.load(documentId)</code> to your own HTTP endpoints.</p>
<p>The package manages the document workflow around your existing Fabric.js canvas. Your app owns the API, authentication, database, and file storage. You can keep the toolbar and React components you already have.</p>
<p>This guide builds a React editor, a typed HTTP adapter, and an Express/PostgreSQL endpoint. The examples follow the public API in package <strong>1.0.2</strong>, checked on <strong>October 1, 2026</strong>. The package supports Fabric.js <strong>6 and 7</strong>. Start with the <a href="https://fabricjs-document-engine.jscrate.dev/docs/overview/quick-start">official quick-start guide</a> if you have not created an engine before.</p>
<h2>What should you save to reopen an editable Fabric.js canvas?</h2>
<p>Save the document JSON. A PNG is useful for a thumbnail or download, but a PNG does not preserve independently editable text, shapes, groups, and object properties.</p>
<p>Fabric.js supplies serialization through <code>canvas.toJSON()</code> and <code>canvas.toObject()</code>. The document engine adds the information needed to identify, reopen, and save a document consistently:</p>
<table>
<thead>
<tr>
<th>Saved field</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td><code>id</code></td>
<td>Finds the document through your API</td>
</tr>
<tr>
<td><code>schemaVersion</code></td>
<td>Identifies the engine's document format</td>
</tr>
<tr>
<td><code>revision</code></td>
<td>Detects a save based on an older stored revision</td>
</tr>
<tr>
<td><code>canvas</code></td>
<td>Records width, height, and background</td>
</tr>
<tr>
<td><code>objects</code></td>
<td>Records serialized objects in stacking order, with stable IDs</td>
</tr>
<tr>
<td><code>assets</code></td>
<td>Lists external images and font variants used by objects</td>
</tr>
<tr>
<td><code>metadata</code></td>
<td>Holds app data such as the document title</td>
</tr>
</tbody></table>
<p>Store the complete document body. Keeping only <code>objects</code> would remove the document ID, revision, and canvas dimensions that the rest of this example uses.</p>
<p>The <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/document-format">document format reference</a> describes the exact fields. The <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-and-load">save and load guide</a> explains how the engine serializes an existing canvas.</p>
<h2>What is the storage adapter contract?</h2>
<p>A basic <code>DocumentStorage</code> adapter has <strong>two required methods</strong>: one reads a document and one saves it. The engine does not prescribe a database, URL structure, or server framework.</p>
<pre><code class="language-ts">import type { FabricDocument } from "fabricjs-document-engine";

// The public contract, shown here for reference.
interface DocumentStorage {
  loadDocument(id: string): Promise&lt;unknown&gt;;
  saveDocument(
    document: FabricDocument,
    context: {
      expectedRevision: number | null;
      signal: AbortSignal;
    }
  ): Promise&lt;void | { revision?: number }&gt;;
}
</code></pre>
<p><code>loadDocument</code> returns <code>unknown</code> because storage is an external boundary. The engine checks the returned document before loading its content.</p>
<p><code>expectedRevision</code> is the revision this editor last loaded or saved. A new document starts at revision <code>0</code>. The outgoing document normally carries the next revision. If your server assigns the revision, return the accepted value as <code>{ revision }</code>.</p>
<p>A value of <code>null</code> means the caller requested an overwrite. Pass the provided <code>signal</code> to the save request so the engine can abort the client request when the session changes. Request cancellation does not undo a write that the server has already committed.</p>
<p>These behaviors come from the <a href="https://fabricjs-document-engine.jscrate.dev/docs/api/storage-api">storage API reference</a> and the <a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/custom-backend">custom backend guide</a>. Optional version-storage methods are outside this article's two-method adapter.</p>
<h2>How do you connect the document engine to an HTTP API?</h2>
<p>Install Fabric.js and the document engine in your React app:</p>
<pre><code class="language-bash">npm install fabric fabricjs-document-engine
</code></pre>
<p>Create <code>api-storage.ts</code>. This adapter uses same-origin endpoints and your app's existing session cookie.</p>
<pre><code class="language-ts">import {
  DocumentEngineError,
  type DocumentStorage,
} from "fabricjs-document-engine";

export const apiStorage: DocumentStorage = {
  async loadDocument(id) {
    const response = await fetch(
      `/api/documents/${encodeURIComponent(id)}`,
      { cache: "no-store" }
    );

    if (response.status === 404) {
      throw new DocumentEngineError(
        "DOCUMENT_NOT_FOUND",
        "This document was not found."
      );
    }
    if (!response.ok) {
      throw new Error(`Opening the document failed (${response.status}).`);
    }
    return response.json();
  },

  async saveDocument(document, { expectedRevision, signal }) {
    const response = await fetch(
      `/api/documents/${encodeURIComponent(document.id)}`,
      {
        method: "PUT",
        signal,
        headers: {
          "Content-Type": "application/json",
          "X-Expected-Revision":
            expectedRevision === null ? "*" : String(expectedRevision),
        },
        body: JSON.stringify(document),
      }
    );

    if (response.status === 409 || response.status === 412) {
      throw new DocumentEngineError(
        "SAVE_CONFLICT",
        "Another tab or device saved this document first."
      );
    }
    if ([400, 401, 403, 404, 413, 422].includes(response.status)) {
      throw Object.assign(
        new Error(`The server refused this save (${response.status}).`),
        { retryable: false }
      );
    }
    if (!response.ok) {
      throw new Error(`Saving the document failed (${response.status}).`);
    }

    const result = (await response.json()) as { revision?: unknown };
    if (!Number.isSafeInteger(result.revision) || Number(result.revision) &lt; 1) {
      throw Object.assign(new Error("The API returned an invalid revision."), {
        retryable: false,
      });
    }
    return { revision: Number(result.revision) };
  },
};
</code></pre>
<p><code>X-Expected-Revision</code> is an <strong>app-defined header</strong>. In this example, <code>0</code> means create a document and <code>*</code> means explicitly overwrite an existing one. These are conventions shared by this adapter and the endpoint below; the package does not require this header.</p>
<p>The official <a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/docs/storage-examples.md#rest-api">REST storage example</a> uses <code>If-Match</code>. This article uses a custom header to keep its numeric revision protocol separate from standard HTTP entity-tag semantics.</p>
<p>Throwing <code>SAVE_CONFLICT</code> tells the engine to stop retrying that save. Setting <code>retryable: false</code> also stops retries for errors that require a change to the request or permissions. Other save failures can follow the engine's retry policy.</p>
<h2>How do you save and open a document in React?</h2>
<p>First, create the Fabric.js canvas after React mounts the canvas element. Put this helper in <code>use-fabric-canvas.ts</code>:</p>
<pre><code class="language-ts">import { Canvas } from "fabric";
import { useEffect, useRef, useState } from "react";

export function useFabricCanvas() {
  const elementRef = useRef&lt;HTMLCanvasElement&gt;(null);
  const [canvas, setCanvas] = useState&lt;Canvas | null&gt;(null);

  useEffect(() =&gt; {
    if (!elementRef.current) return;
    const created = new Canvas(elementRef.current, {
      width: 800,
      height: 500,
    });
    setCanvas(created);
    return () =&gt; {
      void created.dispose().catch(() =&gt; undefined);
    };
  }, []);

  return { elementRef, canvas };
}
</code></pre>
<p>Then create <code>document-editor.tsx</code>:</p>
<pre><code class="language-tsx">"use client";

import { Rect } from "fabric";
import { isDocumentEngineError } from "fabricjs-document-engine";
import {
  useDocumentEngine,
  useDocumentState,
} from "fabricjs-document-engine/react";
import { useState } from "react";
import { apiStorage } from "./api-storage";
import { useFabricCanvas } from "./use-fabric-canvas";

export function DocumentEditor() {
  const { elementRef, canvas } = useFabricCanvas();
  const engine = useDocumentEngine(canvas, {
    storage: apiStorage,
    document: { id: "design-42", metadata: { title: "First design" } },
  });
  const state = useDocumentState(engine);
  const [documentId, setDocumentId] = useState("design-42");
  const [busy, setBusy] = useState(false);
  const [message, setMessage] = useState("");

  async function run(action: () =&gt; Promise&lt;unknown&gt;, success: string) {
    setBusy(true);
    setMessage("");
    try {
      await action();
      setMessage(success);
    } catch (error) {
      if (isDocumentEngineError(error) &amp;&amp; error.code === "LOAD_ABORTED") return;
      if (isDocumentEngineError(error) &amp;&amp; error.code === "UNSAVED_CHANGES") {
        setMessage("Save your current changes before opening another document.");
      } else if (isDocumentEngineError(error) &amp;&amp; error.code === "SAVE_CONFLICT") {
        setMessage("A newer revision exists. Review the conflict before continuing.");
      } else {
        setMessage(error instanceof Error ? error.message : "The request failed.");
      }
    } finally {
      setBusy(false);
    }
  }

  const disabled = !engine || busy || Boolean(state?.isSaving || state?.isLoading);

  return (
    &lt;section&gt;
      &lt;label&gt;
        Document ID
        &lt;input
          value={documentId}
          onChange={(event) =&gt; setDocumentId(event.target.value)}
        /&gt;
      &lt;/label&gt;
      &lt;button
        disabled={disabled || !documentId.trim()}
        onClick={() =&gt; {
          if (engine) void run(
            () =&gt; engine.load(documentId.trim()),
            "Document opened."
          );
        }}
      &gt;Open&lt;/button&gt;
      &lt;button
        disabled={disabled}
        onClick={() =&gt; canvas?.add(new Rect({
          left: 80, top: 60, width: 160, height: 100, fill: "#6366f1",
        }))}
      &gt;Add rectangle&lt;/button&gt;
      &lt;button
        disabled={disabled}
        onClick={() =&gt; {
          if (engine) void run(() =&gt; engine.save(), "Save confirmed by the API.");
        }}
      &gt;Save&lt;/button&gt;
      &lt;p aria-live="polite"&gt;
        Current document: {state?.documentId ?? "Preparing editor"}
        {state ? ` · Revision ${state.revision} · ${state.saveStatus}` : ""}
      &lt;/p&gt;
      &lt;p role="status"&gt;{message}&lt;/p&gt;
      &lt;canvas ref={elementRef} aria-label="Editable design canvas" /&gt;
    &lt;/section&gt;
  );
}
</code></pre>
<p>Add a rectangle and click <strong>Save</strong>. The adapter sends the full document to <code>PUT /api/documents/design-42</code>. Refresh the page and click <strong>Open</strong> to retrieve it through <code>GET /api/documents/design-42</code>.</p>
<p>The text field chooses the document to open. Changing the text field does not rename the current document. A save always uses the engine's current document ID, shown below the buttons.</p>
<p>The example uses manual saves to make the request flow visible. To add autosave, pass <code>autosave: true</code> when creating the engine and continue displaying <code>state.saveStatus</code> and <code>state.saveError</code>. Options are read when the hook creates the engine; changing an options object later does not reconfigure that engine.</p>
<p>See the <a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/react">React guide</a>, <a href="https://fabricjs-document-engine.jscrate.dev/docs/api/react-hooks">React hooks API</a>, and <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/autosave">autosave guide</a>. In Next.js, mount Fabric.js through the client-only setup in the <a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/next-js">Next.js guide</a>.</p>
<h2>What does the app-owned HTTP endpoint look like?</h2>
<p>The following <strong>backend code belongs to your application</strong>. It is an illustrative Express 5 and PostgreSQL implementation, not an endpoint supplied by <code>fabricjs-document-engine</code>.</p>
<p>Create a table through your app's database migration:</p>
<pre><code class="language-sql">CREATE TABLE documents (
  owner_id text NOT NULL,
  id text NOT NULL,
  body jsonb NOT NULL,
  revision integer NOT NULL CHECK (revision &gt;= 1),
  PRIMARY KEY (owner_id, id)
);
</code></pre>
<p>Use your existing authentication middleware as <code>requireUser</code>. That middleware must verify the session, reject unauthenticated requests, and set <code>req.auth.userId</code>. Neither the middleware nor <code>DATABASE_URL</code> comes from the package.</p>
<pre><code class="language-ts">// server.ts — app-owned backend, using Express 5.
import express from "express";
import { Pool } from "pg";
import { validateDocument, type FabricDocument } from "fabricjs-document-engine";
import { requireUser } from "./app-auth"; // Your verified-session middleware.

declare global {
  namespace Express {
    interface Request { auth: { userId: string } }
  }
}

const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

app.use(requireUser);
app.use(express.json({ limit: "2mb" })); // An app-chosen request limit.

app.get("/api/documents/:id", async (req, res) =&gt; {
  const result = await pool.query(
    "SELECT body FROM documents WHERE owner_id = $1 AND id = $2",
    [req.auth.userId, req.params.id]
  );
  if (result.rowCount === 0) {
    res.status(404).json({ error: "DOCUMENT_NOT_FOUND" });
    return;
  }
  res.set("Cache-Control", "no-store").json(result.rows[0].body);
});

app.put("/api/documents/:id", async (req, res) =&gt; {
  const header = req.get("X-Expected-Revision");
  if (!header || (header !== "*" &amp;&amp; !/^(0|[1-9]\d*)$/.test(header))) {
    res.status(400).json({ error: "INVALID_EXPECTED_REVISION" });
    return;
  }
  const expected = header === "*" ? null : Number(header);
  if (expected !== null &amp;&amp; (!Number.isSafeInteger(expected) || expected &gt; 2147483646)) {
    res.status(400).json({ error: "INVALID_EXPECTED_REVISION" });
    return;
  }

  const value: unknown = req.body;
  const issues = validateDocument(value);
  if (issues.length &gt; 0) {
    res.status(422).json({ error: "INVALID_DOCUMENT", issues });
    return;
  }
  const document = value as FabricDocument;
  if (document.id !== req.params.id) {
    res.status(422).json({ error: "DOCUMENT_ID_MISMATCH" });
    return;
  }

  const params = [req.auth.userId, document.id, JSON.stringify(document)];
  const result = expected === 0
    ? await pool.query(
        `INSERT INTO documents (owner_id, id, body, revision)
         VALUES ($1, $2, jsonb_set($3::jsonb, '{revision}', '1'::jsonb), 1)
         ON CONFLICT (owner_id, id) DO NOTHING
         RETURNING revision`,
        params
      )
    : await pool.query(
        `UPDATE documents
         SET body = jsonb_set($3::jsonb, '{revision}', to_jsonb(revision + 1)),
             revision = revision + 1
         WHERE owner_id = $1 AND id = $2
           AND ($4::integer IS NULL OR revision = $4::integer)
         RETURNING revision`,
        [...params, expected]
      );

  if (result.rowCount === 0) {
    res.status(409).json({ error: "SAVE_CONFLICT" });
    return;
  }
  res.status(expected === 0 ? 201 : 200).json({
    revision: result.rows[0].revision,
  });
});

app.listen(3001);
</code></pre>
<p>Run the API behind the same origin as the React app, or configure a development proxy for <code>/api</code>. Apply your existing session and CSRF policy to these routes. A document ID locates a row; ownership comes from the verified session, never from <code>metadata.owner</code> supplied by the browser.</p>
<p>The endpoint assigns revisions and writes that value into both the JSON body and the revision column. This keeps the next <code>GET</code> consistent with the previous save response. Its overwrite branch updates an existing row; it does not recreate a deleted document. A missing row or revision mismatch returns <code>409</code>.</p>
<p>The <code>2mb</code> limit is an example application limit, not a package limit. Adjust it for your documents. <code>validateDocument</code> checks the document shape; enforce any additional object-count, URL, asset, or business rules in your backend too.</p>
<p>The SQL follows the package's <a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/custom-backend#the-server">atomic revision-check guidance</a>. See PostgreSQL's <a href="https://www.postgresql.org/docs/current/sql-update.html">UPDATE and RETURNING documentation</a> for the database behavior. Express 5 forwards rejected async route handlers to error handling; integrate your app's normal handler using the <a href="https://expressjs.com/en/guide/error-handling/">Express error-handling guide</a>.</p>
<h2>How does a revision check prevent lost updates?</h2>
<p>Suppose two tabs open revision <code>4</code> of the same document. Both tabs edit it. Without a server-side check, the second save can overwrite the first save without warning.</p>
<table>
<thead>
<tr>
<th>Request</th>
<th>Expected revision</th>
<th>Stored revision before request</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>Tab A saves</td>
<td><code>4</code></td>
<td><code>4</code></td>
<td>Accepted; server stores revision <code>5</code></td>
</tr>
<tr>
<td>Tab B saves</td>
<td><code>4</code></td>
<td><code>5</code></td>
<td>Rejected with HTTP <code>409</code></td>
</tr>
<tr>
<td>Tab B adapter handles response</td>
<td>—</td>
<td><code>5</code></td>
<td>Throws <code>SAVE_CONFLICT</code></td>
</tr>
</tbody></table>
<p>These numbers illustrate the protocol; they are not a performance measurement.</p>
<p>Check the revision and write the document in the same database operation. A separate <code>SELECT</code> followed by an unconditional <code>UPDATE</code> leaves room for another request to save between them.</p>
<p>The engine also serializes saves within one editor session. That does not replace a database revision check across tabs, devices, or API clients.</p>
<p>For conflict UI, offer to review the stored document or deliberately overwrite it. <code>engine.save({ overwrite: true })</code> passes <code>expectedRevision: null</code>; use that only after the user chooses to overwrite. Loading a newer document can also replace unsaved work, so make that choice explicit before passing <code>discardUnsavedChanges: true</code>.</p>
<p>Read the <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-conflicts">save conflicts guide</a> for the full decision flow.</p>
<h2>Are image files stored inside the canvas JSON?</h2>
<p>A normal image object stores an address for its image. Saving that address does not upload the file. In particular, a <code>blob:</code> URL refers to data in the current browser environment and is unsuitable for reopening on another device.</p>
<p>Configure <code>assets.upload</code> to turn tab-only images into durable URLs while the engine prepares a save:</p>
<pre><code class="language-ts">// Add this assets option when creating the engine.
const assets = {
  async upload({ blob }: { blob: Blob }) {
    const body = new FormData();
    body.append("file", blob);
    const response = await fetch("/api/uploads", { method: "POST", body });
    if (!response.ok) throw new Error("The image could not be uploaded.");
    const result = (await response.json()) as { url: string };
    return result.url;
  },
};
</code></pre>
<p><code>/api/uploads</code> is another <strong>app-owned endpoint</strong>. It must save the file and return an address that your readers can access. The package does not create an S3 bucket, configure a CDN, or supply the upload route.</p>
<p>Avoid treating an expiring signed URL as a permanent asset identity. The <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts">images and fonts guide</a> explains <code>assets.upload</code> and <code>assets.resolveUrl</code>, including how to resolve stored asset addresses when opening a document.</p>
<p>If a stored image cannot be opened, see the companion article, <a href="./fabricjs-loadfromjson-failed-image.md">Why Fabric.js loadFromJSON Can Leave Your Editor Half-Loaded</a>.</p>
<h2>What should you check before shipping this save flow?</h2>
<p>Verify the complete round trip, including failures:</p>
<ul>
<li><strong>Reopen the document.</strong> Save, refresh, and open the same ID.</li>
<li><strong>Compare the content.</strong> Check object IDs, canvas dimensions, and stacking order.</li>
<li><strong>Create a conflict.</strong> Open two tabs; confirm the older revision cannot overwrite the newer save.</li>
<li><strong>Reject invalid input.</strong> Send a mismatched document ID and malformed document body.</li>
<li><strong>Check ownership.</strong> Confirm another account cannot read or write the document.</li>
<li><strong>Interrupt a request.</strong> Confirm the UI does not claim a failed save succeeded.</li>
<li><strong>Reopen uploaded images.</strong> Test from a fresh browser session.</li>
</ul>
<p>The <a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/verify-adapter">adapter verification guide</a> helps check storage behavior. The <a href="https://fabricjs-document-engine.jscrate.dev/docs/production/production-checklist">production checklist</a> covers the wider editor lifecycle.</p>
<p>For this article, the complete TypeScript examples passed type checking against package 1.0.2 and Fabric.js 7.4.0. A Chromium check with the built-in memory adapter confirmed that saved object IDs and canvas dimensions survived reopening, the first save produced revision <code>1</code>, and an older editor's save produced <code>SAVE_CONFLICT</code>. The HTTP/PostgreSQL endpoint is an illustrative implementation; connect and test it with your app's actual authentication and database.</p>
<p>Retries need one extra decision in your API: a write can succeed even if its response is lost. Retrying with the old expected revision can then produce a conflict. This sample preserves the stored document rather than guessing. If your app needs transparent retry acknowledgments, implement request idempotency on the server.</p>
<h2>Can you use MongoDB, MySQL, or a different backend?</h2>
<p>Yes. The adapter sends plain JSON and returns a revision. Replace the PostgreSQL endpoint with your own backend while keeping the two storage methods and an atomic revision condition.</p>
<p>For MongoDB, the equivalent idea is a conditional update filtered by document ID, owner, and expected revision. The exact database code belongs to your app. The package does not ship a MongoDB model or a MySQL table.</p>
<h2>Can you open JSON saved by an older Fabric.js editor?</h2>
<p>The engine can recognize plain Fabric.js JSON through <code>load</code>, <code>loadDocument</code>, and <code>importFabricJson</code>. A document loaded by <code>engine.load(id)</code> retains that requested ID, and its next save uses the engine's current document format.</p>
<p>Check the <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/migration">migration guide</a> before moving existing records. Back up the stored JSON and test your custom classes and assets. The API example above validates incoming saves in the engine's document format.</p>
<h2>Does calling <code>toDocument()</code> mean the canvas has been saved?</h2>
<p>No. <code>engine.toDocument()</code> produces a document object. <code>engine.save()</code> uses the configured storage adapter and updates the engine's save state after storage accepts the write.</p>
<p>Use <code>toDocument()</code> when you need a snapshot. Use <code>save()</code> for the managed persistence flow shown here.</p>
<p>Start with a manual save and an open-by-ID action. Once the round trip and conflicts work with your real storage, add autosave and image uploads. The <a href="https://www.npmjs.com/package/fabricjs-document-engine">npm package</a>, <a href="https://fabricjs-document-engine.jscrate.dev/docs/overview/quick-start">quick start</a>, and <a href="https://github.com/re-sohail/fabricjs-document-engine">GitHub repository</a> contain the implementation and related examples.</p>
]]></content:encoded></item><item><title><![CDATA[Fabric.js Editor Problems: Save, Load, Undo, Autosave, and Recovery]]></title><description><![CDATA[Fabric.js makes it easy to put shapes, text, and images on a canvas. Building an editor around that canvas takes more work. You need to know which object is which after a reload, how to undo a drag, w]]></description><link>https://chroma-panel.hashnode.dev/fabric-js-editor-problems-save-load-undo-autosave-and-recovery</link><guid isPermaLink="true">https://chroma-panel.hashnode.dev/fabric-js-editor-problems-save-load-undo-autosave-and-recovery</guid><category><![CDATA[Fabric.js]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Canvas Editor]]></category><category><![CDATA[Save and Load]]></category><category><![CDATA[Undo and Redo]]></category><category><![CDATA[canvas]]></category><category><![CDATA[fabricjs]]></category><dc:creator><![CDATA[Sohail Khan]]></dc:creator><pubDate>Wed, 30 Sep 2026 07:04:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ab0f13c1a95a677dadec704/73b2f600-7b8f-4f8f-a619-fe136993a14a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Fabric.js makes it easy to put shapes, text, and images on a canvas. Building an editor around that canvas takes more work. You need to know which object is which after a reload, how to undo a drag, what to do when an image disappears, and whether a slow save can overwrite a newer one.</p>
<p>These are the problems <a href="https://www.npmjs.com/package/fabricjs-document-engine">fabricjs-document-engine</a> is built to handle. It connects to a Fabric.js canvas you already own. You keep your own toolbar, storage service, and user interface. The package adds the document workflow around them.</p>
<p>This guide starts with the problems people actually describe, then shows what the package does and where your application still has work to do. The examples use the published 1.0.2 API with Fabric.js 7. The package also supports Fabric.js 6. For a complete first project, see the <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-and-load">save and load tutorial</a>.</p>
<h2>The short answer</h2>
<table>
<thead>
<tr>
<th>What a developer searches for</th>
<th>What is happening</th>
<th>Where the package helps</th>
</tr>
</thead>
<tbody><tr>
<td>“Fabric.js undo redo not working”</td>
<td>History must be built around object changes and asynchronous loads.</td>
<td>Action-level history, <code>undo()</code>, <code>redo()</code>, transactions.</td>
</tr>
<tr>
<td>“Fabric.js custom properties lost after JSON load”</td>
<td>Extra fields need explicit serialization and class registration.</td>
<td>Register the class and its saved properties once.</td>
</tr>
<tr>
<td>“Fabric.js object reference lost after undo”</td>
<td>Reloading creates new object instances.</td>
<td>Stable IDs and <code>getObjectById()</code>.</td>
</tr>
<tr>
<td>“Fabric.js loadFromJSON missing image”</td>
<td>Saved image URLs may expire or fail to load.</td>
<td>Check assets before replacing the current canvas; report missing URLs.</td>
</tr>
<tr>
<td>“Fabric.js autosave overwrites newer changes”</td>
<td>Requests and edits can overlap.</td>
<td>One in-flight save, a follow-up save, and revision checks.</td>
</tr>
<tr>
<td>“Fabric.js canvas lost after refresh”</td>
<td>Unsaved edits never reached durable storage.</td>
<td>Optional local recovery checkpoints and a restore flow.</td>
</tr>
<tr>
<td>“Fabric.js tainted canvas export”</td>
<td>Browser CORS rules block raster output.</td>
<td>Export preflight reports the problem; your image host must allow CORS.</td>
</tr>
</tbody></table>
<p>These are <strong>search phrases, not claims that every report is a Fabric.js core bug</strong>. Many are integration problems that appear when an application grows from a drawing canvas into an editor. The <a href="https://fabricjs.com/api/classes/canvas/">Fabric.js API</a> already includes serialization and image export. The package coordinates them into a document lifecycle.</p>
<h2>Start with a canvas and a document</h2>
<p>Install the package with a supported Fabric version:</p>
<pre><code class="language-bash">npm install fabric@^7 fabricjs-document-engine@1.0.2
</code></pre>
<p>Here is a small setup using the built-in browser storage adapter. The HTML page needs <code>&lt;canvas id="editor"&gt;&lt;/canvas&gt;</code>.</p>
<pre><code class="language-js">import { Canvas, Rect } from "fabric";
import { createDocumentEngine } from "fabricjs-document-engine";
import { createLocalStorage } from "fabricjs-document-engine/storage";

const canvas = new Canvas("editor", {
  width: 800,
  height: 500,
  backgroundColor: "#f8fafc",
});

const engine = createDocumentEngine({
  canvas,
  document: { id: "poster-42" },
  storage: createLocalStorage({ prefix: "my-editor:" }),
  autosave: { delay: 1000, maxWait: 10000 },
});

canvas.add(new Rect({
  left: 80,
  top: 70,
  width: 180,
  height: 120,
  fill: "#4f46e5",
}));

await engine.save();
</code></pre>
<p>The <code>document</code> option gives this design a known ID. <code>createLocalStorage</code> is useful for a prototype on one browser. For a product with accounts or multiple devices, provide a <a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/custom-backend">backend storage adapter</a>. The engine does not host your files or create your API.</p>
<p>You can also use <code>engine.toDocument()</code> and <code>engine.loadDocument(json)</code> without any adapter. Those calls serialize and open a document you store yourself. <code>engine.save()</code>, <code>engine.load(id)</code>, autosave, and versions need an adapter. See the <a href="https://fabricjs-document-engine.jscrate.dev/docs/api/create-document-engine">creation options</a> and <a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/storage-adapters">storage guide</a>.</p>
<h2>1. “Fabric.js undo redo not working”</h2>
<p>Fabric.js has no built-in document history. A developer in <a href="https://github.com/fabricjs/fabric.js/issues/10011">Fabric.js issue #10011</a> tried to add history to a Fabric 6 editor. The thread includes two familiar problems: older callback-style <code>loadFromJSON</code> code no longer matched the promise API, and objects were hard to identify after loading a prior snapshot. It is an example of the integration work a history system requires, not evidence that Fabric.js drawing is broken.</p>
<p>The engine records object additions and removals, pointer moves and transforms, and completed text edits. One user action becomes one history step. Your toolbar can ask whether a step is available:</p>
<p>In this snippet, <code>undoButton</code> and <code>redoButton</code> are your toolbar's button elements.</p>
<pre><code class="language-js">undoButton.disabled = !engine.canUndo();
redoButton.disabled = !engine.canRedo();

undoButton.addEventListener("click", () =&gt; void engine.undo());
redoButton.addEventListener("click", () =&gt; void engine.redo());
</code></pre>
<p><strong>A detail that matters:</strong> calling <code>object.set()</code> in your own code does not necessarily fire a Fabric event. If your toolbar changes several properties, group them yourself:</p>
<p>Here <code>card</code> is the Fabric object your editor is changing.</p>
<pre><code class="language-js">engine.transaction("Move and recolor card", () =&gt; {
  card.set({ left: 200, top: 120, fill: "#f97316" });
  card.setCoords();
  canvas.requestRenderAll();
});
</code></pre>
<p>That whole command becomes one labelled undo step. You can also make changes first and call <code>engine.commit("Recolor card")</code>. Read the <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/undo-redo">undo and redo guide</a> for the automatic events, keyboard shortcuts, and history limits.</p>
<h2>2. “Fabric.js object reference lost after undo or reload”</h2>
<p>An undo operation or JSON load often creates new JavaScript object instances. A variable that pointed to the old rectangle may no longer point to the rectangle now on the canvas. Fabric.js <a href="https://github.com/fabricjs/fabric.js/issues/6799">issue #6799</a> also shows how group-relative transforms make object state harder to reason about.</p>
<p>The engine assigns a stable ID to each object, including children inside groups. Save the ID in your application state. After a load or undo, find the current instance:</p>
<pre><code class="language-js">const id = card.id;

await engine.undo();

const currentCard = engine.getObjectById(id);
if (currentCard) {
  currentCard.set("fill", "#2563eb");
  canvas.requestRenderAll();
  engine.commit("Recolor card");
}
</code></pre>
<p>This solves <strong>identity across document operations</strong>. It does not calculate canvas-relative coordinates for a grouped object or fix every transform issue in #6799. For those, use Fabric.js transformation APIs. The engine's <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/document-format">document format</a> explains object IDs and order.</p>
<h2>3. “Fabric.js custom properties lost after JSON load”</h2>
<p>An editor often adds fields such as <code>label</code>, <code>databaseId</code>, or <code>lockedBy</code>. Fabric.js supports custom serialization, but you must configure it. The request in <a href="https://github.com/fabricjs/fabric.js/issues/10887">issue #10887</a> describes the repeated setup developers face when storing application metadata on objects. Its native <a href="https://fabricjs.com/docs/using-custom-properties/">custom properties guide</a> is also worth reading.</p>
<p>For a custom class, register its type and the properties that must survive a document round trip:</p>
<pre><code class="language-ts">import { Rect } from "fabric";
import { createDocumentEngine } from "fabricjs-document-engine";

class Sticker extends Rect {
  static type = "Sticker";
  declare label: string;
}

const engine = createDocumentEngine({
  canvas,
  customObjects: [
    { fabricClass: Sticker, properties: ["label"] },
  ],
});
</code></pre>
<p>Set <code>label</code> on a <code>Sticker</code>, save, and load it again. You do not have to remember to list that property on every <code>toJSON()</code> call. Register the class <strong>before</strong> you load a document that uses it. Otherwise, the load fails with <code>UNKNOWN_OBJECT_TYPE</code> instead of quietly changing it to another shape. See <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/custom-objects">custom objects</a>.</p>
<p>This feature is for Fabric classes and object fields. Put whole-document information such as a title or customer ID in document <code>metadata</code>, using <code>engine.updateMetadata({ title: "Summer poster" })</code>.</p>
<h2>4. “Fabric.js loadFromJSON failed” or “the old canvas disappeared”</h2>
<p>Loading is more than parsing JSON. A saved design can contain an unknown custom class, a malformed value, an expired image URL, or a newer schema. There may also be two loads in progress because the user quickly opened design A and then design B.</p>
<p>The engine validates and checks the incoming document before replacing the current canvas. An unknown type fails with <code>UNKNOWN_OBJECT_TYPE</code>. A missing image fails with <code>MISSING_ASSETS</code>. If B starts after A, A is cancelled with <code>LOAD_ABORTED</code>, so its late response cannot replace B. The <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-and-load">save and load guide</a> documents these rules.</p>
<pre><code class="language-js">try {
  await engine.load("poster-42");
  showMessage("Poster opened");
} catch (error) {
  if (error.code === "LOAD_ABORTED") return;
  if (error.code === "UNSAVED_CHANGES") {
    showMessage("Save or discard your current edits first");
    return;
  }
  showMessage(`Could not open the poster: ${error.message}`);
}
</code></pre>
<p>When storage is configured, the engine also refuses to replace unsaved work unless the user has chosen to discard it. Do not pass <code>discardUnsavedChanges: true</code> automatically. A failed load should leave the design people were editing on screen. The <a href="https://fabricjs-document-engine.jscrate.dev/docs/api/error-codes">error code reference</a> lists the cases your UI can handle.</p>
<h2>5. “Fabric.js autosave overwrites newer changes”</h2>
<p>Fabric.js does not decide when your app sends a document to a server. A common editor mistake is to fire a request after every change. Request A might start first but finish after request B, leaving older content in storage.</p>
<p>With a storage adapter, the engine keeps one save in flight. If edits happen during that save, it schedules one follow-up save with the latest content. <code>engine.isDirty()</code> and <code>engine.getSaveState()</code> let you show what is saved. Temporary failures can be retried. The exact timing and retry options are in the <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/autosave">autosave guide</a>.</p>
<pre><code class="language-js">engine.on("save:status", ({ status, isDirty, revision }) =&gt; {
  statusElement.textContent = `${status} · revision ${revision}`;
  saveButton.disabled = !isDirty;
});
</code></pre>
<p><strong>One tab is only half the problem.</strong> If another tab or device has saved the same document, a correct adapter compares revisions and reports <code>SAVE_CONFLICT</code>. The app can offer to reload, save a copy, or deliberately overwrite. Do not choose overwrite silently. The built-in adapters check revisions; your own backend must enforce the same rule at the database write. See <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-conflicts">save conflicts</a> and the <a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/custom-backend">custom backend example</a>.</p>
<h3>Put the same save rules on your backend</h3>
<p>For an account-based editor, write a storage adapter around your API. This TypeScript example assumes your server returns a document on <code>GET</code>, and <code>{ revision: number }</code> on a successful <code>PUT</code>:</p>
<pre><code class="language-ts">import type { DocumentStorage } from "fabricjs-document-engine";

const storage: DocumentStorage = {
  async loadDocument(id) {
    const response = await fetch(`/api/documents/${encodeURIComponent(id)}`);
    if (!response.ok) throw new Error(`Load failed: ${response.status}`);
    return response.json();
  },

  async saveDocument(document, { expectedRevision, signal }) {
    const response = await fetch(
      `/api/documents/${encodeURIComponent(document.id)}`,
      {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
          "If-Match": String(expectedRevision ?? "*"),
        },
        body: JSON.stringify(document),
        signal,
      },
    );

    if (response.status === 409) {
      throw Object.assign(new Error("Saved in another tab"), {
        code: "SAVE_CONFLICT",
      });
    }
    if (!response.ok) throw new Error(`Save failed: ${response.status}`);
    return response.json(); // { revision: number }
  },
};
</code></pre>
<p>Your server must check <code>expectedRevision</code> against the stored revision <strong>as part of the write</strong>, then return the new revision. A client-side comparison alone cannot stop two devices from racing. Add authorization for each document ID and decide how your UI presents a conflict. The package's <a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/custom-backend">backend guide</a> and <a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/verify-adapter">adapter verification guide</a> cover the full contract.</p>
<h2>6. “Fabric.js image missing after load” and “my font changed”</h2>
<p>The JSON can be valid while the resources it refers to are gone. An image may point to an expired signed URL. A browser-only <code>blob:</code> URL will disappear after refresh. A web font might not be ready when Fabric measures text.</p>
<p>The engine records an asset manifest with the image and font variants a document uses. It checks them before loading. <code>MISSING_ASSETS</code> includes the missing image URLs and object IDs; unavailable fonts can produce warnings or fail the load if you require them. You can resolve URLs, offer replacement images, load fonts, and upload temporary images through <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts">asset options</a>.</p>
<pre><code class="language-js">const report = await engine.checkAssets();
console.log(report.missingImages, report.unavailableFonts);

// If a stored image URL has moved:
await engine.replaceImage("/old-logo.png", "/new-logo.png");
</code></pre>
<p>The package can <strong>tell you what is missing</strong> and use a replacement you provide. It cannot revive a deleted image on your server. If designs need to open on other devices, upload browser-only images to durable storage and return a portable URL. For web fonts, make the font available and load it before you rely on its measured text layout.</p>
<h2>7. “Fabric.js canvas lost after refresh or crash”</h2>
<p>Autosave helps, but a tab can close while edits are still unsaved or a request is in flight. Optional recovery keeps a separate local checkpoint. It uses IndexedDB while the person edits and can offer a copy after refresh or interruption.</p>
<pre><code class="language-js">import { createIndexedDbRecovery } from "fabricjs-document-engine/recovery";

const engine = createDocumentEngine({
  canvas,
  storage,
  recovery: { store: createIndexedDbRecovery(), interval: 2000 },
});

const recoverable = await engine.getRecoverableDocuments();
if (recoverable.length &gt; 0 &amp;&amp; window.confirm("Restore unsaved work?")) {
  await engine.restoreRecovery(recoverable[0].documentId);
}
</code></pre>
<p>The confirmation demonstrates the choice. In a real editor, show a useful preview and let the user restore or discard a copy before replacing what is currently on screen. Recovery is local to the browser; it is not a server backup or multi-device sync. Private browsing and storage clearing can remove it. Read the <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/recovery">recovery guide</a> for checkpoint timing and temporary images.</p>
<h2>8. “Fabric.js tainted canvas cannot be exported”</h2>
<p>A Fabric.js user reported that exporting after <code>loadFromJSON</code> hit a tainted-canvas error in <a href="https://github.com/fabricjs/fabric.js/issues/5429">issue #5429</a>. This is a browser security rule when a remote image is drawn without the required cross-origin permission. The package cannot bypass that rule.</p>
<p>It can check before export and give you a useful problem list:</p>
<pre><code class="language-js">const check = await engine.preflightExport({ format: "png" });
if (!check.ok) {
  showProblems(check.problems);
} else {
  const result = await engine.export({
    format: "png",
    area: "content",
    scale: 2,
    background: "transparent",
  });
  // Download or upload result.blob.
}
</code></pre>
<p>The export API also supports JPEG, WebP, SVG, and editable JSON. You can choose the canvas, content, selection, or an explicit rectangle as the output area. A blocked raster export reports <code>EXPORT_BLOCKED</code>; successful export does not change the editable canvas or its undo state. See <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/export">export</a>.</p>
<p>To actually fix a cross-origin image, load it with appropriate <code>crossOrigin</code> settings <strong>and</strong> configure its server to send the correct <code>Access-Control-Allow-Origin</code> header. See the package <a href="https://fabricjs-document-engine.jscrate.dev/docs/production/troubleshooting">troubleshooting guide</a>. Switching packages will not remove the browser's CORS requirement.</p>
<h2>9. “How do I keep versions of a Fabric.js design?”</h2>
<p>Undo is for recent steps in the current editing session. A named version is a checkpoint you want to find next week. The engine can create, list, restore, and delete versions when its storage adapter supports them:</p>
<pre><code class="language-js">const version = await engine.createVersion("Approved by client");
const versions = await engine.listVersions();

// Later, after the user chooses to restore it:
await engine.restoreVersion(version.id);
</code></pre>
<p>Restoring keeps an automatic copy of the current state first, then opens the older content as a new unsaved revision. That is different from moving a history pointer backward and erasing later work. The built-in storage adapters support versions; a custom backend needs the version methods described in the <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/versions">versions guide</a>.</p>
<h2>10. “How do I open my old Fabric.js JSON?”</h2>
<p>You may already have designs saved with <code>canvas.toJSON()</code>. You do not have to throw them away to use a document format. The engine can import plain Fabric JSON from supported older versions and wrap it in a document:</p>
<pre><code class="language-js">await engine.importFabricJson(oldJsonText, {
  id: "imported-poster-42",
  metadata: { source: "previous editor" },
});
</code></pre>
<p>The format records a <code>schemaVersion</code>, canvas dimensions and background, objects in stacking order, assets, and document metadata. The <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/migration">migration guide</a> covers Fabric 5, 6, and 7 JSON and schema errors. Test your own saved fixtures, especially if your old app had custom objects or unusual properties. The <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/document-format">document format reference</a> lists the fields.</p>
<h2>11. “Can I safely open JSON uploaded by another user?”</h2>
<p>A JSON file is input from outside your app. It may have unknown classes, unsafe addresses, extreme nesting, or far more objects than your browser can handle. The engine validates the document, refuses unsafe content, and applies object-count and depth limits before loading. Its <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/security">security guide</a> explains the checks and configuration.</p>
<p>This is a useful boundary, but it does not replace your application's authentication, authorization, upload limits, or server-side validation. Only load documents that the current user is allowed to access. If you render remote assets, still control which hosts your product trusts.</p>
<h2>12. “Does it work with React, Next.js, Vue, or Svelte?”</h2>
<p>The core package is framework independent. React has optional hooks for engine creation and toolbar state. Next.js needs the Fabric canvas created in the browser, not during server rendering. Vue should keep a Fabric canvas out of deep reactive proxies. Svelte and plain JavaScript can subscribe to the state store.</p>
<p>The package documents each setup: <a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/react">React</a>, <a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/next-js">Next.js</a>, <a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/vue">Vue</a>, <a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/svelte">Svelte</a>, and <a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/vanilla-js">plain JavaScript</a>. These integrations connect the document engine to your app; they do not supply an editor UI.</p>
<h2>What the package does not solve</h2>
<p>A dependable article should say where the boundary is:</p>
<ul>
<li>It does not draw or style your toolbar, selection controls, or pages. Fabric.js and your app own the canvas UI.</li>
<li>It does not provide a hosted database, image bucket, or backend authentication.</li>
<li>It does not make a deleted remote image available again or bypass CORS.</li>
<li>It does not provide real-time multi-user collaboration or automatic merge of two people's changes. A save conflict needs a product decision.</li>
<li>It does not export PDF directly. Use its image or SVG output with a separate PDF library if needed.</li>
<li>It does not promise that any particular issue in the Fabric.js tracker is fixed in Fabric.js itself. The package addresses the document workflow described here.</li>
</ul>
<h2>How to decide if you need it</h2>
<p>If you only need to draw shapes and download a one-off image, Fabric.js may already be enough. If people will return to edit a saved design, ask three questions:</p>
<ol>
<li>Can I identify the same object after reopening and undoing?</li>
<li>Can a failed load or a slow save erase good work?</li>
<li>Can I tell the user exactly why an image, font, or export failed?</li>
</ol>
<p>If those matter, a document layer is worth considering. Start with the <a href="https://fabricjs-document-engine.jscrate.dev/docs/overview/quick-start">quick start</a>, run the <a href="https://fabricjs-document-engine.jscrate.dev/">live examples</a>, and test your own documents against the <a href="https://fabricjs-document-engine.jscrate.dev/docs/production/production-checklist">production checklist</a>. The code is on <a href="https://github.com/re-sohail/fabricjs-document-engine">GitHub</a>, and the released package is on <a href="https://www.npmjs.com/package/fabricjs-document-engine">npm</a>.</p>
<h3>Test the failure, not just the happy path</h3>
<p>Use a saved fixture that contains a group, a custom object, an image, and text in a web font. Then check these cases in your own app:</p>
<ol>
<li>Save, refresh, and compare object IDs, stacking order, background, and metadata.</li>
<li>Undo a drag and a toolbar-driven change. Confirm each takes one step and the current object can be found by ID.</li>
<li>Delay two document loads. Confirm the later choice stays on screen.</li>
<li>Break an image URL. Confirm the old canvas stays visible and the missing URL is reported.</li>
<li>Delay a save, edit again, and confirm the final stored document contains the latest edit.</li>
<li>Open the same document in two tabs. Save in one, then confirm the other shows a conflict.</li>
<li>Make an unsaved edit, refresh, and check the recovery choice.</li>
<li>Try a remote image without CORS. Confirm export preflight reports the blocker.</li>
</ol>
<p>These tests tell you whether the package and <strong>your storage, assets, and UI</strong> work together. A feature list alone cannot answer that.</p>
<h2>Original sources and further reading</h2>
<p><strong>Fabric.js problem reports and official API</strong></p>
<ul>
<li><a href="https://github.com/fabricjs/fabric.js/issues/10011">Fabric.js issue #10011: undo/redo history in Fabric 6</a></li>
<li><a href="https://github.com/fabricjs/fabric.js/issues/6799">Fabric.js issue #6799: object state inside groups</a></li>
<li><a href="https://github.com/fabricjs/fabric.js/issues/10887">Fabric.js issue #10887: application metadata and JSON serialization</a></li>
<li><a href="https://github.com/fabricjs/fabric.js/issues/5429">Fabric.js issue #5429: tainted canvas after JSON load</a></li>
<li><a href="https://fabricjs.com/api/classes/canvas/">Fabric.js Canvas API</a> and <a href="https://fabricjs.com/docs/using-custom-properties/">custom properties guide</a></li>
</ul>
<p><strong>Package implementation and documentation</strong></p>
<ul>
<li><a href="https://www.npmjs.com/package/fabricjs-document-engine">fabricjs-document-engine on npm</a> and <a href="https://github.com/re-sohail/fabricjs-document-engine">source repository</a></li>
<li><a href="https://fabricjs-document-engine.jscrate.dev/docs/api/document-engine">DocumentEngine API and error codes</a></li>
<li><a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-and-load">Save/load</a>, <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/undo-redo">undo/redo</a>, <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/autosave">autosave</a>, and <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts">assets/fonts</a></li>
<li><a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/recovery">Recovery</a>, <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/versions">versions</a>, <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/export">export</a>, and <a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/document-format">document format</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to Add a Production-Ready React Color Picker in 2026: Forms, Images, Eyedropper, Gradients & OKLCH]]></title><description><![CDATA[Adding a React color picker looks easy at first.
You install a component, pass it a color value, listen for changes, and you are done.
That works when all you need is a simple HEX color.
But real appl]]></description><link>https://chroma-panel.hashnode.dev/how-to-add-a-production-ready-react-color-picker-in-2026</link><guid isPermaLink="true">https://chroma-panel.hashnode.dev/how-to-add-a-production-ready-react-color-picker-in-2026</guid><dc:creator><![CDATA[Sohail Khan]]></dc:creator><pubDate>Thu, 24 Sep 2026 06:18:38 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/6ab0f13c1a95a677dadec704/7ed5f18c-c275-4472-991a-f766d007ea7e.png" alt="" style="display:block;margin:0 auto" />

<p>Adding a <strong>React color picker</strong> looks easy at first.</p>
<p>You install a component, pass it a color value, listen for changes, and you are done.</p>
<p>That works when all you need is a simple HEX color.</p>
<p>But real applications usually need more.</p>
<p>A settings page may need a color input that works with forms. A theme editor needs RGB and HSL controls. A design tool may need palettes, an eyedropper, gradients, or image color sampling. A modern design system may also need OKLCH and Display P3 instead of only HEX and RGB.</p>
<p>That is where a basic color input starts becoming a complete color workflow.</p>
<p>In this guide, I’ll show how to add a production-ready React color picker using <a href="https://chroma-panel.jscrate.dev/">ChromaPanel</a> and gradually add the features that a real application may need.</p>
<p>ChromaPanel is an <a href="https://github.com/re-sohail/chroma-panel">open-source React color picker</a> with five picking modes, image sampling, an eyedropper, gradients, modern CSS color support, accessibility, native form behavior, TypeScript support, and zero runtime dependencies.</p>
<p>You can install it from <a href="https://www.npmjs.com/package/chroma-panel">npm</a> and start with only a few lines of code.</p>
<h2>Install the React color picker</h2>
<p>Start by installing the package:</p>
<pre><code class="language-bash">npm install chroma-panel
</code></pre>
<p>Then import <code>ColorInput</code>.</p>
<pre><code class="language-tsx">import { ColorInput } from "chroma-panel";

export function BrandColor() {
  return &lt;ColorInput defaultValue="#3366cc" /&gt;;
}
</code></pre>
<p>That gives you a working <a href="https://chroma-panel.jscrate.dev/react/components/color-input">React color input</a>.</p>
<p>The user sees a color swatch. Clicking it opens the picker.</p>
<p>There is no provider to wrap around your application and no separate stylesheet required for the standard setup.</p>
<p>For a settings form or a simple customization page, this may already be enough.</p>
<h2>Use a controlled React color picker</h2>
<p>Most React applications eventually need the selected color somewhere else.</p>
<p>Maybe you want to update a preview, save a theme, change a button color, or store the value in your API.</p>
<p>In that case, use the normal controlled React pattern.</p>
<pre><code class="language-tsx">import { useState } from "react";
import { ColorInput } from "chroma-panel";

export function ThemeColor() {
  const [color, setColor] = useState("#7c3aed");

  return (
    &lt;ColorInput
      value={color}
      onChange={(result) =&gt; setColor(result.hex)}
      aria-label="Theme color"
    /&gt;
  );
}
</code></pre>
<p>ChromaPanel supports both <a href="https://chroma-panel.jscrate.dev/react/handbook/controlled">controlled and uncontrolled usage</a>.</p>
<p>One useful detail is that <code>onChange</code> and <code>onChangeComplete</code> solve different problems.</p>
<p><code>onChange</code> runs continuously while the user is changing the color.</p>
<p><code>onChangeComplete</code> runs once when that interaction finishes.</p>
<pre><code class="language-tsx">&lt;ColorInput
  value={color}
  onChange={(result) =&gt; setColor(result.hex)}
  onChangeComplete={(result) =&gt; saveTheme(result.hex)}
/&gt;
</code></pre>
<p>That means you can update your UI instantly without making a database request on every tiny movement.</p>
<p>Use <code>onChange</code> for the live experience.</p>
<p>Use <code>onChangeComplete</code> for saving, network calls, history entries, or other expensive work.</p>
<h2>Make the React color picker work with forms</h2>
<p>This is one feature that often gets ignored when developers add a color picker.</p>
<p>A color picker is still an input.</p>
<p>If it is sitting inside a profile form, settings form, onboarding page, or admin panel, it should work with the rest of that form.</p>
<p>ChromaPanel's <code>ColorInput</code> can behave like a normal form field.</p>
<pre><code class="language-tsx">&lt;form action={saveSettings}&gt;
  &lt;label htmlFor="brand-color"&gt;
    Brand color
  &lt;/label&gt;

  &lt;ColorInput
    id="brand-color"
    name="brandColor"
    defaultValue="#2563eb"
    format="hex"
    required
  /&gt;

  &lt;button type="submit"&gt;
    Save settings
  &lt;/button&gt;
&lt;/form&gt;
</code></pre>
<p>The value can be submitted with the surrounding form.</p>
<p>It also supports things such as <code>required</code>, <code>disabled</code>, labels, form reset behavior, and different output formats.</p>
<p>You can read the full <a href="https://chroma-panel.jscrate.dev/react/handbook/forms">React color picker form guide</a> if your application relies heavily on native forms or React form libraries.</p>
<p>This is useful for:</p>
<p>settings pages, theme configuration, SaaS dashboards, account customization, CMS interfaces, admin panels, and onboarding flows.</p>
<h2>Show the full color picker inline</h2>
<p>A popover works well when color is only one field in a larger form.</p>
<p>A design tool is different.</p>
<p>If users are working with color constantly, you probably want the panel to stay visible.</p>
<p>Use <code>ChromaPanel</code> directly.</p>
<pre><code class="language-tsx">import { ChromaPanel } from "chroma-panel";

export function ThemeEditor() {
  return (
    &lt;ChromaPanel
      defaultValue="#3366cc"
      modes={["wheel", "sliders", "palettes"]}
      showTitleBar={false}
    /&gt;
  );
}
</code></pre>
<p>The <a href="https://chroma-panel.jscrate.dev/react/components/chroma-panel">ChromaPanel component</a> gives you the same color system without hiding it inside a popover.</p>
<p>That works well for theme editors, website builders, design tools, brand editors, graphics applications, product customizers, and other interfaces where color is an important part of the workflow.</p>
<h2>Choose the color picker modes you actually need</h2>
<p>Not every user chooses color the same way.</p>
<p>Someone may want to explore visually.</p>
<p>Someone else knows the exact RGB value.</p>
<p>Another user only wants to select from approved brand colors.</p>
<p>ChromaPanel provides five different ways to choose a color.</p>
<p>The <a href="https://chroma-panel.jscrate.dev/react/modes/wheel">color wheel</a> gives users a visual way to explore hue and saturation.</p>
<p>The <a href="https://chroma-panel.jscrate.dev/react/modes/sliders">RGB, HSL and HSB sliders</a> provide precise channel-level control.</p>
<p>The <a href="https://chroma-panel.jscrate.dev/react/modes/palettes">palette mode</a> provides searchable predefined colors and can also use your own brand palette.</p>
<p>The <a href="https://chroma-panel.jscrate.dev/react/modes/pencils">pencils mode</a> provides a large 120-color swatch grid.</p>
<p>The <a href="https://chroma-panel.jscrate.dev/react/modes/image">image color picker</a> lets users select colors from uploaded images.</p>
<p>You decide which ones should appear.</p>
<pre><code class="language-tsx">&lt;ChromaPanel
  modes={[
    "wheel",
    "sliders",
    "palettes",
  ]}
/&gt;
</code></pre>
<p>A simple settings page may only need the wheel.</p>
<p>A design tool may need all of them.</p>
<p>A company branding screen may only need an approved palette.</p>
<p>That is better than forcing every user to work with the same large interface.</p>
<h2>Add an image color picker to React</h2>
<p>This is where color picking becomes much more useful for creative applications.</p>
<p>Imagine a user uploads a logo.</p>
<p>They want the application theme to match that logo.</p>
<p>Instead of making them manually find each HEX value, the application can extract the dominant colors automatically.</p>
<p>ChromaPanel's <a href="https://chroma-panel.jscrate.dev/react/modes/image">image mode</a> allows the user to drop, paste, or select an image.</p>
<p>The picker can extract dominant colors and show them as swatches.</p>
<p>The user can also sample an exact pixel.</p>
<pre><code class="language-tsx">&lt;ChromaPanel
  modes={["image"]}
  defaultValue="#3366cc"
/&gt;
</code></pre>
<p>You can also use the image API without showing the full React color picker.</p>
<pre><code class="language-tsx">import { extractPalette } from "chroma-panel";

const { swatches } = await extractPalette(file, {
  maxColors: 8,
});
</code></pre>
<p>That opens up other use cases.</p>
<p>You could generate a theme from a company logo.</p>
<p>You could create a palette from a product photograph.</p>
<p>You could extract avatar colors.</p>
<p>You could build image editing tools.</p>
<p>You could automatically suggest colors after a user uploads an asset.</p>
<p>The image sampler uses a bounded, downscaled sampling surface, and a separate worker entry point is available when you want heavier processing away from the main thread.</p>
<h2>Add an eyedropper to your React color picker</h2>
<p>Sometimes the color already exists somewhere on the screen.</p>
<p>The user simply wants to grab it.</p>
<p>ChromaPanel includes <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper">eyedropper support</a> through the browser EyeDropper API.</p>
<p>Where the browser supports it, the user can choose a color from somewhere else on their screen.</p>
<p>This works especially well in:</p>
<p>design tools, website builders, theme editors, graphics applications, and brand customization interfaces.</p>
<p>You can use the eyedropper through the normal picker interface or use the <code>useEyedropper</code> hook when you want to create your own button.</p>
<p>Eyedropper support has also become a common feature in newer React color-picker components, which shows that developers increasingly expect precise screen sampling in advanced color workflows.</p>
<h2>Add a gradient color picker</h2>
<p>Solid colors are enough for many forms.</p>
<p>They are not enough for every design tool.</p>
<p>For gradients, ChromaPanel provides a separate <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor">GradientEditor</a>.</p>
<pre><code class="language-tsx">import {
  GradientEditor,
  gradientToCss,
} from "chroma-panel/gradient";
</code></pre>
<p>It supports linear and radial gradients.</p>
<p>The gradient editor is separate from the normal color-picker entry point, so an application that only needs solid colors does not need to use the gradient functionality.</p>
<p>Keeping these features separate is useful because a production React color picker should not force every feature into every screen.</p>
<p>Gradient color pickers remain their own active search category in the React ecosystem, especially for design and customization tools.</p>
<h2>Work with OKLCH and modern CSS colors</h2>
<p>A few years ago, most web color interfaces stopped at:</p>
<p>HEX, RGB, HSL, and HSV.</p>
<p>Modern CSS has moved further.</p>
<p>OKLCH, OKLab, Lab, LCH, and Display P3 are increasingly useful in design systems and modern frontend work.</p>
<p>ChromaPanel includes <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4">CSS Color 4 utilities</a> for those workflows.</p>
<pre><code class="language-tsx">import {
  parseColor,
  isInGamut,
  mapToGamut,
  serializeColor,
} from "chroma-panel/color";

const color = parseColor(
  "oklch(72% 0.18 250)"
);

if (color &amp;&amp; !isInGamut(color, "srgb")) {
  const fallback = mapToGamut(
    color,
    "srgb"
  );

  console.log(
    serializeColor(fallback)
  );
}
</code></pre>
<p>These utilities can parse, convert, map, and serialize modern CSS colors.</p>
<p>They do not require the React UI.</p>
<p>That means the color engine can also be useful elsewhere in your application.</p>
<p>OKLCH is becoming a much more visible search topic around modern frontend color tooling, with current color tools and React packages specifically targeting OKLCH, gamut handling, Display P3, and modern design-system workflows.</p>
<h2>Accessibility should be part of the color picker</h2>
<p>A color picker is naturally visual.</p>
<p>That makes accessibility especially important.</p>
<p>Users should not need a mouse just to change a color.</p>
<p>ChromaPanel's <a href="https://chroma-panel.jscrate.dev/react/overview/accessibility">accessibility implementation</a> includes keyboard-operable controls and screen-reader behavior.</p>
<p>Color channels use real range inputs.</p>
<p>Keyboard users can change values with normal input behavior.</p>
<p>Focus is managed when the popover opens and closes.</p>
<p>Swatches have accessible names.</p>
<p>The component also accounts for reduced-motion and forced-color preferences.</p>
<p>Accessibility is also a common selling point in current React color-picker packages, so it is worth treating it as part of the component architecture instead of something added later.</p>
<h2>Use ChromaPanel with Next.js</h2>
<p>A modern React package also needs to work inside server-rendered applications.</p>
<p>ChromaPanel documents <a href="https://chroma-panel.jscrate.dev/react/frameworks/next-js">Next.js and server-rendering support</a>.</p>
<p>Browser-dependent parts are separated so the package can be used safely in applications using React server rendering.</p>
<p>If the picker lives inside an interactive Next.js component, you can use it from a client component as expected.</p>
<pre><code class="language-tsx">"use client";

import { ColorInput } from "chroma-panel";

export function BrandColorPicker() {
  return (
    &lt;ColorInput
      defaultValue="#3366cc"
    /&gt;
  );
}
</code></pre>
<p>This makes <strong>Next.js color picker</strong> another useful search path for developers who are not simply looking for a generic React component.</p>
<h2>Use it with Tailwind CSS and shadcn/ui</h2>
<p>Many React applications now build their UI around Tailwind CSS or shadcn/ui.</p>
<p>ChromaPanel has a <a href="https://chroma-panel.jscrate.dev/react/handbook/tailwind">Tailwind CSS styling guide</a> and a dedicated <a href="https://chroma-panel.jscrate.dev/react/integrations/shadcn-ui">shadcn/ui integration guide</a>.</p>
<p>You are not required to install shadcn, Radix, or another UI library.</p>
<p>But if your application already uses them, you can make your existing Popover, Dialog, or Sheet own the surrounding interface and render ChromaPanel inside.</p>
<p>That matters because <strong>shadcn color picker</strong> is now a meaningful search category of its own. Current component libraries are publishing color-picker recipes specifically for shadcn and Next.js projects.</p>
<h2>Keep your React color picker bundle under control</h2>
<p>More features usually mean more JavaScript.</p>
<p>That is why entry points matter.</p>
<p>The full ChromaPanel configuration with all five modes is measured at about <strong>23.1 kB gzipped</strong> in the project's Vite production measurement.</p>
<p>The panel shell plus one mode is around <strong>15 kB gzipped</strong>.</p>
<p>If you only need one or two modes, import the panel shell and those modes explicitly.</p>
<pre><code class="language-tsx">import { ChromaPanel } from "chroma-panel/panel";
import { wheelMode } from "chroma-panel/modes";

export function SmallPicker() {
  return (
    &lt;ChromaPanel
      modes={[wheelMode]}
    /&gt;
  );
}
</code></pre>
<p>You can read more about <a href="https://chroma-panel.jscrate.dev/react/utils/entry-points">ChromaPanel entry points and tree shaking</a>.</p>
<p>This is especially useful when you need the ChromaPanel API but not image picking, palettes, pencils, and every other mode.</p>
<h2>When a smaller React color picker is better</h2>
<p>More functionality is not automatically better.</p>
<p>If your requirement is simply:</p>
<p>“Let the user choose one HEX color.”</p>
<p>then you may not need image sampling, gradients, OKLCH, an eyedropper, palettes, or a complete color engine.</p>
<p>A smaller React color picker may be the better choice.</p>
<p>For example, <code>react-colorful</code> specifically focuses on providing a small, accessible, zero-dependency core picker. Its current npm documentation describes the picker as about 3.1 kB gzipped.</p>
<p>ChromaPanel makes more sense when your application needs the wider workflow.</p>
<p>The right question is not:</p>
<p>“Which React color picker has the most features?”</p>
<p>It is:</p>
<p>“What will users actually need to do with color in this application?”</p>
<h2>A simple React color picker can grow with your app</h2>
<p>You may start here:</p>
<pre><code class="language-tsx">&lt;ColorInput
  defaultValue="#3366cc"
/&gt;
</code></pre>
<p>Six months later, the same product may need:</p>
<p>a controlled color value, brand palettes, image sampling, recent colors, an eyedropper, precise RGB values, gradients, contrast checks, OKLCH, Tailwind tokens, and accessible keyboard controls.</p>
<p>Replacing your color system halfway through a product can create unnecessary work.</p>
<p>The idea behind <a href="https://chroma-panel.jscrate.dev/">ChromaPanel</a> is that you can start small and add those capabilities only when you need them.</p>
<p>The package currently provides five color-selection modes, native forms, TypeScript, CSS Color 4 utilities, gradient editing, image sampling, contrast tools, exports, and zero runtime dependencies.</p>
<h2>Frequently asked questions</h2>
<h3>What is a good React color picker for TypeScript?</h3>
<p>ChromaPanel includes TypeScript types directly in the package, so you do not need a separate <code>@types</code> package.</p>
<p>You can see the <a href="https://chroma-panel.jscrate.dev/react/handbook/typescript">TypeScript documentation</a> for more details.</p>
<h3>Can I use a React color picker with Next.js?</h3>
<p>Yes. ChromaPanel includes documentation for <a href="https://chroma-panel.jscrate.dev/react/frameworks/next-js">Next.js and server rendering</a>.</p>
<p>Interactive picker components should be used from the client side of a Next.js application.</p>
<h3>Is there a React color picker with an eyedropper?</h3>
<p>Yes. ChromaPanel includes <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper">eyedropper support</a> where the browser EyeDropper API is available.</p>
<h3>Can React pick colors from an image?</h3>
<p>Yes. ChromaPanel's <a href="https://chroma-panel.jscrate.dev/react/modes/image">image color picker</a> can extract dominant colors from an image and sample an exact pixel.</p>
<p>The <code>extractPalette</code> utility can also be used independently.</p>
<h3>Is there a React gradient color picker?</h3>
<p>ChromaPanel provides a separate <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor">GradientEditor</a> for linear and radial gradient workflows.</p>
<h3>Can I use OKLCH in a React color picker?</h3>
<p>ChromaPanel's <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4">CSS Color 4 tools</a> support OKLCH, OKLab, Lab, LCH, sRGB, and Display P3 workflows.</p>
<h3>Does ChromaPanel work with Tailwind CSS?</h3>
<p>Yes. There is a dedicated <a href="https://chroma-panel.jscrate.dev/react/handbook/tailwind">Tailwind CSS guide</a>.</p>
<h3>Can I use ChromaPanel with shadcn/ui?</h3>
<p>Yes. The <a href="https://chroma-panel.jscrate.dev/react/integrations/shadcn-ui">shadcn/ui integration guide</a> shows how to use ChromaPanel with an existing shadcn interface.</p>
<h2>Final thoughts</h2>
<p>A <strong>React color picker</strong> can be a tiny control or a complete part of your product's design workflow.</p>
<p>For a basic HEX field, keep things simple.</p>
<p>But when users need precise values, palettes, an eyedropper, image sampling, gradients, accessibility, forms, modern CSS color spaces, or design-token workflows, building every part separately can become a much bigger task.</p>
<p><a href="https://chroma-panel.jscrate.dev/">ChromaPanel</a> brings those pieces together behind one React API while still letting you import only the parts you need.</p>
<p>If you want to try it:</p>
<p><strong>Documentation:</strong> <a href="https://chroma-panel.jscrate.dev/">chroma-panel.jscrate.dev</a></p>
<p><strong>npm:</strong> <a href="https://www.npmjs.com/package/chroma-panel">chroma-panel</a></p>
<p><strong>GitHub:</strong> <a href="https://github.com/re-sohail/chroma-panel">re-sohail/chroma-panel</a></p>
<p>The project is open source and released under the MIT license.</p>
<p>If you build something with it, find an issue, or have an idea for improving the React color picker experience, contributions and feedback are welcome.</p>
]]></content:encoded></item><item><title><![CDATA[ChromaPanel vs Other React Color Pickers: Features, Bundle Size, Accessibility, and More]]></title><description><![CDATA[Choosing a React color picker sounds simple until you start looking at what your application actually needs.
Maybe you only need a small HEX color picker.
Or maybe you need RGB and HSL controls, prede]]></description><link>https://chroma-panel.hashnode.dev/chromapanel-vs-other-react-color-pickers-features-bundle-size-accessibility-and-more</link><guid isPermaLink="true">https://chroma-panel.hashnode.dev/chromapanel-vs-other-react-color-pickers-features-bundle-size-accessibility-and-more</guid><category><![CDATA[React]]></category><category><![CDATA[React]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[chroma-panel]]></category><category><![CDATA[react color picker]]></category><category><![CDATA[color picker]]></category><dc:creator><![CDATA[Sohail Khan]]></dc:creator><pubDate>Wed, 23 Sep 2026 06:52:37 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/6ab0f13c1a95a677dadec704/cf95d74a-643c-42f4-aa44-1993936a4413.png" alt="Screenshot 2026-09-23 at 11.48.39 AM" style="display:block;margin:0 auto" />

<p>Choosing a <strong>React color picker</strong> sounds simple until you start looking at what your application actually needs.</p>
<p>Maybe you only need a small HEX color picker.</p>
<p>Or maybe you need <a href="https://chroma-panel.jscrate.dev/react/modes/sliders">RGB and HSL controls</a>, predefined <a href="https://chroma-panel.jscrate.dev/react/modes/palettes">palettes</a>, an <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper">eyedropper</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/image">image color picking</a>, <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor">gradients</a>, <a href="https://chroma-panel.jscrate.dev/react/overview/accessibility">accessibility</a>, <a href="https://chroma-panel.jscrate.dev/react/handbook/forms">form support</a>, and <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4">modern CSS colors like OKLCH</a>.</p>
<p>Those are very different requirements.</p>
<p>There are several popular React color picker libraries available today, but they are not trying to solve exactly the same problem.</p>
<p>In this comparison, I’m looking at four options:</p>
<p><a href="https://www.npmjs.com/package/chroma-panel"><strong>ChromaPanel</strong></a>, <code>react-colorful</code>, <code>react-color</code>, and <code>@uiw/react-color</code>.</p>
<p>The goal is not to say that one package should be used everywhere. Instead, I want to show what each React color picker provides, where the differences matter, and which kind of project each one fits.</p>
<h2>React color picker comparison at a glance</h2>
<p>The package and bundle figures below are based on the <a href="https://chroma-panel.jscrate.dev/react/overview/comparison">ChromaPanel comparison audit</a> and the current <a href="https://chroma-panel.jscrate.dev/">package documentation</a>. Bundle figures refer to the package main entry measured by <a href="https://bundlephobia.com/">Bundlephobia</a>, which is different from the amount that a tree-shaken application may actually ship.</p>
<table>
<thead>
<tr>
<th></th>
<th><a href="https://www.npmjs.com/package/chroma-panel"><strong>ChromaPanel</strong></a></th>
<th><strong>react-colorful</strong></th>
<th><strong>react-color</strong></th>
<th><strong>@uiw/react-color</strong></th>
</tr>
</thead>
<tbody><tr>
<td>TypeScript</td>
<td>Included</td>
<td>Included</td>
<td>Separate types package</td>
<td>Included</td>
</tr>
<tr>
<td>Runtime dependencies</td>
<td>0</td>
<td>0</td>
<td>7</td>
<td>20 in main package</td>
</tr>
<tr>
<td>Main-entry gzip</td>
<td>~22 kB</td>
<td>~4.9 kB</td>
<td>~38.4 kB</td>
<td>~16 kB</td>
</tr>
<tr>
<td>Color wheel</td>
<td>Yes</td>
<td>Not</td>
<td>Not</td>
<td>Yes</td>
</tr>
<tr>
<td>RGB/HSL channel sliders</td>
<td>Yes</td>
<td>Limited</td>
<td>Limited</td>
<td>Limited</td>
</tr>
<tr>
<td>Alpha</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Palettes/swatches</td>
<td>Yes</td>
<td>Recipe</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Image color picker</td>
<td>Yes</td>
<td>Not</td>
<td>Not</td>
<td>Not</td>
</tr>
<tr>
<td>Eyedropper</td>
<td>Yes</td>
<td>Not</td>
<td>Not</td>
<td>Yes in Chrome picker</td>
</tr>
<tr>
<td>Gradient editor</td>
<td>Yes</td>
<td>Not</td>
<td>Not</td>
<td>Not</td>
</tr>
<tr>
<td>Modern CSS colors</td>
<td>Yes</td>
<td>Not</td>
<td>Not</td>
<td>Not</td>
</tr>
<tr>
<td>Form-ready input</td>
<td>Yes</td>
<td>Build yourself</td>
<td>Not</td>
<td>Not</td>
</tr>
<tr>
<td>Accessibility documented</td>
<td>Yes</td>
<td>Yes</td>
<td>Not</td>
<td>Limited documentation</td>
</tr>
<tr>
<td>Contrast tools</td>
<td>Yes</td>
<td>Not</td>
<td>Not</td>
<td>Not</td>
</tr>
</tbody></table>
<p>The differences become more useful when we look at what those features actually mean inside a real React application.</p>
<h1>1. ChromaPanel</h1>
<p><a href="https://chroma-panel.jscrate.dev/">Chroma Panel</a> is the most feature-focused option in this comparison.</p>
<p>Instead of providing only one saturation area and a hue slider, it treats color selection as a larger UI problem.</p>
<p>You can use it as a simple <a href="https://chroma-panel.jscrate.dev/react/components/color-input">React color input</a>, but the same package can grow into a much more complete color system.</p>
<pre><code class="language-tsx">import { ColorInput } from "chroma-panel";

export function BrandColor() {
  return (
    &lt;ColorInput
      defaultValue="#3366cc"
      aria-label="Brand color"
    /&gt;
  );
}
</code></pre>
<p>That is the basic version.</p>
<p>Behind it, <a href="https://github.com/re-sohail/chroma-panel">ChromaPanel</a> provides five different color-selection modes: <a href="https://chroma-panel.jscrate.dev/react/modes/wheel">wheel</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/sliders">sliders</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/palettes">palettes</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/pencils">pencils</a>, and <a href="https://chroma-panel.jscrate.dev/react/modes/image">image picking</a>. It also includes an <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper">eyedropper</a>, <a href="https://chroma-panel.jscrate.dev/react/handbook/forms">native form behavior</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4">CSS Color 4 utilities</a>, <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor">gradients</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/contrast">contrast tools</a>, <a href="https://chroma-panel.jscrate.dev/react/handbook/typescript">TypeScript types</a>, and <a href="https://www.npmjs.com/package/chroma-panel">zero runtime dependencies</a>.</p>
<h2>Five different ways to pick a color</h2>
<p>A major difference is that ChromaPanel does not force every user into the same color picker interface.</p>
<p>The <a href="https://chroma-panel.jscrate.dev/react/modes/wheel">wheel</a> is useful for visual color exploration.</p>
<p>The <a href="https://chroma-panel.jscrate.dev/react/modes/sliders">sliders</a> provide precise RGB, HSL, and HSB controls.</p>
<p><a href="https://chroma-panel.jscrate.dev/react/modes/palettes">Palettes</a> work well when your product has approved or predefined colors.</p>
<p><a href="https://chroma-panel.jscrate.dev/react/modes/pencils">Pencils</a> provide a large 120-color swatch grid.</p>
<p><a href="https://chroma-panel.jscrate.dev/react/modes/image">Image mode</a> lets someone choose colors directly from a photo or image.</p>
<p>You can show all of them or choose only the modes your UI needs.</p>
<pre><code class="language-tsx">import { ChromaPanel } from "chroma-panel";

export function ThemeColorPicker() {
  return (
    &lt;ChromaPanel
      defaultValue="#7c3aed"
      modes={["wheel", "sliders", "palettes"]}
    /&gt;
  );
}
</code></pre>
<p>This makes ChromaPanel useful when the same application has different color workflows.</p>
<p>For example, a settings form might use <a href="https://chroma-panel.jscrate.dev/react/components/color-input"><code>ColorInput</code></a>, while a design editor can show the <a href="https://chroma-panel.jscrate.dev/react/components/chroma-panel">complete panel</a>.</p>
<h2>Pick colors from images</h2>
<p><a href="https://chroma-panel.jscrate.dev/react/modes/image">Image color picking</a> is one of the bigger differences between ChromaPanel and the other React color picker libraries in this comparison.</p>
<p>A user can provide an image and pick an exact color from it.</p>
<p>ChromaPanel can also extract a palette from the image.</p>
<p>That can be useful when someone uploads a logo, product image, photograph, or brand asset and wants to create matching colors.</p>
<p>The other three libraries in this comparison do not currently document image color sampling as a built-in feature.</p>
<h2>Eyedropper support</h2>
<p>ChromaPanel supports the browser <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper"><code>EyeDropper</code> API</a>.</p>
<p>When the browser supports it, the user can pick a color from elsewhere on their screen.</p>
<p>This works particularly well for design tools and theme editors.</p>
<p>There is an important limitation: the native EyeDropper API is browser-dependent, so this functionality is mainly available in Chromium-based browsers.</p>
<p><a href="https://github.com/uiwjs/react-color"><code>@uiw/react-color</code></a> also exposes eyedropper functionality in its Chrome-style picker. Its own API includes a <code>showEyeDropper</code> option.</p>
<h2>Modern CSS colors</h2>
<p>This is an area where ChromaPanel takes a different approach from most traditional React color pickers.</p>
<p>It includes <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4">utilities for modern color spaces</a> such as:</p>
<p>OKLCH, OKLab, Lab, LCH, Display P3, and sRGB.</p>
<p>That matters more now that modern CSS applications are increasingly using colors beyond the traditional HEX, RGB, and HSL formats.</p>
<p>You can parse a modern CSS color, check whether it fits inside a target gamut, map it when necessary, and serialize it again.</p>
<pre><code class="language-tsx">import {
  parseColor,
  isInGamut,
  mapToGamut,
  serializeColor,
} from "chroma-panel/color";

const color = parseColor("oklch(72% 0.18 250)");

if (color &amp;&amp; !isInGamut(color, "srgb")) {
  const fallback = mapToGamut(color, "srgb");

  console.log(serializeColor(fallback));
}
</code></pre>
<p>The other React color pickers compared here do not currently document equivalent built-in <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4">CSS Color 4 support</a>.</p>
<h2>Gradient editing</h2>
<p>ChromaPanel also provides a separate <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor">gradient editor</a>.</p>
<pre><code class="language-tsx">import {
  GradientEditor,
  gradientToCss,
} from "chroma-panel/gradient";
</code></pre>
<p>You can work with linear and radial gradients while keeping that code separate from the basic color picker.</p>
<p>That separation is useful because projects that only need solid colors do not need to use the gradient functionality.</p>
<h2>Form support</h2>
<p>This feature is easy to overlook.</p>
<p>A color picker often ends up inside a <a href="https://chroma-panel.jscrate.dev/react/handbook/forms">form</a>.</p>
<p>ChromaPanel's <a href="https://chroma-panel.jscrate.dev/react/components/color-input"><code>ColorInput</code></a> behaves more like a real form field.</p>
<pre><code class="language-tsx">&lt;form&gt;
  &lt;ColorInput
    name="brandColor"
    defaultValue="#2563eb"
    required
  /&gt;

  &lt;button type="submit"&gt;Save&lt;/button&gt;
&lt;/form&gt;
</code></pre>
<p>The selected value can participate in form submission, native validation, and form reset behavior.</p>
<p>With smaller color picker libraries, you will often connect the picker to your form state yourself.</p>
<p>Neither approach is wrong, but having the behavior built in removes some repeated application code.</p>
<h2>Accessibility</h2>
<p>Color pickers are visual components, but they should not require a mouse.</p>
<p>ChromaPanel documents <a href="https://chroma-panel.jscrate.dev/react/overview/accessibility">keyboard and screen-reader behavior</a> and uses native range inputs for color axes.</p>
<p><a href="https://github.com/omgovich/react-colorful"><code>react-colorful</code></a> also specifically documents accessibility and says it follows WAI-ARIA guidelines.</p>
<p>That makes these two worth looking at closely when accessibility is an important requirement.</p>
<h2>Contrast checking</h2>
<p>ChromaPanel also includes <a href="https://chroma-panel.jscrate.dev/react/utils/contrast">contrast-related utilities</a>.</p>
<p>The documentation covers WCAG contrast, alpha-aware checks, APCA, and color suggestions.</p>
<p>This means the color picker can be connected to workflows such as choosing a background color and immediately checking whether text will remain readable.</p>
<p>That is particularly useful for theme builders and design systems.</p>
<h1>2. react-colorful</h1>
<p><a href="https://www.npmjs.com/package/react-colorful"><code>react-colorful</code></a> takes almost the opposite approach.</p>
<p>Its biggest strength is simplicity and size.</p>
<p>The <a href="https://github.com/omgovich/react-colorful">package</a> describes itself as a tiny React and Preact color picker. It has zero dependencies, includes TypeScript types, supports accessibility, and provides multiple picker components for HEX, RGB, HSL, HSV, and their alpha variants.</p>
<p>A basic example is very small:</p>
<pre><code class="language-tsx">import { HexColorPicker } from "react-colorful";

function Picker() {
  const [color, setColor] = useState("#aabbcc");

  return (
    &lt;HexColorPicker
      color={color}
      onChange={setColor}
    /&gt;
  );
}
</code></pre>
<p>Its <a href="https://github.com/omgovich/react-colorful">README</a> quotes roughly <strong>3.1 kB gzipped for a picker</strong>, while the <a href="https://chroma-panel.jscrate.dev/react/overview/comparison">comparison audit</a> measured about <strong>4.9 kB for the package main entry</strong>. Those numbers measure different things, so they should not be treated as a contradiction.</p>
<p>This is one of the main reasons <code>react-colorful</code> is so attractive.</p>
<p>You get a focused React color picker without bringing in a large feature set.</p>
<p>It supports saturation, hue, alpha, several color formats, touch devices, accessibility, and TypeScript.</p>
<p>What it does not try to provide is equally important.</p>
<p>Things such as palettes and a popover are shown as recipes rather than being the main responsibility of the package. Image sampling, gradients, modern CSS color spaces, and an eyedropper are not documented as built-in features.</p>
<p>So if you need a compact color control and you are happy building the surrounding UI yourself, <a href="https://www.npmjs.com/package/react-colorful"><code>react-colorful</code></a> remains a very strong option.</p>
<h1>3. react-color</h1>
<p><a href="https://www.npmjs.com/package/react-color"><code>react-color</code></a> follows a different idea again.</p>
<p>Instead of concentrating on one small picker, it provides <strong>13 different picker designs</strong>, including Sketch, Photoshop, Chrome, Material, Circle, Block, Twitter, GitHub, and others.</p>
<p>A typical example looks like this:</p>
<pre><code class="language-tsx">import { SketchPicker } from "react-color";

function Picker() {
  return &lt;SketchPicker /&gt;;
}
</code></pre>
<p>That can be useful when you want an interface that already looks familiar.</p>
<p>Instead of designing a complete color-picker layout yourself, you can choose one of the existing styles.</p>
<p>The trade-off is that <a href="https://github.com/casesandberg/react-color"><code>react-color</code></a> is an older package.</p>
<p><a href="https://www.npmjs.com/package/react-color">npm</a> currently lists version 2.19.3, and that release was published years ago. Its TypeScript definitions are provided separately, and the <a href="https://chroma-panel.jscrate.dev/react/overview/comparison">comparison audit</a> records seven runtime dependencies and a larger main-entry bundle than the other packages shown here.</p>
<p>That does not make the package unusable.</p>
<p>It simply means maintenance history and architecture are worth considering before introducing it into a new React project.</p>
<p>For an existing application that already uses <code>react-color</code>, replacing it only because another library is newer may also create unnecessary work.</p>
<h1>4. @uiw/react-color</h1>
<p><a href="https://www.npmjs.com/package/@uiw/react-color"><code>@uiw/react-color</code></a> sits somewhere between the previous approaches.</p>
<p>It offers many different React color picker components, but those components can also be installed separately.</p>
<p>Its <a href="https://github.com/uiwjs/react-color">collection</a> includes options such as Sketch, Chrome, Wheel, Swatch, Slider, Material, Compact, Circle, Block, GitHub, and Colorful. It also provides lower-level pieces such as saturation, hue, alpha, editable inputs, and shade sliders.</p>
<p>That modular structure is useful.</p>
<p>If your project only needs the wheel, for example, you do not necessarily have to build everything around the main package.</p>
<pre><code class="language-bash">npm install @uiw/react-color-wheel
</code></pre>
<p>The main <a href="https://www.npmjs.com/package/@uiw/react-color"><code>@uiw/react-color</code></a> package currently lists 20 dependencies, but those are largely its own component packages. Installing an individual picker can therefore make more sense when you only need one part of the library.</p>
<p>It also includes TypeScript declarations.</p>
<p>One interesting feature is the Chrome picker's eyedropper option.</p>
<pre><code class="language-tsx">&lt;Chrome
  showEyeDropper
  color={color}
  onChange={handleChange}
/&gt;
</code></pre>
<p>The package also provides editable HEXA, RGBA, and HSLA input modes.</p>
<p>If you prefer having many independent picker styles available as separate packages, <a href="https://github.com/uiwjs/react-color"><code>@uiw/react-color</code></a> provides a lot of flexibility.</p>
<h1>ChromaPanel vs react-colorful</h1>
<p>This is probably the most useful direct comparison.</p>
<p>Both libraries have zero runtime dependencies.</p>
<p>Both include TypeScript types.</p>
<p>Both document accessibility.</p>
<p>But their goals are quite different.</p>
<p><a href="https://www.npmjs.com/package/react-colorful"><code>react-colorful</code></a> focuses on doing the core color-picker job with very little code.</p>
<p><a href="https://www.npmjs.com/package/chroma-panel">ChromaPanel</a> includes a much larger set of functionality around that picker.</p>
<p>With ChromaPanel, you can get the <a href="https://chroma-panel.jscrate.dev/react/modes/wheel">wheel</a>, per-channel <a href="https://chroma-panel.jscrate.dev/react/modes/sliders">RGB/HSL/HSB sliders</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/palettes">palettes</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/image">image sampling</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper">eyedropper</a>, <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor">gradients</a>, <a href="https://chroma-panel.jscrate.dev/react/handbook/forms">form integration</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/contrast">contrast utilities</a>, and <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4">modern CSS colors</a> from the same project.</p>
<p>With <code>react-colorful</code>, you get a significantly smaller foundation and can build the extra pieces your application needs.</p>
<p>So the real question is not simply:</p>
<p><strong>Which React color picker is smaller?</strong></p>
<p>That answer is clearly <code>react-colorful</code>.</p>
<p>The more useful question is:</p>
<p><strong>How much color functionality does my application actually need?</strong></p>
<p>If the answer is only a simple HEX, RGB, HSL, or HSV picker, the smaller package can make a lot of sense.</p>
<p>If the answer includes several additional color workflows, comparing only the size of the base picker becomes less useful.</p>
<h1>ChromaPanel vs react-color</h1>
<p>These two packages are also quite different.</p>
<p><a href="https://www.npmjs.com/package/react-color"><code>react-color</code></a> gives you many recognizable pre-designed picker interfaces.</p>
<p><a href="https://chroma-panel.jscrate.dev/">ChromaPanel</a> instead provides different ways of working with color inside one system.</p>
<p>If you specifically want something that looks like the classic Sketch or Photoshop picker, <a href="https://github.com/casesandberg/react-color"><code>react-color</code></a> already gives you that type of interface.</p>
<p>ChromaPanel focuses more on modern application features: <a href="https://chroma-panel.jscrate.dev/react/modes/image">image sampling</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4">CSS Color 4</a>, <a href="https://chroma-panel.jscrate.dev/react/handbook/forms">form behavior</a>, <a href="https://chroma-panel.jscrate.dev/react/overview/accessibility">accessibility</a>, <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor">gradients</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/contrast">contrast calculations</a>, an <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper">eyedropper</a>, and different selection modes.</p>
<p>Another difference is package age and maintenance.</p>
<p><code>react-color</code> has existed for many years and has a very large usage history. ChromaPanel is much newer, so it does not yet have that same long-term ecosystem history.</p>
<p>That history can matter when evaluating a dependency, and it is worth considering alongside features.</p>
<h1>ChromaPanel vs @uiw/react-color</h1>
<p>This comparison is closer because both provide several ways to choose colors.</p>
<p><a href="https://www.npmjs.com/package/@uiw/react-color"><code>@uiw/react-color</code></a> gives developers a large collection of separate picker components.</p>
<p><a href="https://chroma-panel.jscrate.dev/react/components/chroma-panel">ChromaPanel</a> gives developers multiple modes inside one color panel.</p>
<p><a href="https://github.com/uiwjs/react-color"><code>@uiw/react-color</code></a> has a dedicated wheel, swatches, Chrome-style picker, Sketch-style picker, sliders, and lower-level primitives.</p>
<p>ChromaPanel adds areas such as <a href="https://chroma-panel.jscrate.dev/react/modes/image">image color extraction</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4">modern CSS color spaces</a>, <a href="https://chroma-panel.jscrate.dev/react/handbook/forms">native form integration</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/contrast">contrast utilities</a>, and a dedicated <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor">gradient editor</a>.</p>
<p>Both approaches can work well.</p>
<p>The main difference is whether you want a collection of separate picker packages or one color system that can switch between different modes.</p>
<h1>What about bundle size?</h1>
<p>Bundle size deserves some context.</p>
<p>The <a href="https://chroma-panel.jscrate.dev/react/overview/comparison">comparison audit</a> measured the package main entries at roughly:</p>
<p><a href="https://www.npmjs.com/package/chroma-panel"><strong>ChromaPanel</strong></a><strong>: 22.0 kB gzip →</strong> <a href="https://www.npmjs.com/package/@uiw/react-color"><strong>@uiw/react-color</strong></a><strong>: 16.0 kB →</strong> <a href="https://www.npmjs.com/package/react-colorful"><strong>react-colorful</strong></a><strong>: 4.9 kB →</strong> <a href="https://www.npmjs.com/package/react-color"><strong>react-color</strong></a><strong>: 38.4 kB.</strong></p>
<p>But those numbers are not the complete story.</p>
<p><code>react-colorful</code> documents a smaller figure for an individual picker.</p>
<p><code>@uiw/react-color</code> allows individual picker packages to be installed separately.</p>
<p>ChromaPanel also provides <a href="https://chroma-panel.jscrate.dev/react/utils/entry-points">smaller entry points</a> so applications do not have to use all five modes.</p>
<p>Its own Vite measurement reports around <strong>23.1 kB gzipped for all five modes</strong> and roughly <strong>15 kB for the panel shell plus one mode</strong>.</p>
<p>That is why bundle comparisons should always include the functionality being shipped.</p>
<p>A 5 kB package plus several custom components may not remain 5 kB once the full feature is finished.</p>
<p>At the same time, an application that genuinely needs only a simple color picker should not ship advanced functionality it will never use.</p>
<h1>Which React color picker should you use?</h1>
<p>There is no single answer for every project.</p>
<p>Use <a href="https://chroma-panel.jscrate.dev/"><strong>ChromaPanel</strong></a> when your application needs a broader color system: <a href="https://chroma-panel.jscrate.dev/react/components/chroma-panel">multiple selection modes</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/image">image color picking</a>, an <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper">eyedropper</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/palettes">palettes</a>, <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor">gradients</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4">modern CSS colors</a>, <a href="https://chroma-panel.jscrate.dev/react/handbook/forms">form behavior</a>, <a href="https://chroma-panel.jscrate.dev/react/overview/accessibility">accessibility</a>, and <a href="https://chroma-panel.jscrate.dev/react/utils/contrast">contrast utilities</a>.</p>
<p>Use <strong>react-colorful</strong> when you mainly want a small, accessible React color picker and prefer to build additional UI such as popovers and palettes yourself.</p>
<p>Use <strong>react-color</strong> when its collection of familiar ready-made picker designs matches an existing application or a specific UI requirement.</p>
<p>Use <strong>@uiw/react-color</strong> when you want a collection of modular picker components and like being able to install individual styles such as Wheel, Chrome, Sketch, or Swatch separately.</p>
<p>That is a more useful way to compare React color picker libraries than simply asking which package has the most features or the smallest bundle.</p>
<h1>Final thoughts</h1>
<p>A React color picker can be a tiny input or a surprisingly large part of an application's design system.</p>
<p>That is why these four libraries look so different.</p>
<p><code>react-colorful</code> keeps the core small.</p>
<p><code>react-color</code> gives you many familiar picker designs.</p>
<p><code>@uiw/react-color</code> provides a modular collection of individual picker components.</p>
<p>And <a href="https://github.com/re-sohail/chroma-panel"><strong>ChromaPanel</strong></a> tries to cover the wider color workflow in one package: <a href="https://chroma-panel.jscrate.dev/react/modes/wheel">wheel</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/sliders">sliders</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/palettes">palettes</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/pencils">pencils</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/image">image sampling</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper">eyedropper</a>, <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor">gradients</a>, <a href="https://chroma-panel.jscrate.dev/react/overview/accessibility">accessibility</a>, <a href="https://chroma-panel.jscrate.dev/react/handbook/forms">form integration</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/contrast">contrast tools</a>, and <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4">modern CSS color spaces</a>.</p>
<p>For a simple HEX field, you may not need all of that.</p>
<p>But for a theme editor, website builder, design tool, branding interface, SaaS dashboard, product customizer, or application where users work seriously with color, those additional features can save a lot of custom implementation.</p>
<p>The important thing is to choose the React color picker based on the experience you need to build.</p>
<p>Not just the name of the package.</p>
]]></content:encoded></item><item><title><![CDATA[ChromaPanel: A Modern React Color Picker with Eyedropper, Image Sampling, Gradients, and More]]></title><description><![CDATA[A color picker looks like a simple UI component until you need to use one in a real product.
At first, you may only need a HEX color field.
Then the requirements start growing.
Users want a color whee]]></description><link>https://chroma-panel.hashnode.dev/react-color-picker-with-eyedropper-image-sampling-gradients-and-more</link><guid isPermaLink="true">https://chroma-panel.hashnode.dev/react-color-picker-with-eyedropper-image-sampling-gradients-and-more</guid><category><![CDATA[React]]></category><category><![CDATA[React]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Sohail Khan]]></dc:creator><pubDate>Wed, 23 Sep 2026 06:00:09 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/6ab0f13c1a95a677dadec704/d362b709-80e9-41b9-929f-cd6102f42731.png" alt="" style="display:block;margin:0 auto" />

<p>A color picker looks like a simple UI component until you need to use one in a real product.</p>
<p>At first, you may only need a HEX color field.</p>
<p>Then the requirements start growing.</p>
<p>Users want a <a href="https://chroma-panel.jscrate.dev/react/modes/wheel?utm_source=chatgpt.com">color wheel</a>. Designers want exact <a href="https://chroma-panel.jscrate.dev/react/modes/sliders?utm_source=chatgpt.com">RGB and HSL values</a>. Your settings page needs predefined brand colors. Someone asks to <a href="https://chroma-panel.jscrate.dev/react/modes/image?utm_source=chatgpt.com">pick a color from an uploaded image</a>. Another user wants an <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper?utm_source=chatgpt.com">eyedropper</a>. Your form needs validation. Keyboard navigation has to work. And eventually you may need <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor?utm_source=chatgpt.com">gradients</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com">OKLCH, Display P3</a>, or <a href="https://chroma-panel.jscrate.dev/react/utils/contrast?utm_source=chatgpt.com">accessible contrast checks</a>.</p>
<p>At that point, a basic React color picker is no longer enough.</p>
<p>That is the problem I wanted to solve with <a href="https://chroma-panel.jscrate.dev/?utm_source=chatgpt.com"><strong>ChromaPanel</strong></a>.</p>
<p>ChromaPanel is an <a href="https://github.com/re-sohail/chroma-panel?utm_source=chatgpt.com">open-source</a> React color picker built for forms, settings pages, toolbars, theme editors, design tools, and other interfaces where users need more than a simple HEX picker.</p>
<p>It gives you multiple ways to choose a color while keeping them behind one React API.</p>
<h2>What is ChromaPanel?</h2>
<p><a href="https://chroma-panel.jscrate.dev/react/components/chroma-panel?utm_source=chatgpt.com">ChromaPanel</a> is a React color picker component that can be used as a small color input or as a complete inline color panel.</p>
<p>It includes:</p>
<ul>
<li><p>a <a href="https://chroma-panel.jscrate.dev/react/modes/wheel?utm_source=chatgpt.com">color wheel</a></p>
</li>
<li><p><a href="https://chroma-panel.jscrate.dev/react/modes/sliders?utm_source=chatgpt.com">RGB, HSL, HSB/HSV, and alpha sliders</a></p>
</li>
<li><p>searchable <a href="https://chroma-panel.jscrate.dev/react/modes/palettes?utm_source=chatgpt.com">color palettes</a></p>
</li>
<li><p>a <a href="https://chroma-panel.jscrate.dev/react/modes/pencils?utm_source=chatgpt.com">120-color pencil-style swatch grid</a></p>
</li>
<li><p><a href="https://chroma-panel.jscrate.dev/react/modes/image?utm_source=chatgpt.com">image color extraction and pixel sampling</a></p>
</li>
<li><p>an <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper?utm_source=chatgpt.com">eyedropper in supported browsers</a></p>
</li>
<li><p>HEX, RGB, HSL, HSV, and alpha values</p>
</li>
<li><p><a href="https://chroma-panel.jscrate.dev/react/handbook/controlled?utm_source=chatgpt.com">controlled and uncontrolled React state</a></p>
</li>
<li><p><a href="https://chroma-panel.jscrate.dev/react/handbook/forms?utm_source=chatgpt.com">native form support</a></p>
</li>
<li><p><a href="https://chroma-panel.jscrate.dev/react/overview/accessibility?utm_source=chatgpt.com">keyboard and screen-reader support</a></p>
</li>
<li><p><a href="https://chroma-panel.jscrate.dev/react/handbook/typescript?utm_source=chatgpt.com">TypeScript types</a></p>
</li>
<li><p><a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com">CSS Color 4 utilities</a></p>
</li>
<li><p><a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com">OKLCH, OKLab, Lab, LCH, sRGB, and Display P3 support</a></p>
</li>
<li><p><a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor?utm_source=chatgpt.com">linear and radial gradient editing</a></p>
</li>
<li><p><a href="https://chroma-panel.jscrate.dev/react/utils/contrast?utm_source=chatgpt.com">contrast helpers</a></p>
</li>
<li><p><a href="https://chroma-panel.jscrate.dev/react/utils/token-exports?utm_source=chatgpt.com">CSS, SCSS, Tailwind, and design-token exports</a></p>
</li>
<li><p><a href="https://www.npmjs.com/package/chroma-panel?utm_source=chatgpt.com">zero runtime dependencies</a></p>
</li>
</ul>
<p>You can use everything together or only import the parts your application needs.</p>
<h2>Install the React color picker</h2>
<p>Getting started is intentionally simple.</p>
<pre><code class="language-bash">npm install chroma-panel
</code></pre>
<p>There is no provider that you need to add around your application.</p>
<p>There is also no separate CSS file you need to import for the standard setup.</p>
<p>Once the <a href="https://www.npmjs.com/package/chroma-panel?utm_source=chatgpt.com">package is installed</a>, you can render a working React color picker with <a href="https://chroma-panel.jscrate.dev/react/components/color-input?utm_source=chatgpt.com"><code>ColorInput</code></a>.</p>
<pre><code class="language-tsx">import { ColorInput } from "chroma-panel";

export function BrandColor() {
  return &lt;ColorInput defaultValue="#3366cc" /&gt;;
}
</code></pre>
<p><a href="https://chroma-panel.jscrate.dev/react/components/color-input?utm_source=chatgpt.com"><code>ColorInput</code></a> displays a color swatch and opens ChromaPanel in a popover when the user clicks it.</p>
<p>For many forms and settings pages, that may be all you need.</p>
<h2>Using ChromaPanel as a controlled React color picker</h2>
<p>Most real applications need to keep the selected color in React state.</p>
<p>ChromaPanel supports the normal <a href="https://chroma-panel.jscrate.dev/react/handbook/controlled?utm_source=chatgpt.com">controlled component pattern</a>.</p>
<pre><code class="language-tsx">import { useState } from "react";
import { ColorInput } from "chroma-panel";

export function AccentColorPicker() {
  const [color, setColor] = useState("#3366cc");

  return (
    &lt;ColorInput
      value={color}
      onChange={(result) =&gt; setColor(result.hex)}
      aria-label="Accent color"
    /&gt;
  );
}
</code></pre>
<p>The useful part here is that <code>onChange</code> does not only give you a HEX string.</p>
<p>The result contains different representations of the selected color, so your application can use the format it actually needs.</p>
<p>You can update a live preview with <code>onChange</code> while the user is dragging and use <code>onChangeComplete</code> when the interaction finishes.</p>
<pre><code class="language-tsx">&lt;ColorInput
  value={color}
  onChange={(result) =&gt; setColor(result.hex)}
  onChangeComplete={(result) =&gt; saveColor(result.hex)}
/&gt;
</code></pre>
<p>This is useful when saving the value makes a network request, creates an undo entry, updates a database, or runs another expensive operation.</p>
<p>The UI can remain responsive without sending a request for every tiny movement of the color picker.</p>
<h2>Need an inline color picker instead?</h2>
<p>Sometimes a popover is not the right UI.</p>
<p>A design editor, theme builder, drawing tool, or customization page may need the entire React color picker to stay visible.</p>
<p>That is what <a href="https://chroma-panel.jscrate.dev/react/components/chroma-panel?utm_source=chatgpt.com"><code>ChromaPanel</code></a> is for.</p>
<pre><code class="language-tsx">import { ChromaPanel } from "chroma-panel";

export function ThemeEditor() {
  return (
    &lt;ChromaPanel
      defaultValue="#3366cc"
      modes={["wheel", "sliders", "palettes"]}
    /&gt;
  );
}
</code></pre>
<p>The <code>modes</code> property is important.</p>
<p>You do not have to show every feature just because the package supports it.</p>
<p>A branding form may only need <a href="https://chroma-panel.jscrate.dev/react/modes/palettes?utm_source=chatgpt.com">palettes</a>.</p>
<p>A design tool may need <a href="https://chroma-panel.jscrate.dev/react/modes/sliders?utm_source=chatgpt.com">sliders</a>, a <a href="https://chroma-panel.jscrate.dev/react/modes/wheel?utm_source=chatgpt.com">wheel</a>, and <a href="https://chroma-panel.jscrate.dev/react/modes/image?utm_source=chatgpt.com">image sampling</a>.</p>
<p>A simple theme editor may only need the <a href="https://chroma-panel.jscrate.dev/react/modes/wheel?utm_source=chatgpt.com">wheel</a>.</p>
<p>This makes it possible to use the same color picker package across different parts of an application without giving every user the same large interface.</p>
<h2>Color wheel</h2>
<p>The <a href="https://chroma-panel.jscrate.dev/react/modes/wheel?utm_source=chatgpt.com">wheel</a> gives users a visual way to explore colors.</p>
<p>Instead of typing values, they can move around the hue wheel and adjust the color visually.</p>
<p>This works well when choosing colors is part of a creative interface such as a theme editor, website builder, drawing tool, product customizer, or design system.</p>
<p>It is also useful when users know roughly what color they want but do not know its HEX or RGB value.</p>
<h2>RGB, HSL and HSB sliders</h2>
<p>Sometimes visual selection is not precise enough.</p>
<p>Developers and designers often need exact channel values.</p>
<p>ChromaPanel includes <a href="https://chroma-panel.jscrate.dev/react/modes/sliders?utm_source=chatgpt.com">per-channel controls for RGB, HSL, HSB/HSV, and alpha</a>.</p>
<p>That means someone can visually find a color first and then fine-tune the exact numbers without switching to another tool.</p>
<p>The same React color picker can therefore work for casual users and people who need precise color control.</p>
<h2>Searchable color palettes</h2>
<p>Many applications should not allow completely arbitrary colors.</p>
<p>A company may have an approved brand palette. A theme builder may provide a set of design tokens. A product customizer may only support colors that can actually be manufactured.</p>
<p>ChromaPanel supports <a href="https://chroma-panel.jscrate.dev/react/modes/palettes?utm_source=chatgpt.com">named palettes and searchable swatches</a>, so you can provide those approved colors directly inside the picker.</p>
<p>Instead of building a separate palette component beside your color picker, both experiences can live inside the same UI.</p>
<h2>The 120-color pencils mode</h2>
<p><a href="https://chroma-panel.jscrate.dev/react/modes/pencils?utm_source=chatgpt.com">Pencils mode</a> provides a large grid of ready-made colors.</p>
<p>Despite the name, it is not a drawing tool. It is a quick color-selection interface inspired by physical sets of colored pencils.</p>
<p>There are 120 colors available by default, and the set can be replaced when your product needs something different.</p>
<p>This mode is useful when users want to explore many predefined colors quickly without working with sliders or numerical values.</p>
<h2>Pick colors from an image</h2>
<p>This is one of the areas where a simple React color picker can quickly become a much bigger feature.</p>
<p>Imagine a user uploads a company logo and wants to create a matching theme.</p>
<p>Or they upload a product photo and want to use one of its main colors.</p>
<p>With ChromaPanel's <a href="https://chroma-panel.jscrate.dev/react/modes/image?utm_source=chatgpt.com">image mode</a>, a user can drop, paste, or select an image.</p>
<p>The React color picker can extract dominant colors from that image and display them as swatches.</p>
<p>The user can also select an exact pixel from the image.</p>
<p>You can use the image extraction API without showing the full color picker as well.</p>
<pre><code class="language-tsx">import { extractPalette } from "chroma-panel";

const { swatches } = await extractPalette(file, {
  maxColors: 8,
});
</code></pre>
<p>This makes the same functionality useful for automatic theme generation, logo analysis, image editors, design applications, avatar customization, and other tools.</p>
<p>Image processing is bounded and downscaled rather than processing an unlimited full-size image directly.</p>
<p>There is also a separate worker entry point for applications that want to move heavier image processing away from the main thread.</p>
<h2>A React color picker with an eyedropper</h2>
<p>ChromaPanel also includes support for the browser <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper?utm_source=chatgpt.com">EyeDropper API</a>.</p>
<p>Where that browser API is available, users can pick a color from somewhere else on their screen.</p>
<p>This can be especially useful inside design tools, theme builders, website editors, and other visual applications.</p>
<p>The important part is that unsupported browsers do not need special handling from your UI just to hide the normal picker control.</p>
<p>There is also a <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper?utm_source=chatgpt.com"><code>useEyedropper</code> hook</a> if you want to create your own eyedropper button somewhere else in your interface.</p>
<h2>It behaves like a real form control</h2>
<p>A surprisingly important part of a React color picker is how it works inside <a href="https://chroma-panel.jscrate.dev/react/handbook/forms?utm_source=chatgpt.com">forms</a>.</p>
<p>You should not have to create hidden inputs, manually synchronize values, and rebuild standard browser behavior just because the input happens to select a color.</p>
<p><a href="https://chroma-panel.jscrate.dev/react/components/color-input?utm_source=chatgpt.com"><code>ColorInput</code></a> can take a <code>name</code> and submit its value with <code>FormData</code>.</p>
<pre><code class="language-tsx">&lt;form action={saveProfile}&gt;
  &lt;label htmlFor="profile-color"&gt;Profile color&lt;/label&gt;

  &lt;ColorInput
    id="profile-color"
    name="profileColor"
    defaultValue="#3366cc"
    format="hex"
    required
  /&gt;

  &lt;button type="submit"&gt;Save&lt;/button&gt;
&lt;/form&gt;
</code></pre>
<p>It also supports normal behaviors such as <code>required</code>, <code>disabled</code>, and <code>form.reset()</code>.</p>
<p>That makes ChromaPanel particularly useful for settings screens, account customization, admin panels, SaaS dashboards, onboarding forms, and theme settings.</p>
<h2>Accessibility is part of the component</h2>
<p>Color pickers can be difficult to make <a href="https://chroma-panel.jscrate.dev/react/overview/accessibility?utm_source=chatgpt.com">accessible</a>.</p>
<p>Many visual pickers are built almost entirely around pointer interactions. That works with a mouse, but it can become frustrating or impossible with a keyboard or screen reader.</p>
<p>ChromaPanel uses real range inputs for its color axes.</p>
<p>That allows standard browser keyboard behavior to do much of the work.</p>
<p>Arrow keys can change values. Page Up and Page Down make larger movements. Home and End move to the limits.</p>
<p>Values are announced with meaningful text rather than exposing only an unexplained number.</p>
<p>The popover manages focus, closes with Escape, and returns focus to its trigger.</p>
<p>Swatches use buttons with accessible names, and the component also respects reduced-motion and forced-color preferences.</p>
<p><a href="https://chroma-panel.jscrate.dev/react/overview/accessibility?utm_source=chatgpt.com">Accessibility</a> should not be something that gets added after a React color picker is already built. It affects the way the component itself should work.</p>
<h2>Modern CSS colors: OKLCH, OKLab and Display P3</h2>
<p>HEX and RGB are still everywhere, but modern CSS supports much more.</p>
<p>ChromaPanel includes <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com">color utilities</a> for working with:</p>
<p><a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com"><code>OKLCH</code>, <code>OKLab</code>, <code>Lab</code>, <code>LCH</code>, <code>sRGB</code>, and <code>Display P3</code></a>.</p>
<p>For example:</p>
<pre><code class="language-tsx">import {
  parseColor,
  isInGamut,
  mapToGamut,
  serializeColor,
} from "chroma-panel/color";

const brand = parseColor("oklch(72% 0.18 250)");

if (brand &amp;&amp; !isInGamut(brand, "srgb")) {
  const fallback = mapToGamut(brand, "srgb");

  console.log(serializeColor(fallback));
}
</code></pre>
<p>These utilities do not require React or the DOM.</p>
<p>They can therefore also be used as general TypeScript color utilities inside your application.</p>
<p>This becomes useful when building design systems that are moving toward OKLCH, wide-gamut colors, and modern CSS color workflows.</p>
<h2>A gradient color picker for React</h2>
<p>Solid colors are not always enough either.</p>
<p>ChromaPanel provides a separate <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor?utm_source=chatgpt.com"><code>GradientEditor</code></a> entry point for building linear and radial gradients.</p>
<pre><code class="language-tsx">import {
  GradientEditor,
  gradientToCss,
} from "chroma-panel/gradient";
</code></pre>
<p>Gradient stops can be changed with the keyboard, and the utilities can turn the gradient model back into CSS.</p>
<p>The <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor?utm_source=chatgpt.com">gradient tools</a> are kept in a separate entry point.</p>
<p>That means an application that only needs a solid React color picker does not need to include the gradient editor code.</p>
<p>This separation matters because feature-rich UI libraries should not force every feature into every bundle.</p>
<h2>Zero runtime dependencies</h2>
<p>ChromaPanel has <a href="https://www.npmjs.com/package/chroma-panel?utm_source=chatgpt.com">no runtime dependencies</a>.</p>
<p>React and React DOM are peer dependencies, meaning the package uses the React installation that already exists in your application.</p>
<p><a href="https://chroma-panel.jscrate.dev/react/handbook/typescript?utm_source=chatgpt.com">TypeScript declarations</a> are included as well, so there is no separate <code>@types</code> package to install.</p>
<p>It supports React 16.14 and newer, including React 19.</p>
<p>It also provides ESM and CommonJS builds.</p>
<h2>What about bundle size?</h2>
<p>This is an important trade-off.</p>
<p>ChromaPanel is not trying to be the smallest React color picker on <a href="https://www.npmjs.com/package/chroma-panel?utm_source=chatgpt.com">npm</a>.</p>
<p>The complete package gives you much more functionality than a basic saturation box and hue slider.</p>
<p>With all five modes, the measured addition to a Vite production build is about <strong>23.1 kB gzipped</strong>.</p>
<p>Using the panel shell with one mode is around <strong>15 kB gzipped</strong>.</p>
<p>If your application only needs a tiny HEX picker, a smaller library such as <code>react-colorful</code> can make more sense.</p>
<p>But if your application would otherwise need a color picker, separate palettes, an eyedropper implementation, an image color picker, gradient editing, color conversions, accessibility work, and form integration, comparing only the size of the basic picker does not tell the full story.</p>
<h2>Import only the modes you need</h2>
<p>If you do not need all five modes, you can use the smaller <a href="https://chroma-panel.jscrate.dev/react/utils/entry-points?utm_source=chatgpt.com">entry points</a>.</p>
<pre><code class="language-tsx">import { ChromaPanel } from "chroma-panel/panel";
import { wheelMode } from "chroma-panel/modes";

export function SimplePicker() {
  return &lt;ChromaPanel modes={[wheelMode]} /&gt;;
}
</code></pre>
<p>This is useful when you want ChromaPanel's API and behavior without including every available color-selection mode.</p>
<h2>Works with modern React stacks</h2>
<p>ChromaPanel is not tied to one UI framework.</p>
<p>The documentation includes guidance for <a href="https://chroma-panel.jscrate.dev/react/frameworks/next-js?utm_source=chatgpt.com">Next.js</a>, <a href="https://chroma-panel.jscrate.dev/react/frameworks/vite?utm_source=chatgpt.com">Vite</a>, <a href="https://chroma-panel.jscrate.dev/react/frameworks/remix?utm_source=chatgpt.com">Remix</a>, <a href="https://chroma-panel.jscrate.dev/react/integrations/shadcn-ui?utm_source=chatgpt.com">shadcn/ui</a>, <a href="https://chroma-panel.jscrate.dev/react/integrations/material-ui?utm_source=chatgpt.com">Material UI</a>, <a href="https://chroma-panel.jscrate.dev/react/integrations/react-aria?utm_source=chatgpt.com">React Aria</a>, <a href="https://chroma-panel.jscrate.dev/react/frameworks/react-hook-form?utm_source=chatgpt.com">React Hook Form</a>, <a href="https://chroma-panel.jscrate.dev/react/handbook/tailwind?utm_source=chatgpt.com">Tailwind CSS</a>, and <a href="https://chroma-panel.jscrate.dev/react/handbook/server-rendering?utm_source=chatgpt.com">server-rendered applications</a>.</p>
<p>For example, if your project already uses <a href="https://chroma-panel.jscrate.dev/react/integrations/shadcn-ui?utm_source=chatgpt.com">shadcn/ui</a>, you can let shadcn's Popover, Dialog, or Sheet own the surrounding interface and render <code>ChromaPanel</code> inside it.</p>
<p>ChromaPanel does not require shadcn/ui, Radix UI, or Base UI as dependencies.</p>
<p>That is useful when you want a React color picker that fits your existing design system rather than forcing another UI framework into the application.</p>
<h2>When ChromaPanel makes sense</h2>
<p>I built ChromaPanel for applications where color selection is part of the product rather than just a small decorative field.</p>
<p>If you are building a theme editor, design system, SaaS settings page, website builder, graphics tool, product customizer, brand editor, admin dashboard, CMS, or another interface with serious color controls, having these capabilities behind one API can remove a lot of custom work.</p>
<p>But there is an equally important point.</p>
<p>If your entire requirement is simply:</p>
<blockquote>
<p>“Let the user choose a HEX color.”</p>
</blockquote>
<p>then you probably do not need every feature ChromaPanel provides.</p>
<p>A smaller React color picker can be the right choice.</p>
<p>Choosing a library should be based on what your interface actually needs, not the number of features printed on its README.</p>
<h2>Why I built it</h2>
<p>The main idea behind ChromaPanel was not to build another variation of the same basic React color picker.</p>
<p>It was to create a color-selection component that could grow with a real application.</p>
<p>You can start with:</p>
<pre><code class="language-tsx">&lt;ColorInput defaultValue="#3366cc" /&gt;
</code></pre>
<p>and stop there.</p>
<p>Or the same project can later add <a href="https://chroma-panel.jscrate.dev/react/modes/palettes?utm_source=chatgpt.com">palettes</a>, <a href="https://chroma-panel.jscrate.dev/react/modes/image?utm_source=chatgpt.com">image sampling</a>, precise <a href="https://chroma-panel.jscrate.dev/react/modes/sliders?utm_source=chatgpt.com">sliders</a>, <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor?utm_source=chatgpt.com">gradients</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com">modern CSS color spaces</a>, <a href="https://chroma-panel.jscrate.dev/react/overview/accessibility?utm_source=chatgpt.com">accessibility helpers</a>, <a href="https://chroma-panel.jscrate.dev/react/utils/contrast?utm_source=chatgpt.com">contrast tools</a>, and custom modes without replacing the original color picker.</p>
<p>That flexibility is what I wanted from the package.</p>
<h2>Final thoughts</h2>
<p>There are already good React color picker libraries available, and ChromaPanel is not meant to replace every one of them.</p>
<p>Its focus is different.</p>
<p>It is for applications that need several ways to work with color but still want one consistent React API.</p>
<p>If you need a <strong>React color picker with a</strong> <a href="https://chroma-panel.jscrate.dev/react/modes/wheel?utm_source=chatgpt.com"><strong>color wheel</strong></a><strong>,</strong> <a href="https://chroma-panel.jscrate.dev/react/modes/sliders?utm_source=chatgpt.com"><strong>RGB and HSL sliders</strong></a><strong>,</strong> <a href="https://chroma-panel.jscrate.dev/react/modes/palettes?utm_source=chatgpt.com"><strong>palettes</strong></a><strong>,</strong> <a href="https://chroma-panel.jscrate.dev/react/modes/image?utm_source=chatgpt.com"><strong>image color extraction</strong></a><strong>, an</strong> <a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper?utm_source=chatgpt.com"><strong>eyedropper</strong></a><strong>,</strong> <a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor?utm_source=chatgpt.com"><strong>gradients</strong></a><strong>,</strong> <a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com"><strong>modern CSS colors</strong></a><strong>,</strong> <a href="https://chroma-panel.jscrate.dev/react/handbook/typescript?utm_source=chatgpt.com"><strong>TypeScript support</strong></a><strong>,</strong> <a href="https://chroma-panel.jscrate.dev/react/overview/accessibility?utm_source=chatgpt.com"><strong>accessibility</strong></a><strong>, and</strong> <a href="https://www.npmjs.com/package/chroma-panel?utm_source=chatgpt.com"><strong>zero runtime dependencies</strong></a>, ChromaPanel may be worth trying.</p>
<p>The project is <a href="https://github.com/re-sohail/chroma-panel?utm_source=chatgpt.com">open source</a> and released under the MIT license.</p>
<p>If you use it in a project, find a problem, or have an idea for something that could make the React color picker better, feedback and contributions are welcome.</p>
]]></content:encoded></item><item><title><![CDATA[Stop Using Basic React Color Pickers — Meet Chroma Panel]]></title><description><![CDATA[If you have built a React application that requires user customization, you have probably needed a color picker. And if you are like most developers, you probably reached for one of the older, widely ]]></description><link>https://chroma-panel.hashnode.dev/stop-using-basic-react-color-pickers-meet-chroma-panel</link><guid isPermaLink="true">https://chroma-panel.hashnode.dev/stop-using-basic-react-color-pickers-meet-chroma-panel</guid><category><![CDATA[React]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[frontend]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[UI Design]]></category><dc:creator><![CDATA[Sohail Khan]]></dc:creator><pubDate>Mon, 21 Sep 2026 09:21:04 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/6ab0f13c1a95a677dadec704/edeacd08-a6d5-4bd2-9dec-8a54977db306.png" alt="" style="display:block;margin:0 auto" />

<p>If you have built a React application that requires user customization, you have probably needed a color picker. And if you are like most developers, you probably reached for one of the older, widely used React color picker libraries.</p>
<p>While those older libraries get the job done, they haven't changed much in years. They are basic, their UIs can feel a bit dated, and adding advanced features often requires hacking things together.</p>
<p>Recently, a new library caught my attention that feels like a massive upgrade for modern React applications: <a href="https://www.npmjs.com/package/chroma-panel"><strong>Chroma Panel</strong></a>.</p>
<p>If you are looking for a feature-rich, highly customizable color picker with documentation that rivals modern libraries like Shadcn UI, here is why you should consider making the switch.</p>
<h2>What makes Chroma Panel different?</h2>
<p>Most standard color pickers give you a simple grid or a basic hex input. <a href="https://chroma-panel.jscrate.dev/">Chroma Panel</a> Panel is built for applications that need more power. It comes packed out-of-the-box with features that you normally have to build from scratch.</p>
<p>Here is what it includes:</p>
<ul>
<li><p><strong>Modern Color Wheel:</strong> A smooth, responsive color wheel for precise selections.</p>
</li>
<li><p><strong>Advanced Sliders:</strong> Full control over Hue, Saturation, Lightness, and Opacity (Alpha).</p>
</li>
<li><p><strong>Custom Palettes:</strong> Support for predefined color swatches that match your app's branding.</p>
</li>
<li><p><strong>Image Color Picking:</strong> A standout feature that lets users extract colors directly from uploaded images.</p>
</li>
<li><p><strong>Pencil/Freehand Modes:</strong> Great for drawing apps or creative tools.</p>
</li>
<li><p><strong>Custom Modes:</strong> Total flexibility to configure the picker to match your specific use case.</p>
</li>
</ul>
<h2>World-Class Documentation (The "Shadcn UI" Feel)</h2>
<p>As developers, we all know that a library is only as good as its documentation. This is where Chroma Panel really shines.</p>
<p>If you enjoy the documentation style of <strong>Shadcn UI</strong> or <strong>Base UI</strong>, you will feel right at home here. The creators didn't just throw a GitHub readme together; they built a proper documentation site.</p>
<p>When you visit their docs, you get:</p>
<ul>
<li><p><strong>Step-by-step integration guides:</strong> No guessing how to set it up.</p>
</li>
<li><p><strong>Clear usage examples:</strong> Real-world examples of how to implement different features.</p>
</li>
<li><p><strong>Dedicated React Hooks:</strong> Detailed explanations on how to manage color state efficiently.</p>
</li>
<li><p><strong>Copy-and-paste code:</strong> Ready-to-use snippets to get you moving faster.</p>
</li>
</ul>
<h2>Quick Start: How to use it</h2>
<p>Getting started is incredibly straightforward. First, install the package via NPM:</p>
<pre><code class="language-bash">npm install chroma-panel
</code></pre>
<p>Then, import the styles and the component into your React file. Here is a very basic implementation:</p>
<pre><code class="language-jsx">import React, { useState } from "react";
import { ChromaPanel } from "chroma-panel";
import "chroma-panel/dist/index.css"; // Don't forget the styles!

export default function App() {
  const [color, setColor] = useState("#3b82f6");

  return (
    &lt;div style={{ padding: "50px" }}&gt;
      &lt;h2&gt;Choose your theme color:&lt;/h2&gt;
      
      &lt;ChromaPanel 
        color={color} 
        onChange={(newColor) =&gt; setColor(newColor.hex)} 
      /&gt;
      
      &lt;p&gt;Current Color: {color}&lt;/p&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>Because of how it is built, you can easily swap out this basic view for the color wheel, the image picker, or a custom palette just by passing different props.</p>
<h2>Ready to upgrade?</h2>
<p>If you are tired of wrestling with outdated color pickers or writing custom CSS to make old libraries look modern, give Chroma Panel a try. It is actively maintained, packed with features, and incredibly easy to integrate.</p>
<ul>
<li><p><strong>Read the Documentation:</strong> <a href="https://chroma-panel.jscrate.dev/">https://chroma-panel.jscrate.dev/</a></p>
</li>
<li><p><strong>View on NPM:</strong> <a href="https://www.npmjs.com/package/chroma-panel">https://www.npmjs.com/package/chroma-panel</a></p>
</li>
</ul>
<p>Have you tried Chroma Panel yet? Let me know your thoughts in the comments!</p>
]]></content:encoded></item></channel></rss>