<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sohail Khan</title>
    <description>The latest articles on DEV Community by Sohail Khan (@re-sohail).</description>
    <link>https://dev.to/re-sohail</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4093718%2Fb316328f-8a9f-46a7-9903-7b0e62538c38.jpg</url>
      <title>DEV Community: Sohail Khan</title>
      <link>https://dev.to/re-sohail</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/re-sohail"/>
    <language>en</language>
    <item>
      <title>Why Fabric.js `loadFromJSON` Can Leave Your Editor Half-Loaded</title>
      <dc:creator>Sohail Khan</dc:creator>
      <pubDate>Thu, 01 Oct 2026 07:34:52 +0000</pubDate>
      <link>https://dev.to/re-sohail/why-fabricjs-loadfromjson-can-leave-your-editor-half-loaded-3lh3</link>
      <guid>https://dev.to/re-sohail/why-fabricjs-loadfromjson-can-leave-your-editor-half-loaded-3lh3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7cfew55bsk5fv6wcbash.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7cfew55bsk5fv6wcbash.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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. &lt;a href="https://www.npmjs.com/package/fabricjs-document-engine" rel="noopener noreferrer"&gt;fabricjs-document-engine&lt;/a&gt; checks external images before loading content into the canvas and rejects unresolved images with &lt;code&gt;MISSING_ASSETS&lt;/code&gt;, including the affected URLs and object IDs.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This guide uses React and the public API in &lt;strong&gt;fabricjs-document-engine 1.0.2&lt;/strong&gt;, checked on &lt;strong&gt;October 1, 2026&lt;/strong&gt;. The native Fabric.js behavior discussed below is version-specific, with &lt;strong&gt;Fabric.js 7.4.0&lt;/strong&gt; as the concrete example. The engine supports Fabric.js &lt;strong&gt;6 and 7&lt;/strong&gt;. The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/overview/quick-start" rel="noopener noreferrer"&gt;quick-start guide&lt;/a&gt; covers installation and engine setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does a failed image always leave Fabric.js partially loaded?
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Fabric.js 7.4.0 creates objects before &lt;code&gt;loadFromJSON&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The relevant primary sources are Fabric.js's &lt;a href="https://www.fabricjs.com/api/classes/staticcanvas/#loadfromjson" rel="noopener noreferrer"&gt;loadFromJSON API&lt;/a&gt;, &lt;a href="https://github.com/fabricjs/fabric.js/blob/ce64f450bad811750cb5a75aa749fc1502c644be/src/canvas/StaticCanvas.ts" rel="noopener noreferrer"&gt;canvas implementation&lt;/a&gt;, and &lt;a href="https://github.com/fabricjs/fabric.js/blob/ce64f450bad811750cb5a75aa749fc1502c644be/src/util/misc/objectEnlive.ts" rel="noopener noreferrer"&gt;object-enlivening implementation&lt;/a&gt;. 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.&lt;/p&gt;

&lt;p&gt;The document engine adds two relevant checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check external assets.&lt;/strong&gt; Fail with a missing-image report before invoking the canvas loader.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refuse dropped objects.&lt;/strong&gt; Use a strict reviver when Fabric.js creates document objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package's &lt;a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/src/fabric/fabric-adapter.ts" rel="noopener noreferrer"&gt;Fabric adapter source&lt;/a&gt; shows the strict reviver. Its &lt;a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/src/assets/asset-pipeline.ts" rel="noopener noreferrer"&gt;asset preparation source&lt;/a&gt; shows where missing-image checks run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why can a valid canvas JSON file still fail to reopen?
&lt;/h2&gt;

&lt;p&gt;Valid JSON describes the document. It does not guarantee that every dependency still exists.&lt;/p&gt;

&lt;p&gt;An image object may contain a &lt;code&gt;src&lt;/code&gt; pointing to a deleted file, an expired signed URL, an unavailable host, or a &lt;code&gt;blob:&lt;/code&gt; URL from a previous browser session. The JSON can parse successfully while the browser cannot load that image.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;What to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shapes load but a picture disappears&lt;/td&gt;
&lt;td&gt;Failed image request; omitted object in the loading path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A document works until refresh&lt;/td&gt;
&lt;td&gt;A tab-only &lt;code&gt;blob:&lt;/code&gt; image was saved without durable upload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An image displays but PNG export fails&lt;/td&gt;
&lt;td&gt;Canvas tainting and image CORS settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text uses the wrong face or layout&lt;/td&gt;
&lt;td&gt;Font availability before text creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An older document replaces a newer selection&lt;/td&gt;
&lt;td&gt;Overlapping requests and loads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Objects appear only after clicking the canvas&lt;/td&gt;
&lt;td&gt;Rendering after the asynchronous load&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The package's &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts" rel="noopener noreferrer"&gt;images and fonts guide&lt;/a&gt; separates asset loading, font fallback, and CORS/export problems. The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/production/troubleshooting" rel="noopener noreferrer"&gt;troubleshooting guide&lt;/a&gt; covers related editor symptoms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does clearing the canvas before loading make failures worse?
&lt;/h2&gt;

&lt;p&gt;This code discards the current drawing before the next document has opened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fragile app code: the old drawing is removed before the load succeeds.&lt;/span&gt;
&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadFromJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedJson&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestRenderAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If loading fails, the app has already removed the old objects. Catching the error afterward does not reconstruct those objects.&lt;/p&gt;

&lt;p&gt;Do not clear the canvas before asking the engine to open a document. Let the engine validate and prepare the incoming content first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedDocument&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a storage adapter is configured, open a saved record by its document ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;design-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;loadDocument&lt;/code&gt; accepts a document value. &lt;code&gt;load(id)&lt;/code&gt; first asks the storage adapter for that value. Both use the engine's document-loading path.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-and-load" rel="noopener noreferrer"&gt;save and load guide&lt;/a&gt; and &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/api/document-engine" rel="noopener noreferrer"&gt;document engine API&lt;/a&gt; for the method signatures.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens before the engine mutates the canvas content?
&lt;/h2&gt;

&lt;p&gt;The engine prepares the incoming document before passing it to Fabric.js. With image checks enabled, the loading path does the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check the input.&lt;/strong&gt; Apply content limits, migrate supported formats, and validate the document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check object types.&lt;/strong&gt; Refuse classes that have not been registered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolve asset URLs.&lt;/strong&gt; Run an optional &lt;code&gt;assets.resolveUrl&lt;/code&gt; handler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check dependencies.&lt;/strong&gt; Check external images and font availability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try replacements.&lt;/strong&gt; Run an optional &lt;code&gt;assets.replaceMissingImage&lt;/code&gt; handler for missing images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject missing images.&lt;/strong&gt; Throw &lt;code&gt;MISSING_ASSETS&lt;/code&gt; if unresolved images remain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load the content.&lt;/strong&gt; Invoke Fabric.js with the engine's strict reviver.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the &lt;code&gt;MISSING_ASSETS&lt;/code&gt; failure in step 6, the canvas content has not been replaced. The error carries a list shaped like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;missingAssets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// [&lt;/span&gt;
&lt;span class="c1"&gt;//   { url: "/uploads/deleted-logo.png", objectIds: ["logo-image"] }&lt;/span&gt;
&lt;span class="c1"&gt;// ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;data:&lt;/code&gt; images are not fetched by the external-image preflight; malformed embedded data can fail during object creation instead.&lt;/p&gt;

&lt;p&gt;Keep image checks enabled when you want this preflight behavior. Setting &lt;code&gt;assets.checkImages: false&lt;/code&gt; skips the external-image check and removes the early missing-asset report.&lt;/p&gt;

&lt;p&gt;The exact order is visible in the &lt;a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/src/engine/create-document-engine.ts" rel="noopener noreferrer"&gt;engine loading source&lt;/a&gt; and the &lt;a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/src/assets/asset-pipeline.ts" rel="noopener noreferrer"&gt;asset preparation source&lt;/a&gt;. The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts#when-a-document-is-opened" rel="noopener noreferrer"&gt;images and fonts documentation&lt;/a&gt; explains the public behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you prove that a failed image leaves the current drawing intact?
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Textbox&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabric&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;createDocumentEngine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isDocumentEngineError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FabricDocument&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;demonstrateMissingImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// No storage is configured in this isolated loading demonstration.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createDocumentEngine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Textbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Keep the current drawing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentObjects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getObjects&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FabricDocument&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broken-design&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;logo-image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/uploads/intentionally-missing-logo.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The test URL unexpectedly loaded.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isDocumentEngineError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MISSING_ASSETS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getObjects&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;currentObjects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;currentObjects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The current drawing was changed.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;missingAssets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;missingAssets&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The expected result is &lt;code&gt;MISSING_ASSETS&lt;/code&gt;, the failed URL, and &lt;code&gt;logo-image&lt;/code&gt;. The object-reference check confirms that the existing drawing survived the rejected load. The package repository contains a related &lt;a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/tests/assets.browser.test.ts" rel="noopener noreferrer"&gt;browser test for missing images before canvas mutation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This helper was run in Chromium against package 1.0.2 and Fabric.js 7.4.0. It returned &lt;code&gt;MISSING_ASSETS&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you show the engine error in a React editor?
&lt;/h2&gt;

&lt;p&gt;Catch the rejected operation and branch on the stable error code. Show &lt;code&gt;missingAssets&lt;/code&gt; to the user instead of silently logging a generic failure.&lt;/p&gt;

&lt;p&gt;Install the packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;fabric fabricjs-document-engine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Textbox&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabric&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;isDocumentEngineError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DocumentEngineError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FabricDocument&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMemoryStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine/storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;useDocumentEngine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useDocumentState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMemoryStorage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;brokenDocument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FabricDocument&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broken-design&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;logo-image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/uploads/intentionally-missing-logo.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SafeLoadEditor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elementRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCanvas&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Canvas&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setProblem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DocumentEngineError&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;busy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setBusy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;setCanvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDocumentEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;working-design&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDocumentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setBusy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setProblem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isDocumentEngineError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LOAD_ABORTED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;setProblem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Opening failed.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setBusy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;busy&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isSaving&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Textbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Keep the current drawing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Add text&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Current drawing saved.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Save current drawing&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brokenDocument&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Document opened.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Try opening the broken document&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

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

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;saveStatus&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Preparing editor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;canvas&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Drawing preserved after a failed load"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clicking &lt;strong&gt;Try opening the broken document&lt;/strong&gt; before saving an edit should show &lt;code&gt;UNSAVED_CHANGES&lt;/code&gt;. After saving, the same operation reaches image preflight and should show &lt;code&gt;MISSING_ASSETS&lt;/code&gt;. The app never passes &lt;code&gt;discardUnsavedChanges: true&lt;/code&gt; automatically.&lt;/p&gt;

&lt;p&gt;For an API-backed editor, replace the memory adapter with your HTTP adapter and call &lt;code&gt;engine.load(documentId)&lt;/code&gt; inside the same handler. The companion guide, &lt;a href="//./save-fabricjs-canvas-json-to-api.md"&gt;Save Fabric.js Canvas JSON to an API and Open It by Document ID&lt;/a&gt;, includes the adapter and app-owned endpoint.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/react" rel="noopener noreferrer"&gt;React guide&lt;/a&gt;, &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/api/react-hooks" rel="noopener noreferrer"&gt;React hooks reference&lt;/a&gt;, and &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/api/error-codes" rel="noopener noreferrer"&gt;error code reference&lt;/a&gt; document the hooks and error fields used here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you catch errors or subscribe to &lt;code&gt;load:error&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Use the operation's &lt;code&gt;catch&lt;/code&gt; when the result belongs to a particular button, route, or document selection. Use &lt;code&gt;load:error&lt;/code&gt; when a shared panel or logging system needs to observe engine loading failures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useDocumentEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Inside a React component that already has an engine.&lt;/span&gt;
&lt;span class="nf"&gt;useDocumentEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load:error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LOAD_ABORTED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Document loading failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event observes an error; it does not consume the promise rejection. Still handle the promise returned by &lt;code&gt;engine.load()&lt;/code&gt; or &lt;code&gt;engine.loadDocument()&lt;/code&gt;. If both paths show notifications, one failure can produce duplicate messages.&lt;/p&gt;

&lt;p&gt;An unsaved-change guard can reject before the loading sequence starts. Catching the requested operation also covers that case. &lt;code&gt;useDocumentState(engine)&lt;/code&gt; exposes &lt;code&gt;loadError&lt;/code&gt; for failures tracked by the state store, but a persistent guard message should not depend only on that field.&lt;/p&gt;

&lt;p&gt;The event payload is &lt;code&gt;{ error }&lt;/code&gt;, not the error directly. See the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/api/events" rel="noopener noreferrer"&gt;events API&lt;/a&gt; for the exact contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you repair a missing image without removing the object?
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;assets.replaceMissingImage&lt;/code&gt; when your app has a known replacement. The hook receives the missing URL and the object IDs that refer to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createDocumentEngine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;replaceMissingImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/uploads/deleted-logo.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/replacement-logo.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an engine-options fragment; &lt;code&gt;canvas&lt;/code&gt; and &lt;code&gt;storage&lt;/code&gt; are the instances already owned by your app, and &lt;code&gt;createDocumentEngine&lt;/code&gt; is imported from the package.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;IMAGE_REPLACED&lt;/code&gt; warning.&lt;/p&gt;

&lt;p&gt;A replacement policy changes document content. Make that policy clear to the user. For client artwork, returning &lt;code&gt;null&lt;/code&gt; and asking for a replacement may be preferable to inserting a generic placeholder.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts" rel="noopener noreferrer"&gt;asset options guide&lt;/a&gt; and &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/api/create-document-engine" rel="noopener noreferrer"&gt;createDocumentEngine API&lt;/a&gt;. &lt;code&gt;engine.replaceImage(oldUrl, newUrl)&lt;/code&gt; repairs images already present in the current canvas; it does not modify an incoming document that preflight rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if the image URL expires instead of disappearing?
&lt;/h2&gt;

&lt;p&gt;Resolve the stored asset address into a currently accessible URL with &lt;code&gt;assets.resolveUrl&lt;/code&gt;. Your app can use that handler to request a fresh signed URL from its backend before the engine checks and loads the image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;resolveUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;asset://&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;asset://&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/api/assets/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;/url`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The asset address could not be resolved.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The asset-resolution endpoint is &lt;strong&gt;app-owned backend code&lt;/strong&gt;. The package does not implement asset permissions or signed-URL generation.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For tab-only &lt;code&gt;blob:&lt;/code&gt; images, configure &lt;code&gt;assets.upload&lt;/code&gt; while saving so reopening does not depend on the previous tab. Both hooks are described in the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts" rel="noopener noreferrer"&gt;images and fonts guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you fix an image CORS failure from React alone?
&lt;/h2&gt;

&lt;p&gt;The image host must permit the cross-origin request when the image is loaded with CORS enabled. Setting &lt;code&gt;crossOrigin: "anonymous"&lt;/code&gt; in React or Fabric.js does not create permission on the remote server.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The engine's &lt;code&gt;IMAGE_CROSS_ORIGIN&lt;/code&gt; warning helps identify risky image references. Export preflight can reject a raster export with &lt;code&gt;EXPORT_BLOCKED&lt;/code&gt;; that is a different error from loading a document with &lt;code&gt;MISSING_ASSETS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Read the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts#cors-and-export" rel="noopener noreferrer"&gt;CORS guidance&lt;/a&gt; and &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/export" rel="noopener noreferrer"&gt;export guide&lt;/a&gt;. The engine reports the problem; your app and image host control how the asset is served.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should the editor do with each loading error?
&lt;/h2&gt;

&lt;p&gt;Use error codes to decide what action to offer. Do not match error-message wording.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine code&lt;/th&gt;
&lt;th&gt;Useful editor response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MISSING_ASSETS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List missing image URLs and affected object IDs; offer repair or retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MISSING_FONTS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Load the required fonts or explain why the document cannot open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNKNOWN_OBJECT_TYPE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Register the missing custom class before retrying&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INVALID_DOCUMENT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show document validation details; request a valid saved record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNSAVED_CHANGES&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Offer save, cancel, or an explicit discard decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DOCUMENT_NOT_FOUND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Explain that the requested stored document is unavailable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOAD_ABORTED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Usually keep quiet when a newer document selection superseded the request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOAD_FAILED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inspect the storage or Fabric.js failure and the underlying &lt;code&gt;cause&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Missing fonts normally produce &lt;code&gt;FONT_UNAVAILABLE&lt;/code&gt; warnings and fallback text. Set &lt;code&gt;assets.requireFonts: true&lt;/code&gt; when the document must not open with fallback fonts; unavailable fonts then produce &lt;code&gt;MISSING_FONTS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The package's &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/api/error-codes" rel="noopener noreferrer"&gt;error code reference&lt;/a&gt; and &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/custom-objects" rel="noopener noreferrer"&gt;custom object guide&lt;/a&gt; describe these cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Will awaiting &lt;code&gt;loadFromJSON&lt;/code&gt; solve every loading problem?
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For a direct Fabric.js load, a basic successful path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadFromJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedJson&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestRenderAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;When using the document engine, wait for its load method. The engine requests rendering after its successful load.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you keep an older load from replacing a newer document?
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;engine.load(id)&lt;/code&gt; 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 &lt;code&gt;LOAD_ABORTED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If your app separately fetches JSON and then calls &lt;code&gt;loadDocument&lt;/code&gt;, your app must also prevent older fetch responses from being submitted after newer ones. The engine cannot know that a later &lt;code&gt;loadDocument&lt;/code&gt; call came from an earlier UI selection.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/api/document-engine" rel="noopener noreferrer"&gt;loading API&lt;/a&gt; documents the entry points, and the &lt;a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/src/engine/create-document-engine.ts" rel="noopener noreferrer"&gt;engine source&lt;/a&gt; shows the storage-response guard. Fabric.js itself also warns about overlapping loads in its &lt;a href="https://www.fabricjs.com/api/classes/staticcanvas/#loadfromjson" rel="noopener noreferrer"&gt;loadFromJSON reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://www.npmjs.com/package/fabricjs-document-engine" rel="noopener noreferrer"&gt;npm package&lt;/a&gt;, &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/overview/quick-start" rel="noopener noreferrer"&gt;quick start&lt;/a&gt;, and &lt;a href="https://github.com/re-sohail/fabricjs-document-engine" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; provide the source and further examples.&lt;/p&gt;

</description>
      <category>fabricjs</category>
      <category>react</category>
      <category>javascript</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Save Fabric.js Canvas JSON to an API and Open It by Document ID</title>
      <dc:creator>Sohail Khan</dc:creator>
      <pubDate>Thu, 01 Oct 2026 05:25:57 +0000</pubDate>
      <link>https://dev.to/re-sohail/save-fabricjs-canvas-json-to-an-api-and-open-it-by-document-id-4pj0</link>
      <guid>https://dev.to/re-sohail/save-fabricjs-canvas-json-to-an-api-and-open-it-by-document-id-4pj0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39cf8qnwdczfhyu5o9y0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39cf8qnwdczfhyu5o9y0.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://www.npmjs.com/package/fabricjs-document-engine" rel="noopener noreferrer"&gt;fabricjs-document-engine on npm&lt;/a&gt;, a storage adapter connects &lt;code&gt;engine.save()&lt;/code&gt; and &lt;code&gt;engine.load(documentId)&lt;/code&gt; to your own HTTP endpoints.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This guide builds a React editor, a typed HTTP adapter, and an Express/PostgreSQL endpoint. The examples follow the public API in package &lt;strong&gt;1.0.2&lt;/strong&gt;, checked on &lt;strong&gt;October 1, 2026&lt;/strong&gt;. The package supports Fabric.js &lt;strong&gt;6 and 7&lt;/strong&gt;. Start with the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/overview/quick-start" rel="noopener noreferrer"&gt;official quick-start guide&lt;/a&gt; if you have not created an engine before.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should you save to reopen an editable Fabric.js canvas?
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Fabric.js supplies serialization through &lt;code&gt;canvas.toJSON()&lt;/code&gt; and &lt;code&gt;canvas.toObject()&lt;/code&gt;. The document engine adds the information needed to identify, reopen, and save a document consistently:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Saved field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Finds the document through your API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;schemaVersion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identifies the engine's document format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;revision&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Detects a save based on an older stored revision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;canvas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Records width, height, and background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;objects&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Records serialized objects in stacking order, with stable IDs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists external images and font variants used by objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;metadata&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Holds app data such as the document title&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Store the complete document body. Keeping only &lt;code&gt;objects&lt;/code&gt; would remove the document ID, revision, and canvas dimensions that the rest of this example uses.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/document-format" rel="noopener noreferrer"&gt;document format reference&lt;/a&gt; describes the exact fields. The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-and-load" rel="noopener noreferrer"&gt;save and load guide&lt;/a&gt; explains how the engine serializes an existing canvas.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the storage adapter contract?
&lt;/h2&gt;

&lt;p&gt;A basic &lt;code&gt;DocumentStorage&lt;/code&gt; adapter has &lt;strong&gt;two required methods&lt;/strong&gt;: one reads a document and one saves it. The engine does not prescribe a database, URL structure, or server framework.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FabricDocument&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The public contract, shown here for reference.&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DocumentStorage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;loadDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;saveDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FabricDocument&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;expectedRevision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;loadDocument&lt;/code&gt; returns &lt;code&gt;unknown&lt;/code&gt; because storage is an external boundary. The engine checks the returned document before loading its content.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;expectedRevision&lt;/code&gt; is the revision this editor last loaded or saved. A new document starts at revision &lt;code&gt;0&lt;/code&gt;. The outgoing document normally carries the next revision. If your server assigns the revision, return the accepted value as &lt;code&gt;{ revision }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A value of &lt;code&gt;null&lt;/code&gt; means the caller requested an overwrite. Pass the provided &lt;code&gt;signal&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;These behaviors come from the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/api/storage-api" rel="noopener noreferrer"&gt;storage API reference&lt;/a&gt; and the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/custom-backend" rel="noopener noreferrer"&gt;custom backend guide&lt;/a&gt;. Optional version-storage methods are outside this article's two-method adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you connect the document engine to an HTTP API?
&lt;/h2&gt;

&lt;p&gt;Install Fabric.js and the document engine in your React app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;fabric fabricjs-document-engine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;api-storage.ts&lt;/code&gt;. This adapter uses same-origin endpoints and your app's existing session cookie.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;DocumentEngineError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DocumentStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiStorage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DocumentStorage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;loadDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/api/documents/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DocumentEngineError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DOCUMENT_NOT_FOUND&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This document was not found.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Opening the document failed (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;).`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;saveDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expectedRevision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/api/documents/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Expected-Revision&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nx"&gt;expectedRevision&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expectedRevision&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;412&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DocumentEngineError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SAVE_CONFLICT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Another tab or device saved this document first.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;413&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`The server refused this save (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;).`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;retryable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Saving the document failed (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;).`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The API returned an invalid revision.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;retryable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;X-Expected-Revision&lt;/code&gt; is an &lt;strong&gt;app-defined header&lt;/strong&gt;. In this example, &lt;code&gt;0&lt;/code&gt; means create a document and &lt;code&gt;*&lt;/code&gt; means explicitly overwrite an existing one. These are conventions shared by this adapter and the endpoint below; the package does not require this header.&lt;/p&gt;

&lt;p&gt;The official &lt;a href="https://github.com/re-sohail/fabricjs-document-engine/blob/12bd614d75b71b47f4e11c6de7dace40fa11081e/docs/storage-examples.md#rest-api" rel="noopener noreferrer"&gt;REST storage example&lt;/a&gt; uses &lt;code&gt;If-Match&lt;/code&gt;. This article uses a custom header to keep its numeric revision protocol separate from standard HTTP entity-tag semantics.&lt;/p&gt;

&lt;p&gt;Throwing &lt;code&gt;SAVE_CONFLICT&lt;/code&gt; tells the engine to stop retrying that save. Setting &lt;code&gt;retryable: false&lt;/code&gt; also stops retries for errors that require a change to the request or permissions. Other save failures can follow the engine's retry policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you save and open a document in React?
&lt;/h2&gt;

&lt;p&gt;First, create the Fabric.js canvas after React mounts the canvas element. Put this helper in &lt;code&gt;use-fabric-canvas.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Canvas&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabric&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useFabricCanvas&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elementRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCanvas&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Canvas&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;setCanvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create &lt;code&gt;document-editor.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Rect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabric&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isDocumentEngineError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;useDocumentEngine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useDocumentState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;apiStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./api-storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useFabricCanvas&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./use-fabric-canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DocumentEditor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFabricCanvas&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDocumentEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;design-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;First design&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDocumentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDocumentId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;design-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;busy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setBusy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;busy&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isSaving&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Document ID
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setDocumentId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
        &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Document opened.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Open&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
        &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#6366f1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Add rectangle&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
        &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Save confirmed by the API.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Save&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Current document: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;documentId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Preparing editor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;` · Revision &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; · &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;saveStatus&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;canvas&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Editable design canvas"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a rectangle and click &lt;strong&gt;Save&lt;/strong&gt;. The adapter sends the full document to &lt;code&gt;PUT /api/documents/design-42&lt;/code&gt;. Refresh the page and click &lt;strong&gt;Open&lt;/strong&gt; to retrieve it through &lt;code&gt;GET /api/documents/design-42&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The example uses manual saves to make the request flow visible. To add autosave, pass &lt;code&gt;autosave: true&lt;/code&gt; when creating the engine and continue displaying &lt;code&gt;state.saveStatus&lt;/code&gt; and &lt;code&gt;state.saveError&lt;/code&gt;. Options are read when the hook creates the engine; changing an options object later does not reconfigure that engine.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/react" rel="noopener noreferrer"&gt;React guide&lt;/a&gt;, &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/api/react-hooks" rel="noopener noreferrer"&gt;React hooks API&lt;/a&gt;, and &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/autosave" rel="noopener noreferrer"&gt;autosave guide&lt;/a&gt;. In Next.js, mount Fabric.js through the client-only setup in the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/next-js" rel="noopener noreferrer"&gt;Next.js guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the app-owned HTTP endpoint look like?
&lt;/h2&gt;

&lt;p&gt;The following &lt;strong&gt;backend code belongs to your application&lt;/strong&gt;. It is an illustrative Express 5 and PostgreSQL implementation, not an endpoint supplied by &lt;code&gt;fabricjs-document-engine&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create a table through your app's database migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;owner_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;revision&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revision&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use your existing authentication middleware as &lt;code&gt;requireUser&lt;/code&gt;. That middleware must verify the session, reject unauthenticated requests, and set &lt;code&gt;req.auth.userId&lt;/code&gt;. Neither the middleware nor &lt;code&gt;DATABASE_URL&lt;/code&gt; comes from the package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server.ts — app-owned backend, using Express 5.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;validateDocument&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FabricDocument&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;requireUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./app-auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Your verified-session middleware.&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nx"&gt;Express&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requireUser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2mb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt; &lt;span class="c1"&gt;// An app-chosen request limit.&lt;/span&gt;

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

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/documents/:id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Expected-Revision&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;0|&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;1-9&lt;/span&gt;&lt;span class="se"&gt;]\d&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INVALID_EXPECTED_REVISION&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2147483646&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INVALID_EXPECTED_REVISION&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INVALID_DOCUMENT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issues&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;FabricDocument&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DOCUMENT_ID_MISMATCH&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`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`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;params&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`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`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SAVE_CONFLICT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3001&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the API behind the same origin as the React app, or configure a development proxy for &lt;code&gt;/api&lt;/code&gt;. Apply your existing session and CSRF policy to these routes. A document ID locates a row; ownership comes from the verified session, never from &lt;code&gt;metadata.owner&lt;/code&gt; supplied by the browser.&lt;/p&gt;

&lt;p&gt;The endpoint assigns revisions and writes that value into both the JSON body and the revision column. This keeps the next &lt;code&gt;GET&lt;/code&gt; 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 &lt;code&gt;409&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;2mb&lt;/code&gt; limit is an example application limit, not a package limit. Adjust it for your documents. &lt;code&gt;validateDocument&lt;/code&gt; checks the document shape; enforce any additional object-count, URL, asset, or business rules in your backend too.&lt;/p&gt;

&lt;p&gt;The SQL follows the package's &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/custom-backend#the-server" rel="noopener noreferrer"&gt;atomic revision-check guidance&lt;/a&gt;. See PostgreSQL's &lt;a href="https://www.postgresql.org/docs/current/sql-update.html" rel="noopener noreferrer"&gt;UPDATE and RETURNING documentation&lt;/a&gt; for the database behavior. Express 5 forwards rejected async route handlers to error handling; integrate your app's normal handler using the &lt;a href="https://expressjs.com/en/guide/error-handling/" rel="noopener noreferrer"&gt;Express error-handling guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does a revision check prevent lost updates?
&lt;/h2&gt;

&lt;p&gt;Suppose two tabs open revision &lt;code&gt;4&lt;/code&gt; of the same document. Both tabs edit it. Without a server-side check, the second save can overwrite the first save without warning.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Expected revision&lt;/th&gt;
&lt;th&gt;Stored revision before request&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tab A saves&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Accepted; server stores revision &lt;code&gt;5&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tab B saves&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rejected with HTTP &lt;code&gt;409&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tab B adapter handles response&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Throws &lt;code&gt;SAVE_CONFLICT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These numbers illustrate the protocol; they are not a performance measurement.&lt;/p&gt;

&lt;p&gt;Check the revision and write the document in the same database operation. A separate &lt;code&gt;SELECT&lt;/code&gt; followed by an unconditional &lt;code&gt;UPDATE&lt;/code&gt; leaves room for another request to save between them.&lt;/p&gt;

&lt;p&gt;The engine also serializes saves within one editor session. That does not replace a database revision check across tabs, devices, or API clients.&lt;/p&gt;

&lt;p&gt;For conflict UI, offer to review the stored document or deliberately overwrite it. &lt;code&gt;engine.save({ overwrite: true })&lt;/code&gt; passes &lt;code&gt;expectedRevision: null&lt;/code&gt;; 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 &lt;code&gt;discardUnsavedChanges: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Read the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-conflicts" rel="noopener noreferrer"&gt;save conflicts guide&lt;/a&gt; for the full decision flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are image files stored inside the canvas JSON?
&lt;/h2&gt;

&lt;p&gt;A normal image object stores an address for its image. Saving that address does not upload the file. In particular, a &lt;code&gt;blob:&lt;/code&gt; URL refers to data in the current browser environment and is unsuitable for reopening on another device.&lt;/p&gt;

&lt;p&gt;Configure &lt;code&gt;assets.upload&lt;/code&gt; to turn tab-only images into durable URLs while the engine prepares a save:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Add this assets option when creating the engine.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Blob&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/uploads&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The image could not be uploaded.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/api/uploads&lt;/code&gt; is another &lt;strong&gt;app-owned endpoint&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;Avoid treating an expiring signed URL as a permanent asset identity. The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts" rel="noopener noreferrer"&gt;images and fonts guide&lt;/a&gt; explains &lt;code&gt;assets.upload&lt;/code&gt; and &lt;code&gt;assets.resolveUrl&lt;/code&gt;, including how to resolve stored asset addresses when opening a document.&lt;/p&gt;

&lt;p&gt;If a stored image cannot be opened, see the companion article, &lt;a href="//./fabricjs-loadfromjson-failed-image.md"&gt;Why Fabric.js loadFromJSON Can Leave Your Editor Half-Loaded&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should you check before shipping this save flow?
&lt;/h2&gt;

&lt;p&gt;Verify the complete round trip, including failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reopen the document.&lt;/strong&gt; Save, refresh, and open the same ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare the content.&lt;/strong&gt; Check object IDs, canvas dimensions, and stacking order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a conflict.&lt;/strong&gt; Open two tabs; confirm the older revision cannot overwrite the newer save.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject invalid input.&lt;/strong&gt; Send a mismatched document ID and malformed document body.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check ownership.&lt;/strong&gt; Confirm another account cannot read or write the document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interrupt a request.&lt;/strong&gt; Confirm the UI does not claim a failed save succeeded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reopen uploaded images.&lt;/strong&gt; Test from a fresh browser session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/verify-adapter" rel="noopener noreferrer"&gt;adapter verification guide&lt;/a&gt; helps check storage behavior. The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/production/production-checklist" rel="noopener noreferrer"&gt;production checklist&lt;/a&gt; covers the wider editor lifecycle.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;1&lt;/code&gt;, and an older editor's save produced &lt;code&gt;SAVE_CONFLICT&lt;/code&gt;. The HTTP/PostgreSQL endpoint is an illustrative implementation; connect and test it with your app's actual authentication and database.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you use MongoDB, MySQL, or a different backend?
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you open JSON saved by an older Fabric.js editor?
&lt;/h2&gt;

&lt;p&gt;The engine can recognize plain Fabric.js JSON through &lt;code&gt;load&lt;/code&gt;, &lt;code&gt;loadDocument&lt;/code&gt;, and &lt;code&gt;importFabricJson&lt;/code&gt;. A document loaded by &lt;code&gt;engine.load(id)&lt;/code&gt; retains that requested ID, and its next save uses the engine's current document format.&lt;/p&gt;

&lt;p&gt;Check the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/migration" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does calling &lt;code&gt;toDocument()&lt;/code&gt; mean the canvas has been saved?
&lt;/h2&gt;

&lt;p&gt;No. &lt;code&gt;engine.toDocument()&lt;/code&gt; produces a document object. &lt;code&gt;engine.save()&lt;/code&gt; uses the configured storage adapter and updates the engine's save state after storage accepts the write.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;toDocument()&lt;/code&gt; when you need a snapshot. Use &lt;code&gt;save()&lt;/code&gt; for the managed persistence flow shown here.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://www.npmjs.com/package/fabricjs-document-engine" rel="noopener noreferrer"&gt;npm package&lt;/a&gt;, &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/overview/quick-start" rel="noopener noreferrer"&gt;quick start&lt;/a&gt;, and &lt;a href="https://github.com/re-sohail/fabricjs-document-engine" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; contain the implementation and related examples.&lt;/p&gt;

</description>
      <category>api</category>
      <category>javascript</category>
      <category>react</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Fabric.js Editor Problems: Save, Load, Undo, Autosave, and Recovery</title>
      <dc:creator>Sohail Khan</dc:creator>
      <pubDate>Wed, 30 Sep 2026 06:47:52 +0000</pubDate>
      <link>https://dev.to/re-sohail/fabricjs-editor-problems-save-load-undo-autosave-and-recovery-5a79</link>
      <guid>https://dev.to/re-sohail/fabricjs-editor-problems-save-load-undo-autosave-and-recovery-5a79</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fytfp9lmyldfpx1okzuwx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fytfp9lmyldfpx1okzuwx.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;These are the problems &lt;a href="https://www.npmjs.com/package/fabricjs-document-engine" rel="noopener noreferrer"&gt;fabricjs-document-engine&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-and-load" rel="noopener noreferrer"&gt;save and load tutorial&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What a developer searches for&lt;/th&gt;
&lt;th&gt;What is happening&lt;/th&gt;
&lt;th&gt;Where the package helps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;“Fabric.js undo redo not working”&lt;/td&gt;
&lt;td&gt;History must be built around object changes and asynchronous loads.&lt;/td&gt;
&lt;td&gt;Action-level history, &lt;code&gt;undo()&lt;/code&gt;, &lt;code&gt;redo()&lt;/code&gt;, transactions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Fabric.js custom properties lost after JSON load”&lt;/td&gt;
&lt;td&gt;Extra fields need explicit serialization and class registration.&lt;/td&gt;
&lt;td&gt;Register the class and its saved properties once.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Fabric.js object reference lost after undo”&lt;/td&gt;
&lt;td&gt;Reloading creates new object instances.&lt;/td&gt;
&lt;td&gt;Stable IDs and &lt;code&gt;getObjectById()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Fabric.js loadFromJSON missing image”&lt;/td&gt;
&lt;td&gt;Saved image URLs may expire or fail to load.&lt;/td&gt;
&lt;td&gt;Check assets before replacing the current canvas; report missing URLs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Fabric.js autosave overwrites newer changes”&lt;/td&gt;
&lt;td&gt;Requests and edits can overlap.&lt;/td&gt;
&lt;td&gt;One in-flight save, a follow-up save, and revision checks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Fabric.js canvas lost after refresh”&lt;/td&gt;
&lt;td&gt;Unsaved edits never reached durable storage.&lt;/td&gt;
&lt;td&gt;Optional local recovery checkpoints and a restore flow.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Fabric.js tainted canvas export”&lt;/td&gt;
&lt;td&gt;Browser CORS rules block raster output.&lt;/td&gt;
&lt;td&gt;Export preflight reports the problem; your image host must allow CORS.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are &lt;strong&gt;search phrases, not claims that every report is a Fabric.js core bug&lt;/strong&gt;. Many are integration problems that appear when an application grows from a drawing canvas into an editor. The &lt;a href="https://fabricjs.com/api/classes/canvas/" rel="noopener noreferrer"&gt;Fabric.js API&lt;/a&gt; already includes serialization and image export. The package coordinates them into a document lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a canvas and a document
&lt;/h2&gt;

&lt;p&gt;Install the package with a supported Fabric version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;fabric@^7 fabricjs-document-engine@1.0.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a small setup using the built-in browser storage adapter. The HTML page needs &lt;code&gt;&amp;lt;canvas id="editor"&amp;gt;&amp;lt;/canvas&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Rect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabric&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDocumentEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createLocalStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine/storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;editor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#f8fafc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createDocumentEngine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;poster-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createLocalStorage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-editor:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;autosave&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxWait&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#4f46e5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;document&lt;/code&gt; option gives this design a known ID. &lt;code&gt;createLocalStorage&lt;/code&gt; is useful for a prototype on one browser. For a product with accounts or multiple devices, provide a &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/custom-backend" rel="noopener noreferrer"&gt;backend storage adapter&lt;/a&gt;. The engine does not host your files or create your API.&lt;/p&gt;

&lt;p&gt;You can also use &lt;code&gt;engine.toDocument()&lt;/code&gt; and &lt;code&gt;engine.loadDocument(json)&lt;/code&gt; without any adapter. Those calls serialize and open a document you store yourself. &lt;code&gt;engine.save()&lt;/code&gt;, &lt;code&gt;engine.load(id)&lt;/code&gt;, autosave, and versions need an adapter. See the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/api/create-document-engine" rel="noopener noreferrer"&gt;creation options&lt;/a&gt; and &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/storage-adapters" rel="noopener noreferrer"&gt;storage guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. “Fabric.js undo redo not working”
&lt;/h2&gt;

&lt;p&gt;Fabric.js has no built-in document history. A developer in &lt;a href="https://github.com/fabricjs/fabric.js/issues/10011" rel="noopener noreferrer"&gt;Fabric.js issue #10011&lt;/a&gt; tried to add history to a Fabric 6 editor. The thread includes two familiar problems: older callback-style &lt;code&gt;loadFromJSON&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;In this snippet, &lt;code&gt;undoButton&lt;/code&gt; and &lt;code&gt;redoButton&lt;/code&gt; are your toolbar's button elements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;undoButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;canUndo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;redoButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;canRedo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;undoButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;undo&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nx"&gt;redoButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redo&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A detail that matters:&lt;/strong&gt; calling &lt;code&gt;object.set()&lt;/code&gt; in your own code does not necessarily fire a Fabric event. If your toolbar changes several properties, group them yourself:&lt;/p&gt;

&lt;p&gt;Here &lt;code&gt;card&lt;/code&gt; is the Fabric object your editor is changing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Move and recolor card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#f97316&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCoords&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestRenderAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That whole command becomes one labelled undo step. You can also make changes first and call &lt;code&gt;engine.commit("Recolor card")&lt;/code&gt;. Read the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/undo-redo" rel="noopener noreferrer"&gt;undo and redo guide&lt;/a&gt; for the automatic events, keyboard shortcuts, and history limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. “Fabric.js object reference lost after undo or reload”
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="https://github.com/fabricjs/fabric.js/issues/6799" rel="noopener noreferrer"&gt;issue #6799&lt;/a&gt; also shows how group-relative transforms make object state harder to reason about.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;undo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getObjectById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentCard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;currentCard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fill&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#2563eb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestRenderAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Recolor card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This solves &lt;strong&gt;identity across document operations&lt;/strong&gt;. 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 &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/document-format" rel="noopener noreferrer"&gt;document format&lt;/a&gt; explains object IDs and order.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. “Fabric.js custom properties lost after JSON load”
&lt;/h2&gt;

&lt;p&gt;An editor often adds fields such as &lt;code&gt;label&lt;/code&gt;, &lt;code&gt;databaseId&lt;/code&gt;, or &lt;code&gt;lockedBy&lt;/code&gt;. Fabric.js supports custom serialization, but you must configure it. The request in &lt;a href="https://github.com/fabricjs/fabric.js/issues/10887" rel="noopener noreferrer"&gt;issue #10887&lt;/a&gt; describes the repeated setup developers face when storing application metadata on objects. Its native &lt;a href="https://fabricjs.com/docs/using-custom-properties/" rel="noopener noreferrer"&gt;custom properties guide&lt;/a&gt; is also worth reading.&lt;/p&gt;

&lt;p&gt;For a custom class, register its type and the properties that must survive a document round trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Rect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabric&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDocumentEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Sticker&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Rect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sticker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createDocumentEngine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;customObjects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fabricClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Sticker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;label&lt;/code&gt; on a &lt;code&gt;Sticker&lt;/code&gt;, save, and load it again. You do not have to remember to list that property on every &lt;code&gt;toJSON()&lt;/code&gt; call. Register the class &lt;strong&gt;before&lt;/strong&gt; you load a document that uses it. Otherwise, the load fails with &lt;code&gt;UNKNOWN_OBJECT_TYPE&lt;/code&gt; instead of quietly changing it to another shape. See &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/custom-objects" rel="noopener noreferrer"&gt;custom objects&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This feature is for Fabric classes and object fields. Put whole-document information such as a title or customer ID in document &lt;code&gt;metadata&lt;/code&gt;, using &lt;code&gt;engine.updateMetadata({ title: "Summer poster" })&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. “Fabric.js loadFromJSON failed” or “the old canvas disappeared”
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The engine validates and checks the incoming document before replacing the current canvas. An unknown type fails with &lt;code&gt;UNKNOWN_OBJECT_TYPE&lt;/code&gt;. A missing image fails with &lt;code&gt;MISSING_ASSETS&lt;/code&gt;. If B starts after A, A is cancelled with &lt;code&gt;LOAD_ABORTED&lt;/code&gt;, so its late response cannot replace B. The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-and-load" rel="noopener noreferrer"&gt;save and load guide&lt;/a&gt; documents these rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;poster-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Poster opened&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LOAD_ABORTED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UNSAVED_CHANGES&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Save or discard your current edits first&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Could not open the poster: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When storage is configured, the engine also refuses to replace unsaved work unless the user has chosen to discard it. Do not pass &lt;code&gt;discardUnsavedChanges: true&lt;/code&gt; automatically. A failed load should leave the design people were editing on screen. The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/api/error-codes" rel="noopener noreferrer"&gt;error code reference&lt;/a&gt; lists the cases your UI can handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. “Fabric.js autosave overwrites newer changes”
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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. &lt;code&gt;engine.isDirty()&lt;/code&gt; and &lt;code&gt;engine.getSaveState()&lt;/code&gt; let you show what is saved. Temporary failures can be retried. The exact timing and retry options are in the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/autosave" rel="noopener noreferrer"&gt;autosave guide&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;save:status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isDirty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;revision&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statusElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; · revision &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;saveButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isDirty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One tab is only half the problem.&lt;/strong&gt; If another tab or device has saved the same document, a correct adapter compares revisions and reports &lt;code&gt;SAVE_CONFLICT&lt;/code&gt;. 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 &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-conflicts" rel="noopener noreferrer"&gt;save conflicts&lt;/a&gt; and the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/custom-backend" rel="noopener noreferrer"&gt;custom backend example&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Put the same save rules on your backend
&lt;/h3&gt;

&lt;p&gt;For an account-based editor, write a storage adapter around your API. This TypeScript example assumes your server returns a document on &lt;code&gt;GET&lt;/code&gt;, and &lt;code&gt;{ revision: number }&lt;/code&gt; on a successful &lt;code&gt;PUT&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DocumentStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DocumentStorage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;loadDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/documents/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Load failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;saveDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expectedRevision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/api/documents/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;If-Match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expectedRevision&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Saved in another tab&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SAVE_CONFLICT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Save failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// { revision: number }&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server must check &lt;code&gt;expectedRevision&lt;/code&gt; against the stored revision &lt;strong&gt;as part of the write&lt;/strong&gt;, 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 &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/custom-backend" rel="noopener noreferrer"&gt;backend guide&lt;/a&gt; and &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/storage/verify-adapter" rel="noopener noreferrer"&gt;adapter verification guide&lt;/a&gt; cover the full contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. “Fabric.js image missing after load” and “my font changed”
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;blob:&lt;/code&gt; URL will disappear after refresh. A web font might not be ready when Fabric measures text.&lt;/p&gt;

&lt;p&gt;The engine records an asset manifest with the image and font variants a document uses. It checks them before loading. &lt;code&gt;MISSING_ASSETS&lt;/code&gt; 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 &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts" rel="noopener noreferrer"&gt;asset options&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkAssets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;missingImages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unavailableFonts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// If a stored image URL has moved:&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/old-logo.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/new-logo.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package can &lt;strong&gt;tell you what is missing&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. “Fabric.js canvas lost after refresh or crash”
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createIndexedDbRecovery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fabricjs-document-engine/recovery&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createDocumentEngine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;recovery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createIndexedDbRecovery&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recoverable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRecoverableDocuments&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recoverable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Restore unsaved work?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restoreRecovery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recoverable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/recovery" rel="noopener noreferrer"&gt;recovery guide&lt;/a&gt; for checkpoint timing and temporary images.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. “Fabric.js tainted canvas cannot be exported”
&lt;/h2&gt;

&lt;p&gt;A Fabric.js user reported that exporting after &lt;code&gt;loadFromJSON&lt;/code&gt; hit a tainted-canvas error in &lt;a href="https://github.com/fabricjs/fabric.js/issues/5429" rel="noopener noreferrer"&gt;issue #5429&lt;/a&gt;. This is a browser security rule when a remote image is drawn without the required cross-origin permission. The package cannot bypass that rule.&lt;/p&gt;

&lt;p&gt;It can check before export and give you a useful problem list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preflightExport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;showProblems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transparent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// Download or upload result.blob.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;EXPORT_BLOCKED&lt;/code&gt;; successful export does not change the editable canvas or its undo state. See &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/export" rel="noopener noreferrer"&gt;export&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To actually fix a cross-origin image, load it with appropriate &lt;code&gt;crossOrigin&lt;/code&gt; settings &lt;strong&gt;and&lt;/strong&gt; configure its server to send the correct &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header. See the package &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/production/troubleshooting" rel="noopener noreferrer"&gt;troubleshooting guide&lt;/a&gt;. Switching packages will not remove the browser's CORS requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. “How do I keep versions of a Fabric.js design?”
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Approved by client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listVersions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Later, after the user chooses to restore it:&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restoreVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/versions" rel="noopener noreferrer"&gt;versions guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. “How do I open my old Fabric.js JSON?”
&lt;/h2&gt;

&lt;p&gt;You may already have designs saved with &lt;code&gt;canvas.toJSON()&lt;/code&gt;. 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importFabricJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldJsonText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;imported-poster-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;previous editor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The format records a &lt;code&gt;schemaVersion&lt;/code&gt;, canvas dimensions and background, objects in stacking order, assets, and document metadata. The &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/migration" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; 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 &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/document-format" rel="noopener noreferrer"&gt;document format reference&lt;/a&gt; lists the fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. “Can I safely open JSON uploaded by another user?”
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/security" rel="noopener noreferrer"&gt;security guide&lt;/a&gt; explains the checks and configuration.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. “Does it work with React, Next.js, Vue, or Svelte?”
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The package documents each setup: &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/react" rel="noopener noreferrer"&gt;React&lt;/a&gt;, &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/next-js" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt;, &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/vue" rel="noopener noreferrer"&gt;Vue&lt;/a&gt;, &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/svelte" rel="noopener noreferrer"&gt;Svelte&lt;/a&gt;, and &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/frameworks/vanilla-js" rel="noopener noreferrer"&gt;plain JavaScript&lt;/a&gt;. These integrations connect the document engine to your app; they do not supply an editor UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the package does not solve
&lt;/h2&gt;

&lt;p&gt;A dependable article should say where the boundary is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does not draw or style your toolbar, selection controls, or pages. Fabric.js and your app own the canvas UI.&lt;/li&gt;
&lt;li&gt;It does not provide a hosted database, image bucket, or backend authentication.&lt;/li&gt;
&lt;li&gt;It does not make a deleted remote image available again or bypass CORS.&lt;/li&gt;
&lt;li&gt;It does not provide real-time multi-user collaboration or automatic merge of two people's changes. A save conflict needs a product decision.&lt;/li&gt;
&lt;li&gt;It does not export PDF directly. Use its image or SVG output with a separate PDF library if needed.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to decide if you need it
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can I identify the same object after reopening and undoing?&lt;/li&gt;
&lt;li&gt;Can a failed load or a slow save erase good work?&lt;/li&gt;
&lt;li&gt;Can I tell the user exactly why an image, font, or export failed?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If those matter, a document layer is worth considering. Start with the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/overview/quick-start" rel="noopener noreferrer"&gt;quick start&lt;/a&gt;, run the &lt;a href="https://fabricjs-document-engine.jscrate.dev/" rel="noopener noreferrer"&gt;live examples&lt;/a&gt;, and test your own documents against the &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/production/production-checklist" rel="noopener noreferrer"&gt;production checklist&lt;/a&gt;. The code is on &lt;a href="https://github.com/re-sohail/fabricjs-document-engine" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, and the released package is on &lt;a href="https://www.npmjs.com/package/fabricjs-document-engine" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the failure, not just the happy path
&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save, refresh, and compare object IDs, stacking order, background, and metadata.&lt;/li&gt;
&lt;li&gt;Undo a drag and a toolbar-driven change. Confirm each takes one step and the current object can be found by ID.&lt;/li&gt;
&lt;li&gt;Delay two document loads. Confirm the later choice stays on screen.&lt;/li&gt;
&lt;li&gt;Break an image URL. Confirm the old canvas stays visible and the missing URL is reported.&lt;/li&gt;
&lt;li&gt;Delay a save, edit again, and confirm the final stored document contains the latest edit.&lt;/li&gt;
&lt;li&gt;Open the same document in two tabs. Save in one, then confirm the other shows a conflict.&lt;/li&gt;
&lt;li&gt;Make an unsaved edit, refresh, and check the recovery choice.&lt;/li&gt;
&lt;li&gt;Try a remote image without CORS. Confirm export preflight reports the blocker.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These tests tell you whether the package and &lt;strong&gt;your storage, assets, and UI&lt;/strong&gt; work together. A feature list alone cannot answer that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Original sources and further reading
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fabric.js problem reports and official API&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/fabricjs/fabric.js/issues/10011" rel="noopener noreferrer"&gt;Fabric.js issue #10011: undo/redo history in Fabric 6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fabricjs/fabric.js/issues/6799" rel="noopener noreferrer"&gt;Fabric.js issue #6799: object state inside groups&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fabricjs/fabric.js/issues/10887" rel="noopener noreferrer"&gt;Fabric.js issue #10887: application metadata and JSON serialization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fabricjs/fabric.js/issues/5429" rel="noopener noreferrer"&gt;Fabric.js issue #5429: tainted canvas after JSON load&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fabricjs.com/api/classes/canvas/" rel="noopener noreferrer"&gt;Fabric.js Canvas API&lt;/a&gt; and &lt;a href="https://fabricjs.com/docs/using-custom-properties/" rel="noopener noreferrer"&gt;custom properties guide&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Package implementation and documentation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/fabricjs-document-engine" rel="noopener noreferrer"&gt;fabricjs-document-engine on npm&lt;/a&gt; and &lt;a href="https://github.com/re-sohail/fabricjs-document-engine" rel="noopener noreferrer"&gt;source repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/api/document-engine" rel="noopener noreferrer"&gt;DocumentEngine API and error codes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/save-and-load" rel="noopener noreferrer"&gt;Save/load&lt;/a&gt;, &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/undo-redo" rel="noopener noreferrer"&gt;undo/redo&lt;/a&gt;, &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/autosave" rel="noopener noreferrer"&gt;autosave&lt;/a&gt;, and &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts" rel="noopener noreferrer"&gt;assets/fonts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/recovery" rel="noopener noreferrer"&gt;Recovery&lt;/a&gt;, &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/versions" rel="noopener noreferrer"&gt;versions&lt;/a&gt;, &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/export" rel="noopener noreferrer"&gt;export&lt;/a&gt;, and &lt;a href="https://fabricjs-document-engine.jscrate.dev/docs/guides/document-format" rel="noopener noreferrer"&gt;document format&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Editorial note: Use the problem phrases above because they describe the reader's task. No title, keyword placement, article length, or package mention guarantees a Google ranking. The working examples, clear limitations, and original issue context are what make this page useful.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>softwaredevelopment</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Add a Production-Ready React Color Picker in 2026: Forms, Images, Eyedropper, Gradients &amp; OKLCH</title>
      <dc:creator>Sohail Khan</dc:creator>
      <pubDate>Thu, 24 Sep 2026 06:21:30 +0000</pubDate>
      <link>https://dev.to/re-sohail/how-to-add-a-production-ready-react-color-picker-in-2026-forms-images-eyedropper-gradients--2d7a</link>
      <guid>https://dev.to/re-sohail/how-to-add-a-production-ready-react-color-picker-in-2026-forms-images-eyedropper-gradients--2d7a</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcd5o0s19sdiqqtgo6lv5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcd5o0s19sdiqqtgo6lv5.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adding a &lt;strong&gt;React color picker&lt;/strong&gt; looks easy at first.&lt;/p&gt;

&lt;p&gt;You install a component, pass it a color value, listen for changes, and you are done.&lt;/p&gt;

&lt;p&gt;That works when all you need is a simple HEX color.&lt;/p&gt;

&lt;p&gt;But real applications usually need more.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That is where a basic color input starts becoming a complete color workflow.&lt;/p&gt;

&lt;p&gt;In this guide, I’ll show how to add a production-ready React color picker using &lt;a href="https://chroma-panel.jscrate.dev/" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt; and gradually add the features that a real application may need.&lt;/p&gt;

&lt;p&gt;ChromaPanel is an &lt;a href="https://github.com/re-sohail/chroma-panel" rel="noopener noreferrer"&gt;open-source React color picker&lt;/a&gt; with five picking modes, image sampling, an eyedropper, gradients, modern CSS color support, accessibility, native form behavior, TypeScript support, and zero runtime dependencies.&lt;/p&gt;

&lt;p&gt;You can install it from &lt;a href="https://www.npmjs.com/package/chroma-panel" rel="noopener noreferrer"&gt;npm&lt;/a&gt; and start with only a few lines of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the React color picker
&lt;/h2&gt;

&lt;p&gt;Start by installing the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;chroma-panel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then import &lt;code&gt;ColorInput&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ColorInput&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BrandColor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#3366cc"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a working &lt;a href="https://chroma-panel.jscrate.dev/react/components/color-input" rel="noopener noreferrer"&gt;React color input&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The user sees a color swatch. Clicking it opens the picker.&lt;/p&gt;

&lt;p&gt;There is no provider to wrap around your application and no separate stylesheet required for the standard setup.&lt;/p&gt;

&lt;p&gt;For a settings form or a simple customization page, this may already be enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a controlled React color picker
&lt;/h2&gt;

&lt;p&gt;Most React applications eventually need the selected color somewhere else.&lt;/p&gt;

&lt;p&gt;Maybe you want to update a preview, save a theme, change a button color, or store the value in your API.&lt;/p&gt;

&lt;p&gt;In that case, use the normal controlled React pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ColorInput&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ThemeColor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setColor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#7c3aed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Theme color"&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ChromaPanel supports both &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/controlled" rel="noopener noreferrer"&gt;controlled and uncontrolled usage&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One useful detail is that &lt;code&gt;onChange&lt;/code&gt; and &lt;code&gt;onChangeComplete&lt;/code&gt; solve different problems.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;onChange&lt;/code&gt; runs continuously while the user is changing the color.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;onChangeComplete&lt;/code&gt; runs once when that interaction finishes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onChangeComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;saveTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means you can update your UI instantly without making a database request on every tiny movement.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;onChange&lt;/code&gt; for the live experience.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;onChangeComplete&lt;/code&gt; for saving, network calls, history entries, or other expensive work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the React color picker work with forms
&lt;/h2&gt;

&lt;p&gt;This is one feature that often gets ignored when developers add a color picker.&lt;/p&gt;

&lt;p&gt;A color picker is still an input.&lt;/p&gt;

&lt;p&gt;If it is sitting inside a profile form, settings form, onboarding page, or admin panel, it should work with the rest of that form.&lt;/p&gt;

&lt;p&gt;ChromaPanel's &lt;code&gt;ColorInput&lt;/code&gt; can behave like a normal form field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;saveSettings&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"brand-color"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Brand color
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"brand-color"&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"brandColor"&lt;/span&gt;
    &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#2563eb"&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hex"&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;
  &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Save settings
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value can be submitted with the surrounding form.&lt;/p&gt;

&lt;p&gt;It also supports things such as &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;disabled&lt;/code&gt;, labels, form reset behavior, and different output formats.&lt;/p&gt;

&lt;p&gt;You can read the full &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/forms" rel="noopener noreferrer"&gt;React color picker form guide&lt;/a&gt; if your application relies heavily on native forms or React form libraries.&lt;/p&gt;

&lt;p&gt;This is useful for:&lt;/p&gt;

&lt;p&gt;settings pages, theme configuration, SaaS dashboards, account customization, CMS interfaces, admin panels, and onboarding flows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show the full color picker inline
&lt;/h2&gt;

&lt;p&gt;A popover works well when color is only one field in a larger form.&lt;/p&gt;

&lt;p&gt;A design tool is different.&lt;/p&gt;

&lt;p&gt;If users are working with color constantly, you probably want the panel to stay visible.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;ChromaPanel&lt;/code&gt; directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChromaPanel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ThemeEditor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChromaPanel&lt;/span&gt;
      &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#3366cc"&lt;/span&gt;
      &lt;span class="na"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wheel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sliders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;palettes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;showTitleBar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://chroma-panel.jscrate.dev/react/components/chroma-panel" rel="noopener noreferrer"&gt;ChromaPanel component&lt;/a&gt; gives you the same color system without hiding it inside a popover.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the color picker modes you actually need
&lt;/h2&gt;

&lt;p&gt;Not every user chooses color the same way.&lt;/p&gt;

&lt;p&gt;Someone may want to explore visually.&lt;/p&gt;

&lt;p&gt;Someone else knows the exact RGB value.&lt;/p&gt;

&lt;p&gt;Another user only wants to select from approved brand colors.&lt;/p&gt;

&lt;p&gt;ChromaPanel provides five different ways to choose a color.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://chroma-panel.jscrate.dev/react/modes/wheel" rel="noopener noreferrer"&gt;color wheel&lt;/a&gt; gives users a visual way to explore hue and saturation.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://chroma-panel.jscrate.dev/react/modes/sliders" rel="noopener noreferrer"&gt;RGB, HSL and HSB sliders&lt;/a&gt; provide precise channel-level control.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://chroma-panel.jscrate.dev/react/modes/palettes" rel="noopener noreferrer"&gt;palette mode&lt;/a&gt; provides searchable predefined colors and can also use your own brand palette.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://chroma-panel.jscrate.dev/react/modes/pencils" rel="noopener noreferrer"&gt;pencils mode&lt;/a&gt; provides a large 120-color swatch grid.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image" rel="noopener noreferrer"&gt;image color picker&lt;/a&gt; lets users select colors from uploaded images.&lt;/p&gt;

&lt;p&gt;You decide which ones should appear.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChromaPanel&lt;/span&gt;
  &lt;span class="na"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wheel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sliders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;palettes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple settings page may only need the wheel.&lt;/p&gt;

&lt;p&gt;A design tool may need all of them.&lt;/p&gt;

&lt;p&gt;A company branding screen may only need an approved palette.&lt;/p&gt;

&lt;p&gt;That is better than forcing every user to work with the same large interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add an image color picker to React
&lt;/h2&gt;

&lt;p&gt;This is where color picking becomes much more useful for creative applications.&lt;/p&gt;

&lt;p&gt;Imagine a user uploads a logo.&lt;/p&gt;

&lt;p&gt;They want the application theme to match that logo.&lt;/p&gt;

&lt;p&gt;Instead of making them manually find each HEX value, the application can extract the dominant colors automatically.&lt;/p&gt;

&lt;p&gt;ChromaPanel's &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image" rel="noopener noreferrer"&gt;image mode&lt;/a&gt; allows the user to drop, paste, or select an image.&lt;/p&gt;

&lt;p&gt;The picker can extract dominant colors and show them as swatches.&lt;/p&gt;

&lt;p&gt;The user can also sample an exact pixel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChromaPanel&lt;/span&gt;
  &lt;span class="na"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#3366cc"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use the image API without showing the full React color picker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;extractPalette&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;swatches&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;extractPalette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;maxColors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That opens up other use cases.&lt;/p&gt;

&lt;p&gt;You could generate a theme from a company logo.&lt;/p&gt;

&lt;p&gt;You could create a palette from a product photograph.&lt;/p&gt;

&lt;p&gt;You could extract avatar colors.&lt;/p&gt;

&lt;p&gt;You could build image editing tools.&lt;/p&gt;

&lt;p&gt;You could automatically suggest colors after a user uploads an asset.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add an eyedropper to your React color picker
&lt;/h2&gt;

&lt;p&gt;Sometimes the color already exists somewhere on the screen.&lt;/p&gt;

&lt;p&gt;The user simply wants to grab it.&lt;/p&gt;

&lt;p&gt;ChromaPanel includes &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper" rel="noopener noreferrer"&gt;eyedropper support&lt;/a&gt; through the browser EyeDropper API.&lt;/p&gt;

&lt;p&gt;Where the browser supports it, the user can choose a color from somewhere else on their screen.&lt;/p&gt;

&lt;p&gt;This works especially well in:&lt;/p&gt;

&lt;p&gt;design tools, website builders, theme editors, graphics applications, and brand customization interfaces.&lt;/p&gt;

&lt;p&gt;You can use the eyedropper through the normal picker interface or use the &lt;code&gt;useEyedropper&lt;/code&gt; hook when you want to create your own button.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a gradient color picker
&lt;/h2&gt;

&lt;p&gt;Solid colors are enough for many forms.&lt;/p&gt;

&lt;p&gt;They are not enough for every design tool.&lt;/p&gt;

&lt;p&gt;For gradients, ChromaPanel provides a separate &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor" rel="noopener noreferrer"&gt;GradientEditor&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;GradientEditor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gradientToCss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel/gradient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It supports linear and radial gradients.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Keeping these features separate is useful because a production React color picker should not force every feature into every screen.&lt;/p&gt;

&lt;p&gt;Gradient color pickers remain their own active search category in the React ecosystem, especially for design and customization tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Work with OKLCH and modern CSS colors
&lt;/h2&gt;

&lt;p&gt;A few years ago, most web color interfaces stopped at:&lt;/p&gt;

&lt;p&gt;HEX, RGB, HSL, and HSV.&lt;/p&gt;

&lt;p&gt;Modern CSS has moved further.&lt;/p&gt;

&lt;p&gt;OKLCH, OKLab, Lab, LCH, and Display P3 are increasingly useful in design systems and modern frontend work.&lt;/p&gt;

&lt;p&gt;ChromaPanel includes &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4" rel="noopener noreferrer"&gt;CSS Color 4 utilities&lt;/a&gt; for those workflows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;parseColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isInGamut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;mapToGamut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;serializeColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel/color&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oklch(72% 0.18 250)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isInGamut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;srgb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapToGamut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;srgb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;serializeColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These utilities can parse, convert, map, and serialize modern CSS colors.&lt;/p&gt;

&lt;p&gt;They do not require the React UI.&lt;/p&gt;

&lt;p&gt;That means the color engine can also be useful elsewhere in your application.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility should be part of the color picker
&lt;/h2&gt;

&lt;p&gt;A color picker is naturally visual.&lt;/p&gt;

&lt;p&gt;That makes accessibility especially important.&lt;/p&gt;

&lt;p&gt;Users should not need a mouse just to change a color.&lt;/p&gt;

&lt;p&gt;ChromaPanel's &lt;a href="https://chroma-panel.jscrate.dev/react/overview/accessibility" rel="noopener noreferrer"&gt;accessibility implementation&lt;/a&gt; includes keyboard-operable controls and screen-reader behavior.&lt;/p&gt;

&lt;p&gt;Color channels use real range inputs.&lt;/p&gt;

&lt;p&gt;Keyboard users can change values with normal input behavior.&lt;/p&gt;

&lt;p&gt;Focus is managed when the popover opens and closes.&lt;/p&gt;

&lt;p&gt;Swatches have accessible names.&lt;/p&gt;

&lt;p&gt;The component also accounts for reduced-motion and forced-color preferences.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use ChromaPanel with Next.js
&lt;/h2&gt;

&lt;p&gt;A modern React package also needs to work inside server-rendered applications.&lt;/p&gt;

&lt;p&gt;ChromaPanel documents &lt;a href="https://chroma-panel.jscrate.dev/react/frameworks/next-js" rel="noopener noreferrer"&gt;Next.js and server-rendering support&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Browser-dependent parts are separated so the package can be used safely in applications using React server rendering.&lt;/p&gt;

&lt;p&gt;If the picker lives inside an interactive Next.js component, you can use it from a client component as expected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ColorInput&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BrandColorPicker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt;
      &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#3366cc"&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes &lt;strong&gt;Next.js color picker&lt;/strong&gt; another useful search path for developers who are not simply looking for a generic React component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use it with Tailwind CSS and shadcn/ui
&lt;/h2&gt;

&lt;p&gt;Many React applications now build their UI around Tailwind CSS or shadcn/ui.&lt;/p&gt;

&lt;p&gt;ChromaPanel has a &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/tailwind" rel="noopener noreferrer"&gt;Tailwind CSS styling guide&lt;/a&gt; and a dedicated &lt;a href="https://chroma-panel.jscrate.dev/react/integrations/shadcn-ui" rel="noopener noreferrer"&gt;shadcn/ui integration guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You are not required to install shadcn, Radix, or another UI library.&lt;/p&gt;

&lt;p&gt;But if your application already uses them, you can make your existing Popover, Dialog, or Sheet own the surrounding interface and render ChromaPanel inside.&lt;/p&gt;

&lt;p&gt;That matters because &lt;strong&gt;shadcn color picker&lt;/strong&gt; is now a meaningful search category of its own. Current component libraries are publishing color-picker recipes specifically for shadcn and Next.js projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep your React color picker bundle under control
&lt;/h2&gt;

&lt;p&gt;More features usually mean more JavaScript.&lt;/p&gt;

&lt;p&gt;That is why entry points matter.&lt;/p&gt;

&lt;p&gt;The full ChromaPanel configuration with all five modes is measured at about &lt;strong&gt;23.1 kB gzipped&lt;/strong&gt; in the project's Vite production measurement.&lt;/p&gt;

&lt;p&gt;The panel shell plus one mode is around &lt;strong&gt;15 kB gzipped&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you only need one or two modes, import the panel shell and those modes explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChromaPanel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel/panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;wheelMode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel/modes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SmallPicker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChromaPanel&lt;/span&gt;
      &lt;span class="na"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;wheelMode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can read more about &lt;a href="https://chroma-panel.jscrate.dev/react/utils/entry-points" rel="noopener noreferrer"&gt;ChromaPanel entry points and tree shaking&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is especially useful when you need the ChromaPanel API but not image picking, palettes, pencils, and every other mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a smaller React color picker is better
&lt;/h2&gt;

&lt;p&gt;More functionality is not automatically better.&lt;/p&gt;

&lt;p&gt;If your requirement is simply:&lt;/p&gt;

&lt;p&gt;“Let the user choose one HEX color.”&lt;/p&gt;

&lt;p&gt;then you may not need image sampling, gradients, OKLCH, an eyedropper, palettes, or a complete color engine.&lt;/p&gt;

&lt;p&gt;A smaller React color picker may be the better choice.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;react-colorful&lt;/code&gt; specifically focuses on providing a small, accessible, zero-dependency core picker. Its current npm documentation describes the picker as about 3.1 kB gzipped.&lt;/p&gt;

&lt;p&gt;ChromaPanel makes more sense when your application needs the wider workflow.&lt;/p&gt;

&lt;p&gt;The right question is not:&lt;/p&gt;

&lt;p&gt;“Which React color picker has the most features?”&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;p&gt;“What will users actually need to do with color in this application?”&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple React color picker can grow with your app
&lt;/h2&gt;

&lt;p&gt;You may start here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt;
  &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#3366cc"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six months later, the same product may need:&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Replacing your color system halfway through a product can create unnecessary work.&lt;/p&gt;

&lt;p&gt;The idea behind &lt;a href="https://chroma-panel.jscrate.dev/" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt; is that you can start small and add those capabilities only when you need them.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a good React color picker for TypeScript?
&lt;/h3&gt;

&lt;p&gt;ChromaPanel includes TypeScript types directly in the package, so you do not need a separate &lt;code&gt;@types&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;You can see the &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/typescript" rel="noopener noreferrer"&gt;TypeScript documentation&lt;/a&gt; for more details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use a React color picker with Next.js?
&lt;/h3&gt;

&lt;p&gt;Yes. ChromaPanel includes documentation for &lt;a href="https://chroma-panel.jscrate.dev/react/frameworks/next-js" rel="noopener noreferrer"&gt;Next.js and server rendering&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Interactive picker components should be used from the client side of a Next.js application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a React color picker with an eyedropper?
&lt;/h3&gt;

&lt;p&gt;Yes. ChromaPanel includes &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper" rel="noopener noreferrer"&gt;eyedropper support&lt;/a&gt; where the browser EyeDropper API is available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can React pick colors from an image?
&lt;/h3&gt;

&lt;p&gt;Yes. ChromaPanel's &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image" rel="noopener noreferrer"&gt;image color picker&lt;/a&gt; can extract dominant colors from an image and sample an exact pixel.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;extractPalette&lt;/code&gt; utility can also be used independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a React gradient color picker?
&lt;/h3&gt;

&lt;p&gt;ChromaPanel provides a separate &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor" rel="noopener noreferrer"&gt;GradientEditor&lt;/a&gt; for linear and radial gradient workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use OKLCH in a React color picker?
&lt;/h3&gt;

&lt;p&gt;ChromaPanel's &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4" rel="noopener noreferrer"&gt;CSS Color 4 tools&lt;/a&gt; support OKLCH, OKLab, Lab, LCH, sRGB, and Display P3 workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does ChromaPanel work with Tailwind CSS?
&lt;/h3&gt;

&lt;p&gt;Yes. There is a dedicated &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/tailwind" rel="noopener noreferrer"&gt;Tailwind CSS guide&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use ChromaPanel with shadcn/ui?
&lt;/h3&gt;

&lt;p&gt;Yes. The &lt;a href="https://chroma-panel.jscrate.dev/react/integrations/shadcn-ui" rel="noopener noreferrer"&gt;shadcn/ui integration guide&lt;/a&gt; shows how to use ChromaPanel with an existing shadcn interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;React color picker&lt;/strong&gt; can be a tiny control or a complete part of your product's design workflow.&lt;/p&gt;

&lt;p&gt;For a basic HEX field, keep things simple.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt; brings those pieces together behind one React API while still letting you import only the parts you need.&lt;/p&gt;

&lt;p&gt;If you want to try it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://chroma-panel.jscrate.dev/" rel="noopener noreferrer"&gt;chroma-panel.jscrate.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/chroma-panel" rel="noopener noreferrer"&gt;chroma-panel&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/re-sohail/chroma-panel" rel="noopener noreferrer"&gt;re-sohail/chroma-panel&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project is open source and released under the MIT license.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>react</category>
    </item>
    <item>
      <title>ChromaPanel vs Other React Color Pickers: Features, Bundle Size, Accessibility, and More</title>
      <dc:creator>Sohail Khan</dc:creator>
      <pubDate>Wed, 23 Sep 2026 06:47:11 +0000</pubDate>
      <link>https://dev.to/re-sohail/chromapanel-vs-other-react-color-pickers-features-bundle-size-accessibility-and-more-2ooc</link>
      <guid>https://dev.to/re-sohail/chromapanel-vs-other-react-color-pickers-features-bundle-size-accessibility-and-more-2ooc</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxbihplbtmukrtk8kje6p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxbihplbtmukrtk8kje6p.png" alt=" " width="799" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choosing a &lt;strong&gt;React color picker&lt;/strong&gt; sounds simple until you start looking at what your application actually needs.&lt;/p&gt;

&lt;p&gt;Maybe you only need a small HEX color picker.&lt;/p&gt;

&lt;p&gt;Or maybe you need &lt;a href="https://chroma-panel.jscrate.dev/react/modes/sliders" rel="noopener noreferrer"&gt;RGB and HSL controls&lt;/a&gt;, predefined &lt;a href="https://chroma-panel.jscrate.dev/react/modes/palettes" rel="noopener noreferrer"&gt;palettes&lt;/a&gt;, an &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper" rel="noopener noreferrer"&gt;eyedropper&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image" rel="noopener noreferrer"&gt;image color picking&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor" rel="noopener noreferrer"&gt;gradients&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/overview/accessibility" rel="noopener noreferrer"&gt;accessibility&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/forms" rel="noopener noreferrer"&gt;form support&lt;/a&gt;, and &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4" rel="noopener noreferrer"&gt;modern CSS colors like OKLCH&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Those are very different requirements.&lt;/p&gt;

&lt;p&gt;There are several popular React color picker libraries available today, but they are not trying to solve exactly the same problem.&lt;/p&gt;

&lt;p&gt;In this comparison, I’m looking at four options:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/chroma-panel" rel="noopener noreferrer"&gt;&lt;strong&gt;ChromaPanel&lt;/strong&gt;&lt;/a&gt;, &lt;code&gt;react-colorful&lt;/code&gt;, &lt;code&gt;react-color&lt;/code&gt;, and &lt;code&gt;@uiw/react-color&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  React color picker comparison at a glance
&lt;/h2&gt;

&lt;p&gt;The package and bundle figures below are based on the &lt;a href="https://chroma-panel.jscrate.dev/react/overview/comparison" rel="noopener noreferrer"&gt;ChromaPanel comparison audit&lt;/a&gt; and the current &lt;a href="https://chroma-panel.jscrate.dev/" rel="noopener noreferrer"&gt;package documentation&lt;/a&gt;. Bundle figures refer to the package main entry measured by &lt;a href="https://bundlephobia.com/" rel="noopener noreferrer"&gt;Bundlephobia&lt;/a&gt;, which is different from the amount that a tree-shaken application may actually ship.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/chroma-panel" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt;&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;react-colorful&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;react-color&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;@uiw/react-color&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Separate types package&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime dependencies&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;20 in main package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main-entry gzip&lt;/td&gt;
&lt;td&gt;~22 kB&lt;/td&gt;
&lt;td&gt;~4.9 kB&lt;/td&gt;
&lt;td&gt;~38.4 kB&lt;/td&gt;
&lt;td&gt;~16 kB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Color wheel&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RGB/HSL channel sliders&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alpha&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Palettes/swatches&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Recipe&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image color picker&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eyedropper&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Yes in Chrome picker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradient editor&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modern CSS colors&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form-ready input&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Build yourself&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility documented&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Limited documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contrast tools&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;td&gt;Not&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The differences become more useful when we look at what those features actually mean inside a real React application.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. ChromaPanel
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/" rel="noopener noreferrer"&gt;Chroma Panel&lt;/a&gt; is the most feature-focused option in this comparison.&lt;/p&gt;

&lt;p&gt;Instead of providing only one saturation area and a hue slider, it treats color selection as a larger UI problem.&lt;/p&gt;

&lt;p&gt;You can use it as a simple &lt;a href="https://chroma-panel.jscrate.dev/react/components/color-input" rel="noopener noreferrer"&gt;React color input&lt;/a&gt;, but the same package can grow into a much more complete color system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ColorInput&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BrandColor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt;
      &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#3366cc"&lt;/span&gt;
      &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Brand color"&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the basic version.&lt;/p&gt;

&lt;p&gt;Behind it, &lt;a href="https://github.com/re-sohail/chroma-panel" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt; provides five different color-selection modes: &lt;a href="https://chroma-panel.jscrate.dev/react/modes/wheel" rel="noopener noreferrer"&gt;wheel&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/sliders" rel="noopener noreferrer"&gt;sliders&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/palettes" rel="noopener noreferrer"&gt;palettes&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/pencils" rel="noopener noreferrer"&gt;pencils&lt;/a&gt;, and &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image" rel="noopener noreferrer"&gt;image picking&lt;/a&gt;. It also includes an &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper" rel="noopener noreferrer"&gt;eyedropper&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/forms" rel="noopener noreferrer"&gt;native form behavior&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4" rel="noopener noreferrer"&gt;CSS Color 4 utilities&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor" rel="noopener noreferrer"&gt;gradients&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/contrast" rel="noopener noreferrer"&gt;contrast tools&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/typescript" rel="noopener noreferrer"&gt;TypeScript types&lt;/a&gt;, and &lt;a href="https://www.npmjs.com/package/chroma-panel" rel="noopener noreferrer"&gt;zero runtime dependencies&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five different ways to pick a color
&lt;/h2&gt;

&lt;p&gt;A major difference is that ChromaPanel does not force every user into the same color picker interface.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://chroma-panel.jscrate.dev/react/modes/wheel" rel="noopener noreferrer"&gt;wheel&lt;/a&gt; is useful for visual color exploration.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://chroma-panel.jscrate.dev/react/modes/sliders" rel="noopener noreferrer"&gt;sliders&lt;/a&gt; provide precise RGB, HSL, and HSB controls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/modes/palettes" rel="noopener noreferrer"&gt;Palettes&lt;/a&gt; work well when your product has approved or predefined colors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/modes/pencils" rel="noopener noreferrer"&gt;Pencils&lt;/a&gt; provide a large 120-color swatch grid.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/modes/image" rel="noopener noreferrer"&gt;Image mode&lt;/a&gt; lets someone choose colors directly from a photo or image.&lt;/p&gt;

&lt;p&gt;You can show all of them or choose only the modes your UI needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChromaPanel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ThemeColorPicker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChromaPanel&lt;/span&gt;
      &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#7c3aed"&lt;/span&gt;
      &lt;span class="na"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wheel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sliders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;palettes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes ChromaPanel useful when the same application has different color workflows.&lt;/p&gt;

&lt;p&gt;For example, a settings form might use &lt;a href="https://chroma-panel.jscrate.dev/react/components/color-input" rel="noopener noreferrer"&gt;&lt;code&gt;ColorInput&lt;/code&gt;&lt;/a&gt;, while a design editor can show the &lt;a href="https://chroma-panel.jscrate.dev/react/components/chroma-panel" rel="noopener noreferrer"&gt;complete panel&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick colors from images
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/modes/image" rel="noopener noreferrer"&gt;Image color picking&lt;/a&gt; is one of the bigger differences between ChromaPanel and the other React color picker libraries in this comparison.&lt;/p&gt;

&lt;p&gt;A user can provide an image and pick an exact color from it.&lt;/p&gt;

&lt;p&gt;ChromaPanel can also extract a palette from the image.&lt;/p&gt;

&lt;p&gt;That can be useful when someone uploads a logo, product image, photograph, or brand asset and wants to create matching colors.&lt;/p&gt;

&lt;p&gt;The other three libraries in this comparison do not currently document image color sampling as a built-in feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eyedropper support
&lt;/h2&gt;

&lt;p&gt;ChromaPanel supports the browser &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper" rel="noopener noreferrer"&gt;&lt;code&gt;EyeDropper&lt;/code&gt;&lt;/a&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper" rel="noopener noreferrer"&gt; API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When the browser supports it, the user can pick a color from elsewhere on their screen.&lt;/p&gt;

&lt;p&gt;This works particularly well for design tools and theme editors.&lt;/p&gt;

&lt;p&gt;There is an important limitation: the native EyeDropper API is browser-dependent, so this functionality is mainly available in Chromium-based browsers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/uiwjs/react-color" rel="noopener noreferrer"&gt;&lt;code&gt;@uiw/react-color&lt;/code&gt;&lt;/a&gt; also exposes eyedropper functionality in its Chrome-style picker. Its own API includes a &lt;code&gt;showEyeDropper&lt;/code&gt; option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modern CSS colors
&lt;/h2&gt;

&lt;p&gt;This is an area where ChromaPanel takes a different approach from most traditional React color pickers.&lt;/p&gt;

&lt;p&gt;It includes &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4" rel="noopener noreferrer"&gt;utilities for modern color spaces&lt;/a&gt; such as:&lt;/p&gt;

&lt;p&gt;OKLCH, OKLab, Lab, LCH, Display P3, and sRGB.&lt;/p&gt;

&lt;p&gt;That matters more now that modern CSS applications are increasingly using colors beyond the traditional HEX, RGB, and HSL formats.&lt;/p&gt;

&lt;p&gt;You can parse a modern CSS color, check whether it fits inside a target gamut, map it when necessary, and serialize it again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;parseColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isInGamut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;mapToGamut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;serializeColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel/color&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oklch(72% 0.18 250)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isInGamut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;srgb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapToGamut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;srgb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;serializeColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other React color pickers compared here do not currently document equivalent built-in &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4" rel="noopener noreferrer"&gt;CSS Color 4 support&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gradient editing
&lt;/h2&gt;

&lt;p&gt;ChromaPanel also provides a separate &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor" rel="noopener noreferrer"&gt;gradient editor&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;GradientEditor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gradientToCss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel/gradient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can work with linear and radial gradients while keeping that code separate from the basic color picker.&lt;/p&gt;

&lt;p&gt;That separation is useful because projects that only need solid colors do not need to use the gradient functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Form support
&lt;/h2&gt;

&lt;p&gt;This feature is easy to overlook.&lt;/p&gt;

&lt;p&gt;A color picker often ends up inside a &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/forms" rel="noopener noreferrer"&gt;form&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;ChromaPanel's &lt;a href="https://chroma-panel.jscrate.dev/react/components/color-input" rel="noopener noreferrer"&gt;&lt;code&gt;ColorInput&lt;/code&gt;&lt;/a&gt; behaves more like a real form field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"brandColor"&lt;/span&gt;
    &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#2563eb"&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;
  &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Save&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The selected value can participate in form submission, native validation, and form reset behavior.&lt;/p&gt;

&lt;p&gt;With smaller color picker libraries, you will often connect the picker to your form state yourself.&lt;/p&gt;

&lt;p&gt;Neither approach is wrong, but having the behavior built in removes some repeated application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility
&lt;/h2&gt;

&lt;p&gt;Color pickers are visual components, but they should not require a mouse.&lt;/p&gt;

&lt;p&gt;ChromaPanel documents &lt;a href="https://chroma-panel.jscrate.dev/react/overview/accessibility" rel="noopener noreferrer"&gt;keyboard and screen-reader behavior&lt;/a&gt; and uses native range inputs for color axes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/omgovich/react-colorful" rel="noopener noreferrer"&gt;&lt;code&gt;react-colorful&lt;/code&gt;&lt;/a&gt; also specifically documents accessibility and says it follows WAI-ARIA guidelines.&lt;/p&gt;

&lt;p&gt;That makes these two worth looking at closely when accessibility is an important requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contrast checking
&lt;/h2&gt;

&lt;p&gt;ChromaPanel also includes &lt;a href="https://chroma-panel.jscrate.dev/react/utils/contrast" rel="noopener noreferrer"&gt;contrast-related utilities&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The documentation covers WCAG contrast, alpha-aware checks, APCA, and color suggestions.&lt;/p&gt;

&lt;p&gt;This means the color picker can be connected to workflows such as choosing a background color and immediately checking whether text will remain readable.&lt;/p&gt;

&lt;p&gt;That is particularly useful for theme builders and design systems.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. react-colorful
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/react-colorful" rel="noopener noreferrer"&gt;&lt;code&gt;react-colorful&lt;/code&gt;&lt;/a&gt; takes almost the opposite approach.&lt;/p&gt;

&lt;p&gt;Its biggest strength is simplicity and size.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/omgovich/react-colorful" rel="noopener noreferrer"&gt;package&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;A basic example is very small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HexColorPicker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-colorful&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Picker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setColor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#aabbcc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HexColorPicker&lt;/span&gt;
      &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setColor&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its &lt;a href="https://github.com/omgovich/react-colorful" rel="noopener noreferrer"&gt;README&lt;/a&gt; quotes roughly &lt;strong&gt;3.1 kB gzipped for a picker&lt;/strong&gt;, while the &lt;a href="https://chroma-panel.jscrate.dev/react/overview/comparison" rel="noopener noreferrer"&gt;comparison audit&lt;/a&gt; measured about &lt;strong&gt;4.9 kB for the package main entry&lt;/strong&gt;. Those numbers measure different things, so they should not be treated as a contradiction.&lt;/p&gt;

&lt;p&gt;This is one of the main reasons &lt;code&gt;react-colorful&lt;/code&gt; is so attractive.&lt;/p&gt;

&lt;p&gt;You get a focused React color picker without bringing in a large feature set.&lt;/p&gt;

&lt;p&gt;It supports saturation, hue, alpha, several color formats, touch devices, accessibility, and TypeScript.&lt;/p&gt;

&lt;p&gt;What it does not try to provide is equally important.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So if you need a compact color control and you are happy building the surrounding UI yourself, &lt;a href="https://www.npmjs.com/package/react-colorful" rel="noopener noreferrer"&gt;&lt;code&gt;react-colorful&lt;/code&gt;&lt;/a&gt; remains a very strong option.&lt;/p&gt;

&lt;h1&gt;
  
  
  3. react-color
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/react-color" rel="noopener noreferrer"&gt;&lt;code&gt;react-color&lt;/code&gt;&lt;/a&gt; follows a different idea again.&lt;/p&gt;

&lt;p&gt;Instead of concentrating on one small picker, it provides &lt;strong&gt;13 different picker designs&lt;/strong&gt;, including Sketch, Photoshop, Chrome, Material, Circle, Block, Twitter, GitHub, and others.&lt;/p&gt;

&lt;p&gt;A typical example looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SketchPicker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-color&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Picker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SketchPicker&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That can be useful when you want an interface that already looks familiar.&lt;/p&gt;

&lt;p&gt;Instead of designing a complete color-picker layout yourself, you can choose one of the existing styles.&lt;/p&gt;

&lt;p&gt;The trade-off is that &lt;a href="https://github.com/casesandberg/react-color" rel="noopener noreferrer"&gt;&lt;code&gt;react-color&lt;/code&gt;&lt;/a&gt; is an older package.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/react-color" rel="noopener noreferrer"&gt;npm&lt;/a&gt; currently lists version 2.19.3, and that release was published years ago. Its TypeScript definitions are provided separately, and the &lt;a href="https://chroma-panel.jscrate.dev/react/overview/comparison" rel="noopener noreferrer"&gt;comparison audit&lt;/a&gt; records seven runtime dependencies and a larger main-entry bundle than the other packages shown here.&lt;/p&gt;

&lt;p&gt;That does not make the package unusable.&lt;/p&gt;

&lt;p&gt;It simply means maintenance history and architecture are worth considering before introducing it into a new React project.&lt;/p&gt;

&lt;p&gt;For an existing application that already uses &lt;code&gt;react-color&lt;/code&gt;, replacing it only because another library is newer may also create unnecessary work.&lt;/p&gt;

&lt;h1&gt;
  
  
  4. @uiw/react-color
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@uiw/react-color" rel="noopener noreferrer"&gt;&lt;code&gt;@uiw/react-color&lt;/code&gt;&lt;/a&gt; sits somewhere between the previous approaches.&lt;/p&gt;

&lt;p&gt;It offers many different React color picker components, but those components can also be installed separately.&lt;/p&gt;

&lt;p&gt;Its &lt;a href="https://github.com/uiwjs/react-color" rel="noopener noreferrer"&gt;collection&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;That modular structure is useful.&lt;/p&gt;

&lt;p&gt;If your project only needs the wheel, for example, you do not necessarily have to build everything around the main package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @uiw/react-color-wheel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main &lt;a href="https://www.npmjs.com/package/@uiw/react-color" rel="noopener noreferrer"&gt;&lt;code&gt;@uiw/react-color&lt;/code&gt;&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;It also includes TypeScript declarations.&lt;/p&gt;

&lt;p&gt;One interesting feature is the Chrome picker's eyedropper option.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;
  &lt;span class="na"&gt;showEyeDropper&lt;/span&gt;
  &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package also provides editable HEXA, RGBA, and HSLA input modes.&lt;/p&gt;

&lt;p&gt;If you prefer having many independent picker styles available as separate packages, &lt;a href="https://github.com/uiwjs/react-color" rel="noopener noreferrer"&gt;&lt;code&gt;@uiw/react-color&lt;/code&gt;&lt;/a&gt; provides a lot of flexibility.&lt;/p&gt;

&lt;h1&gt;
  
  
  ChromaPanel vs react-colorful
&lt;/h1&gt;

&lt;p&gt;This is probably the most useful direct comparison.&lt;/p&gt;

&lt;p&gt;Both libraries have zero runtime dependencies.&lt;/p&gt;

&lt;p&gt;Both include TypeScript types.&lt;/p&gt;

&lt;p&gt;Both document accessibility.&lt;/p&gt;

&lt;p&gt;But their goals are quite different.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/react-colorful" rel="noopener noreferrer"&gt;&lt;code&gt;react-colorful&lt;/code&gt;&lt;/a&gt; focuses on doing the core color-picker job with very little code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/chroma-panel" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt; includes a much larger set of functionality around that picker.&lt;/p&gt;

&lt;p&gt;With ChromaPanel, you can get the &lt;a href="https://chroma-panel.jscrate.dev/react/modes/wheel" rel="noopener noreferrer"&gt;wheel&lt;/a&gt;, per-channel &lt;a href="https://chroma-panel.jscrate.dev/react/modes/sliders" rel="noopener noreferrer"&gt;RGB/HSL/HSB sliders&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/palettes" rel="noopener noreferrer"&gt;palettes&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image" rel="noopener noreferrer"&gt;image sampling&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper" rel="noopener noreferrer"&gt;eyedropper&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor" rel="noopener noreferrer"&gt;gradients&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/forms" rel="noopener noreferrer"&gt;form integration&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/contrast" rel="noopener noreferrer"&gt;contrast utilities&lt;/a&gt;, and &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4" rel="noopener noreferrer"&gt;modern CSS colors&lt;/a&gt; from the same project.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;react-colorful&lt;/code&gt;, you get a significantly smaller foundation and can build the extra pieces your application needs.&lt;/p&gt;

&lt;p&gt;So the real question is not simply:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which React color picker is smaller?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That answer is clearly &lt;code&gt;react-colorful&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The more useful question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much color functionality does my application actually need?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is only a simple HEX, RGB, HSL, or HSV picker, the smaller package can make a lot of sense.&lt;/p&gt;

&lt;p&gt;If the answer includes several additional color workflows, comparing only the size of the base picker becomes less useful.&lt;/p&gt;

&lt;h1&gt;
  
  
  ChromaPanel vs react-color
&lt;/h1&gt;

&lt;p&gt;These two packages are also quite different.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/react-color" rel="noopener noreferrer"&gt;&lt;code&gt;react-color&lt;/code&gt;&lt;/a&gt; gives you many recognizable pre-designed picker interfaces.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt; instead provides different ways of working with color inside one system.&lt;/p&gt;

&lt;p&gt;If you specifically want something that looks like the classic Sketch or Photoshop picker, &lt;a href="https://github.com/casesandberg/react-color" rel="noopener noreferrer"&gt;&lt;code&gt;react-color&lt;/code&gt;&lt;/a&gt; already gives you that type of interface.&lt;/p&gt;

&lt;p&gt;ChromaPanel focuses more on modern application features: &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image" rel="noopener noreferrer"&gt;image sampling&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4" rel="noopener noreferrer"&gt;CSS Color 4&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/forms" rel="noopener noreferrer"&gt;form behavior&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/overview/accessibility" rel="noopener noreferrer"&gt;accessibility&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor" rel="noopener noreferrer"&gt;gradients&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/contrast" rel="noopener noreferrer"&gt;contrast calculations&lt;/a&gt;, an &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper" rel="noopener noreferrer"&gt;eyedropper&lt;/a&gt;, and different selection modes.&lt;/p&gt;

&lt;p&gt;Another difference is package age and maintenance.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;react-color&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;That history can matter when evaluating a dependency, and it is worth considering alongside features.&lt;/p&gt;

&lt;h1&gt;
  
  
  ChromaPanel vs @uiw/react-color
&lt;/h1&gt;

&lt;p&gt;This comparison is closer because both provide several ways to choose colors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@uiw/react-color" rel="noopener noreferrer"&gt;&lt;code&gt;@uiw/react-color&lt;/code&gt;&lt;/a&gt; gives developers a large collection of separate picker components.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/components/chroma-panel" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt; gives developers multiple modes inside one color panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/uiwjs/react-color" rel="noopener noreferrer"&gt;&lt;code&gt;@uiw/react-color&lt;/code&gt;&lt;/a&gt; has a dedicated wheel, swatches, Chrome-style picker, Sketch-style picker, sliders, and lower-level primitives.&lt;/p&gt;

&lt;p&gt;ChromaPanel adds areas such as &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image" rel="noopener noreferrer"&gt;image color extraction&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4" rel="noopener noreferrer"&gt;modern CSS color spaces&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/forms" rel="noopener noreferrer"&gt;native form integration&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/contrast" rel="noopener noreferrer"&gt;contrast utilities&lt;/a&gt;, and a dedicated &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor" rel="noopener noreferrer"&gt;gradient editor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Both approaches can work well.&lt;/p&gt;

&lt;p&gt;The main difference is whether you want a collection of separate picker packages or one color system that can switch between different modes.&lt;/p&gt;

&lt;h1&gt;
  
  
  What about bundle size?
&lt;/h1&gt;

&lt;p&gt;Bundle size deserves some context.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://chroma-panel.jscrate.dev/react/overview/comparison" rel="noopener noreferrer"&gt;comparison audit&lt;/a&gt; measured the package main entries at roughly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/chroma-panel" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt;: 22.0 kB gzip → &lt;a href="https://www.npmjs.com/package/@uiw/react-color" rel="noopener noreferrer"&gt;@uiw/react-color&lt;/a&gt;: 16.0 kB → &lt;a href="https://www.npmjs.com/package/react-colorful" rel="noopener noreferrer"&gt;react-colorful&lt;/a&gt;: 4.9 kB → &lt;a href="https://www.npmjs.com/package/react-color" rel="noopener noreferrer"&gt;react-color&lt;/a&gt;: 38.4 kB.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But those numbers are not the complete story.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;react-colorful&lt;/code&gt; documents a smaller figure for an individual picker.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@uiw/react-color&lt;/code&gt; allows individual picker packages to be installed separately.&lt;/p&gt;

&lt;p&gt;ChromaPanel also provides &lt;a href="https://chroma-panel.jscrate.dev/react/utils/entry-points" rel="noopener noreferrer"&gt;smaller entry points&lt;/a&gt; so applications do not have to use all five modes.&lt;/p&gt;

&lt;p&gt;Its own Vite measurement reports around &lt;strong&gt;23.1 kB gzipped for all five modes&lt;/strong&gt; and roughly &lt;strong&gt;15 kB for the panel shell plus one mode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is why bundle comparisons should always include the functionality being shipped.&lt;/p&gt;

&lt;p&gt;A 5 kB package plus several custom components may not remain 5 kB once the full feature is finished.&lt;/p&gt;

&lt;p&gt;At the same time, an application that genuinely needs only a simple color picker should not ship advanced functionality it will never use.&lt;/p&gt;

&lt;h1&gt;
  
  
  Which React color picker should you use?
&lt;/h1&gt;

&lt;p&gt;There is no single answer for every project.&lt;/p&gt;

&lt;p&gt;Use &lt;strong&gt;&lt;a href="https://chroma-panel.jscrate.dev/" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt;&lt;/strong&gt; when your application needs a broader color system: &lt;a href="https://chroma-panel.jscrate.dev/react/components/chroma-panel" rel="noopener noreferrer"&gt;multiple selection modes&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image" rel="noopener noreferrer"&gt;image color picking&lt;/a&gt;, an &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper" rel="noopener noreferrer"&gt;eyedropper&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/palettes" rel="noopener noreferrer"&gt;palettes&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor" rel="noopener noreferrer"&gt;gradients&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4" rel="noopener noreferrer"&gt;modern CSS colors&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/forms" rel="noopener noreferrer"&gt;form behavior&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/overview/accessibility" rel="noopener noreferrer"&gt;accessibility&lt;/a&gt;, and &lt;a href="https://chroma-panel.jscrate.dev/react/utils/contrast" rel="noopener noreferrer"&gt;contrast utilities&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Use &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/react-colorful" rel="noopener noreferrer"&gt;react-colorful&lt;/a&gt;&lt;/strong&gt; when you mainly want a small, accessible React color picker and prefer to build additional UI such as popovers and palettes yourself.&lt;/p&gt;

&lt;p&gt;Use &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/react-color" rel="noopener noreferrer"&gt;react-color&lt;/a&gt;&lt;/strong&gt; when its collection of familiar ready-made picker designs matches an existing application or a specific UI requirement.&lt;/p&gt;

&lt;p&gt;Use &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/@uiw/react-color" rel="noopener noreferrer"&gt;@uiw/react-color&lt;/a&gt;&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h1&gt;
  
  
  Final thoughts
&lt;/h1&gt;

&lt;p&gt;A React color picker can be a tiny input or a surprisingly large part of an application's design system.&lt;/p&gt;

&lt;p&gt;That is why these four libraries look so different.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/omgovich/react-colorful" rel="noopener noreferrer"&gt;&lt;code&gt;react-colorful&lt;/code&gt;&lt;/a&gt; keeps the core small.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/casesandberg/react-color" rel="noopener noreferrer"&gt;&lt;code&gt;react-color&lt;/code&gt;&lt;/a&gt; gives you many familiar picker designs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/uiwjs/react-color" rel="noopener noreferrer"&gt;&lt;code&gt;@uiw/react-color&lt;/code&gt;&lt;/a&gt; provides a modular collection of individual picker components.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;&lt;a href="https://github.com/re-sohail/chroma-panel" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt;&lt;/strong&gt; tries to cover the wider color workflow in one package: &lt;a href="https://chroma-panel.jscrate.dev/react/modes/wheel" rel="noopener noreferrer"&gt;wheel&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/sliders" rel="noopener noreferrer"&gt;sliders&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/palettes" rel="noopener noreferrer"&gt;palettes&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/pencils" rel="noopener noreferrer"&gt;pencils&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image" rel="noopener noreferrer"&gt;image sampling&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper" rel="noopener noreferrer"&gt;eyedropper&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor" rel="noopener noreferrer"&gt;gradients&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/overview/accessibility" rel="noopener noreferrer"&gt;accessibility&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/forms" rel="noopener noreferrer"&gt;form integration&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/contrast" rel="noopener noreferrer"&gt;contrast tools&lt;/a&gt;, and &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4" rel="noopener noreferrer"&gt;modern CSS color spaces&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For a simple HEX field, you may not need all of that.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The important thing is to choose the React color picker based on the experience you need to build.&lt;/p&gt;

&lt;p&gt;Not just the name of the package.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>ChromaPanel: A Modern React Color Picker with Eyedropper, Image Sampling, Gradients, and More</title>
      <dc:creator>Sohail Khan</dc:creator>
      <pubDate>Wed, 23 Sep 2026 06:03:21 +0000</pubDate>
      <link>https://dev.to/re-sohail/chromapanel-a-modern-react-color-picker-with-eyedropper-image-sampling-gradients-and-more-501a</link>
      <guid>https://dev.to/re-sohail/chromapanel-a-modern-react-color-picker-with-eyedropper-image-sampling-gradients-and-more-501a</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1b9552yez0vg3uo4ydof.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1b9552yez0vg3uo4ydof.png" alt=" " width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A color picker looks like a simple UI component until you need to use one in a real product.&lt;/p&gt;

&lt;p&gt;At first, you may only need a HEX color field.&lt;/p&gt;

&lt;p&gt;Then the requirements start growing.&lt;/p&gt;

&lt;p&gt;Users want a &lt;a href="https://chroma-panel.jscrate.dev/react/modes/wheel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;color wheel&lt;/a&gt;. Designers want exact &lt;a href="https://chroma-panel.jscrate.dev/react/modes/sliders?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;RGB and HSL values&lt;/a&gt;. Your settings page needs predefined brand colors. Someone asks to &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;pick a color from an uploaded image&lt;/a&gt;. Another user wants an &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;eyedropper&lt;/a&gt;. Your form needs validation. Keyboard navigation has to work. And eventually you may need &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;gradients&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;OKLCH, Display P3&lt;/a&gt;, or &lt;a href="https://chroma-panel.jscrate.dev/react/utils/contrast?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;accessible contrast checks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At that point, a basic React color picker is no longer enough.&lt;/p&gt;

&lt;p&gt;That is the problem I wanted to solve with &lt;strong&gt;&lt;a href="https://chroma-panel.jscrate.dev/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;ChromaPanel is an &lt;a href="https://github.com/re-sohail/chroma-panel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;open-source&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;It gives you multiple ways to choose a color while keeping them behind one React API.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is ChromaPanel?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/components/chroma-panel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;ChromaPanel&lt;/a&gt; is a React color picker component that can be used as a small color input or as a complete inline color panel.&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;a href="https://chroma-panel.jscrate.dev/react/modes/wheel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;color wheel&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/modes/sliders?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;RGB, HSL, HSB/HSV, and alpha sliders&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;searchable &lt;a href="https://chroma-panel.jscrate.dev/react/modes/palettes?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;color palettes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;a &lt;a href="https://chroma-panel.jscrate.dev/react/modes/pencils?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;120-color pencil-style swatch grid&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/modes/image?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;image color extraction and pixel sampling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;an &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;eyedropper in supported browsers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HEX, RGB, HSL, HSV, and alpha values&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/handbook/controlled?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;controlled and uncontrolled React state&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/handbook/forms?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;native form support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/overview/accessibility?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;keyboard and screen-reader support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/handbook/typescript?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;TypeScript types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;CSS Color 4 utilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;OKLCH, OKLab, Lab, LCH, sRGB, and Display P3 support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;linear and radial gradient editing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/utils/contrast?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;contrast helpers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/utils/token-exports?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;CSS, SCSS, Tailwind, and design-token exports&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/chroma-panel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;zero runtime dependencies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can use everything together or only import the parts your application needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the React color picker
&lt;/h2&gt;

&lt;p&gt;Getting started is intentionally simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;chroma-panel

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no provider that you need to add around your application.&lt;/p&gt;

&lt;p&gt;There is also no separate CSS file you need to import for the standard setup.&lt;/p&gt;

&lt;p&gt;Once the &lt;a href="https://www.npmjs.com/package/chroma-panel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;package is installed&lt;/a&gt;, you can render a working React color picker with &lt;a href="https://chroma-panel.jscrate.dev/react/components/color-input?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;&lt;code&gt;ColorInput&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ColorInput&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BrandColor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#3366cc"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/components/color-input?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;&lt;code&gt;ColorInput&lt;/code&gt;&lt;/a&gt; displays a color swatch and opens ChromaPanel in a popover when the user clicks it.&lt;/p&gt;

&lt;p&gt;For many forms and settings pages, that may be all you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using ChromaPanel as a controlled React color picker
&lt;/h2&gt;

&lt;p&gt;Most real applications need to keep the selected color in React state.&lt;/p&gt;

&lt;p&gt;ChromaPanel supports the normal &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/controlled?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;controlled component pattern&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ColorInput&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AccentColorPicker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setColor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#3366cc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Accent color"&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful part here is that &lt;code&gt;onChange&lt;/code&gt; does not only give you a HEX string.&lt;/p&gt;

&lt;p&gt;The result contains different representations of the selected color, so your application can use the format it actually needs.&lt;/p&gt;

&lt;p&gt;You can update a live preview with &lt;code&gt;onChange&lt;/code&gt; while the user is dragging and use &lt;code&gt;onChangeComplete&lt;/code&gt; when the interaction finishes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onChangeComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;saveColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when saving the value makes a network request, creates an undo entry, updates a database, or runs another expensive operation.&lt;/p&gt;

&lt;p&gt;The UI can remain responsive without sending a request for every tiny movement of the color picker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Need an inline color picker instead?
&lt;/h2&gt;

&lt;p&gt;Sometimes a popover is not the right UI.&lt;/p&gt;

&lt;p&gt;A design editor, theme builder, drawing tool, or customization page may need the entire React color picker to stay visible.&lt;/p&gt;

&lt;p&gt;That is what &lt;a href="https://chroma-panel.jscrate.dev/react/components/chroma-panel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;&lt;code&gt;ChromaPanel&lt;/code&gt;&lt;/a&gt; is for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChromaPanel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ThemeEditor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChromaPanel&lt;/span&gt;
      &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#3366cc"&lt;/span&gt;
      &lt;span class="na"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wheel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sliders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;palettes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;modes&lt;/code&gt; property is important.&lt;/p&gt;

&lt;p&gt;You do not have to show every feature just because the package supports it.&lt;/p&gt;

&lt;p&gt;A branding form may only need &lt;a href="https://chroma-panel.jscrate.dev/react/modes/palettes?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;palettes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A design tool may need &lt;a href="https://chroma-panel.jscrate.dev/react/modes/sliders?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;sliders&lt;/a&gt;, a &lt;a href="https://chroma-panel.jscrate.dev/react/modes/wheel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;wheel&lt;/a&gt;, and &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;image sampling&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A simple theme editor may only need the &lt;a href="https://chroma-panel.jscrate.dev/react/modes/wheel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;wheel&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Color wheel
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://chroma-panel.jscrate.dev/react/modes/wheel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;wheel&lt;/a&gt; gives users a visual way to explore colors.&lt;/p&gt;

&lt;p&gt;Instead of typing values, they can move around the hue wheel and adjust the color visually.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It is also useful when users know roughly what color they want but do not know its HEX or RGB value.&lt;/p&gt;

&lt;h2&gt;
  
  
  RGB, HSL and HSB sliders
&lt;/h2&gt;

&lt;p&gt;Sometimes visual selection is not precise enough.&lt;/p&gt;

&lt;p&gt;Developers and designers often need exact channel values.&lt;/p&gt;

&lt;p&gt;ChromaPanel includes &lt;a href="https://chroma-panel.jscrate.dev/react/modes/sliders?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;per-channel controls for RGB, HSL, HSB/HSV, and alpha&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That means someone can visually find a color first and then fine-tune the exact numbers without switching to another tool.&lt;/p&gt;

&lt;p&gt;The same React color picker can therefore work for casual users and people who need precise color control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Searchable color palettes
&lt;/h2&gt;

&lt;p&gt;Many applications should not allow completely arbitrary colors.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;ChromaPanel supports &lt;a href="https://chroma-panel.jscrate.dev/react/modes/palettes?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;named palettes and searchable swatches&lt;/a&gt;, so you can provide those approved colors directly inside the picker.&lt;/p&gt;

&lt;p&gt;Instead of building a separate palette component beside your color picker, both experiences can live inside the same UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 120-color pencils mode
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/modes/pencils?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Pencils mode&lt;/a&gt; provides a large grid of ready-made colors.&lt;/p&gt;

&lt;p&gt;Despite the name, it is not a drawing tool. It is a quick color-selection interface inspired by physical sets of colored pencils.&lt;/p&gt;

&lt;p&gt;There are 120 colors available by default, and the set can be replaced when your product needs something different.&lt;/p&gt;

&lt;p&gt;This mode is useful when users want to explore many predefined colors quickly without working with sliders or numerical values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick colors from an image
&lt;/h2&gt;

&lt;p&gt;This is one of the areas where a simple React color picker can quickly become a much bigger feature.&lt;/p&gt;

&lt;p&gt;Imagine a user uploads a company logo and wants to create a matching theme.&lt;/p&gt;

&lt;p&gt;Or they upload a product photo and want to use one of its main colors.&lt;/p&gt;

&lt;p&gt;With ChromaPanel's &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;image mode&lt;/a&gt;, a user can drop, paste, or select an image.&lt;/p&gt;

&lt;p&gt;The React color picker can extract dominant colors from that image and display them as swatches.&lt;/p&gt;

&lt;p&gt;The user can also select an exact pixel from the image.&lt;/p&gt;

&lt;p&gt;You can use the image extraction API without showing the full color picker as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;extractPalette&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;swatches&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;extractPalette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;maxColors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the same functionality useful for automatic theme generation, logo analysis, image editors, design applications, avatar customization, and other tools.&lt;/p&gt;

&lt;p&gt;Image processing is bounded and downscaled rather than processing an unlimited full-size image directly.&lt;/p&gt;

&lt;p&gt;There is also a separate worker entry point for applications that want to move heavier image processing away from the main thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  A React color picker with an eyedropper
&lt;/h2&gt;

&lt;p&gt;ChromaPanel also includes support for the browser &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;EyeDropper API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Where that browser API is available, users can pick a color from somewhere else on their screen.&lt;/p&gt;

&lt;p&gt;This can be especially useful inside design tools, theme builders, website editors, and other visual applications.&lt;/p&gt;

&lt;p&gt;The important part is that unsupported browsers do not need special handling from your UI just to hide the normal picker control.&lt;/p&gt;

&lt;p&gt;There is also a &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;&lt;code&gt;useEyedropper&lt;/code&gt; hook&lt;/a&gt; if you want to create your own eyedropper button somewhere else in your interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  It behaves like a real form control
&lt;/h2&gt;

&lt;p&gt;A surprisingly important part of a React color picker is how it works inside &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/forms?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;forms&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/components/color-input?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;&lt;code&gt;ColorInput&lt;/code&gt;&lt;/a&gt; can take a &lt;code&gt;name&lt;/code&gt; and submit its value with &lt;code&gt;FormData&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;saveProfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"profile-color"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Profile color&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"profile-color"&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"profileColor"&lt;/span&gt;
    &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#3366cc"&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hex"&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;
  &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Save&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also supports normal behaviors such as &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;disabled&lt;/code&gt;, and &lt;code&gt;form.reset()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That makes ChromaPanel particularly useful for settings screens, account customization, admin panels, SaaS dashboards, onboarding forms, and theme settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility is part of the component
&lt;/h2&gt;

&lt;p&gt;Color pickers can be difficult to make &lt;a href="https://chroma-panel.jscrate.dev/react/overview/accessibility?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;accessible&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;ChromaPanel uses real range inputs for its color axes.&lt;/p&gt;

&lt;p&gt;That allows standard browser keyboard behavior to do much of the work.&lt;/p&gt;

&lt;p&gt;Arrow keys can change values. Page Up and Page Down make larger movements. Home and End move to the limits.&lt;/p&gt;

&lt;p&gt;Values are announced with meaningful text rather than exposing only an unexplained number.&lt;/p&gt;

&lt;p&gt;The popover manages focus, closes with Escape, and returns focus to its trigger.&lt;/p&gt;

&lt;p&gt;Swatches use buttons with accessible names, and the component also respects reduced-motion and forced-color preferences.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/overview/accessibility?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Accessibility&lt;/a&gt; should not be something that gets added after a React color picker is already built. It affects the way the component itself should work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modern CSS colors: OKLCH, OKLab and Display P3
&lt;/h2&gt;

&lt;p&gt;HEX and RGB are still everywhere, but modern CSS supports much more.&lt;/p&gt;

&lt;p&gt;ChromaPanel includes &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;color utilities&lt;/a&gt; for working with:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;&lt;code&gt;OKLCH&lt;/code&gt;, &lt;code&gt;OKLab&lt;/code&gt;, &lt;code&gt;Lab&lt;/code&gt;, &lt;code&gt;LCH&lt;/code&gt;, &lt;code&gt;sRGB&lt;/code&gt;, and &lt;code&gt;Display P3&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;parseColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isInGamut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;mapToGamut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;serializeColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel/color&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oklch(72% 0.18 250)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isInGamut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;srgb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapToGamut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;srgb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;serializeColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These utilities do not require React or the DOM.&lt;/p&gt;

&lt;p&gt;They can therefore also be used as general TypeScript color utilities inside your application.&lt;/p&gt;

&lt;p&gt;This becomes useful when building design systems that are moving toward OKLCH, wide-gamut colors, and modern CSS color workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gradient color picker for React
&lt;/h2&gt;

&lt;p&gt;Solid colors are not always enough either.&lt;/p&gt;

&lt;p&gt;ChromaPanel provides a separate &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;&lt;code&gt;GradientEditor&lt;/code&gt;&lt;/a&gt; entry point for building linear and radial gradients.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;GradientEditor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gradientToCss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel/gradient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gradient stops can be changed with the keyboard, and the utilities can turn the gradient model back into CSS.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;gradient tools&lt;/a&gt; are kept in a separate entry point.&lt;/p&gt;

&lt;p&gt;That means an application that only needs a solid React color picker does not need to include the gradient editor code.&lt;/p&gt;

&lt;p&gt;This separation matters because feature-rich UI libraries should not force every feature into every bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero runtime dependencies
&lt;/h2&gt;

&lt;p&gt;ChromaPanel has &lt;a href="https://www.npmjs.com/package/chroma-panel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;no runtime dependencies&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;React and React DOM are peer dependencies, meaning the package uses the React installation that already exists in your application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chroma-panel.jscrate.dev/react/handbook/typescript?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;TypeScript declarations&lt;/a&gt; are included as well, so there is no separate &lt;code&gt;@types&lt;/code&gt; package to install.&lt;/p&gt;

&lt;p&gt;It supports React 16.14 and newer, including React 19.&lt;/p&gt;

&lt;p&gt;It also provides ESM and CommonJS builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about bundle size?
&lt;/h2&gt;

&lt;p&gt;This is an important trade-off.&lt;/p&gt;

&lt;p&gt;ChromaPanel is not trying to be the smallest React color picker on &lt;a href="https://www.npmjs.com/package/chroma-panel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The complete package gives you much more functionality than a basic saturation box and hue slider.&lt;/p&gt;

&lt;p&gt;With all five modes, the measured addition to a Vite production build is about &lt;strong&gt;23.1 kB gzipped&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Using the panel shell with one mode is around &lt;strong&gt;15 kB gzipped&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your application only needs a tiny HEX picker, a smaller library such as &lt;code&gt;react-colorful&lt;/code&gt; can make more sense.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Import only the modes you need
&lt;/h2&gt;

&lt;p&gt;If you do not need all five modes, you can use the smaller &lt;a href="https://chroma-panel.jscrate.dev/react/utils/entry-points?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;entry points&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChromaPanel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel/panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;wheelMode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel/modes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SimplePicker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChromaPanel&lt;/span&gt; &lt;span class="na"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;wheelMode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when you want ChromaPanel's API and behavior without including every available color-selection mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Works with modern React stacks
&lt;/h2&gt;

&lt;p&gt;ChromaPanel is not tied to one UI framework.&lt;/p&gt;

&lt;p&gt;The documentation includes guidance for &lt;a href="https://chroma-panel.jscrate.dev/react/frameworks/next-js?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/frameworks/vite?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Vite&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/frameworks/remix?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Remix&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/integrations/shadcn-ui?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;shadcn/ui&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/integrations/material-ui?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Material UI&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/integrations/react-aria?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;React Aria&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/frameworks/react-hook-form?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;React Hook Form&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/tailwind?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Tailwind CSS&lt;/a&gt;, and &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/server-rendering?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;server-rendered applications&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For example, if your project already uses &lt;a href="https://chroma-panel.jscrate.dev/react/integrations/shadcn-ui?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;shadcn/ui&lt;/a&gt;, you can let shadcn's Popover, Dialog, or Sheet own the surrounding interface and render &lt;code&gt;ChromaPanel&lt;/code&gt; inside it.&lt;/p&gt;

&lt;p&gt;ChromaPanel does not require shadcn/ui, Radix UI, or Base UI as dependencies.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  When ChromaPanel makes sense
&lt;/h2&gt;

&lt;p&gt;I built ChromaPanel for applications where color selection is part of the product rather than just a small decorative field.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;But there is an equally important point.&lt;/p&gt;

&lt;p&gt;If your entire requirement is simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Let the user choose a HEX color.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;then you probably do not need every feature ChromaPanel provides.&lt;/p&gt;

&lt;p&gt;A smaller React color picker can be the right choice.&lt;/p&gt;

&lt;p&gt;Choosing a library should be based on what your interface actually needs, not the number of features printed on its README.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;The main idea behind ChromaPanel was not to build another variation of the same basic React color picker.&lt;/p&gt;

&lt;p&gt;It was to create a color-selection component that could grow with a real application.&lt;/p&gt;

&lt;p&gt;You can start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ColorInput&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#3366cc"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and stop there.&lt;/p&gt;

&lt;p&gt;Or the same project can later add &lt;a href="https://chroma-panel.jscrate.dev/react/modes/palettes?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;palettes&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;image sampling&lt;/a&gt;, precise &lt;a href="https://chroma-panel.jscrate.dev/react/modes/sliders?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;sliders&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;gradients&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;modern CSS color spaces&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/overview/accessibility?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;accessibility helpers&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/contrast?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;contrast tools&lt;/a&gt;, and custom modes without replacing the original color picker.&lt;/p&gt;

&lt;p&gt;That flexibility is what I wanted from the package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;There are already good React color picker libraries available, and ChromaPanel is not meant to replace every one of them.&lt;/p&gt;

&lt;p&gt;Its focus is different.&lt;/p&gt;

&lt;p&gt;It is for applications that need several ways to work with color but still want one consistent React API.&lt;/p&gt;

&lt;p&gt;If you need a &lt;strong&gt;React color picker with a &lt;a href="https://chroma-panel.jscrate.dev/react/modes/wheel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;color wheel&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/sliders?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;RGB and HSL sliders&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/palettes?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;palettes&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/modes/image?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;image color extraction&lt;/a&gt;, an &lt;a href="https://chroma-panel.jscrate.dev/react/utils/use-eyedropper?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;eyedropper&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/components/gradient-editor?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;gradients&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/utils/css-color-4?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;modern CSS colors&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/handbook/typescript?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;TypeScript support&lt;/a&gt;, &lt;a href="https://chroma-panel.jscrate.dev/react/overview/accessibility?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;accessibility&lt;/a&gt;, and &lt;a href="https://www.npmjs.com/package/chroma-panel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;zero runtime dependencies&lt;/a&gt;&lt;/strong&gt;, ChromaPanel may be worth trying.&lt;/p&gt;

&lt;p&gt;The project is &lt;a href="https://github.com/re-sohail/chroma-panel?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;open source&lt;/a&gt; and released under the MIT license.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A Better React Color Picker: Why I Switched to Chroma Panel</title>
      <dc:creator>Sohail Khan</dc:creator>
      <pubDate>Mon, 21 Sep 2026 09:00:34 +0000</pubDate>
      <link>https://dev.to/re-sohail/a-better-react-color-picker-why-i-switched-to-chroma-panel-e6f</link>
      <guid>https://dev.to/re-sohail/a-better-react-color-picker-why-i-switched-to-chroma-panel-e6f</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxn2c6vidj5kedgljjkh2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxn2c6vidj5kedgljjkh2.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finding a good React color picker is harder than it should be. &lt;/p&gt;

&lt;p&gt;If you search for one right now, you mostly find the same old packages. They work, but they are very basic. Most of them just give you a simple box to pick a color. &lt;/p&gt;

&lt;p&gt;But what if you need more? Sometimes your users need to pick colors from an image, use a color wheel, or choose from a custom brand palette. Building these extra features yourself takes a lot of time.&lt;/p&gt;

&lt;p&gt;Recently, I found a new npm package called &lt;strong&gt;Chroma Panel&lt;/strong&gt;. It solves all these problems. Here is why I started using it in my React projects instead of the older libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  More Than Just a Basic Picker
&lt;/h2&gt;

&lt;p&gt;Chroma Panel gives you a lot of advanced tools right out of the box. You do not have to write custom code to get them working.&lt;/p&gt;

&lt;p&gt;Here are the main features it includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Color Wheel:&lt;/strong&gt; A nice, smooth circle to pick exact colors.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Sliders:&lt;/strong&gt; Easy sliders to change hue, saturation, lightness, and transparency.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Image Picker:&lt;/strong&gt; Users can upload an image and pick a color straight from the picture.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Palettes:&lt;/strong&gt; You can set up custom color swatches for your users to pick from.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Pencil Mode:&lt;/strong&gt; A drawing tool feature.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Custom Modes:&lt;/strong&gt; You can change the layout to fit exactly what your app needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Amazing Documentation (Like Shadcn UI)
&lt;/h2&gt;

&lt;p&gt;My favorite thing about Chroma Panel is the documentation. &lt;/p&gt;

&lt;p&gt;We all hate when a package has bad docs. But the Chroma Panel website looks and feels like modern docs, very similar to Shadcn UI or Base UI. &lt;/p&gt;

&lt;p&gt;They explain everything in plain English. You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Clear, step-by-step guides on how to install it.&lt;/li&gt;
&lt;li&gt;  Code examples that you can just copy and paste.&lt;/li&gt;
&lt;li&gt;  Details on how to use their custom React hooks.&lt;/li&gt;
&lt;li&gt;  Examples of how to set up every single feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Set It Up
&lt;/h2&gt;

&lt;p&gt;It is very easy to use. First, install the package in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;chroma-panel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, you just bring it into your React component. Here is a simple example to show how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChromaPanel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chroma-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MyColorPicker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;myColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMyColor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#ff0000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Pick a Color&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChromaPanel&lt;/span&gt; 
        &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;myColor&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; 
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newColor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setMyColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newColor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; 
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;The hex code is: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;myColor&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is all you need to get a great-looking color picker on your screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;If you are starting a new React project, or if you want to replace an old, boring color picker, you should check this one out. It saves a lot of time and makes your app look much better.&lt;/p&gt;

&lt;p&gt;You can learn more about it at these links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Read the Docs:&lt;/strong&gt; &lt;a href="https://chroma-panel.jscrate.dev/" rel="noopener noreferrer"&gt;https://chroma-panel.jscrate.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;NPM Page:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/chroma-panel" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/chroma-panel&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me know in the comments what color picker you normally use!&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>react</category>
      <category>ui</category>
    </item>
  </channel>
</rss>
