Add On-Device Summarization to React with Chrome's Summarizer API A developer built a local summarization feature into the Trip Workbench demo using Chrome's on-device Summarizer API, adding capability-state checks, download feedback, cancellation, and a guard that prevents a stale summary from appearing beside an edited brief. The implementation keeps the original brief visible, sends the full original text rather than the lossy summary to the separate Gemini field-extraction step, and requires no application API key for the local inference path. Chrome documents Summarizer support from version 138, though the API remains limited-availability and depends on platform, hardware, storage, and model availability. A travel brief often mixes the trip itself with meeting context, preferences, and unresolved decisions. A short summary can help the person review it before requesting proposed form changes. That is a useful place for Chrome’s Summarizer API: a bounded reading aid beside an existing text field. In Trip Workbench https://github.com/rajudandigam/trip-workbench , the public demo I built and maintain, the original brief already feeds a separate Gemini feature that proposes trip fields for review. The local summary has a different purpose. It helps the person read the brief; it does not update the trip, replace the source text, or decide which details the cloud model receives. This walkthrough builds on commit 4b3f370 https://github.com/rajudandigam/trip-workbench/tree/4b3f37034fc291ad5221969e0507d349878dc150 . The demo has the basic API call. The lifecycle extensions below add capability states, download feedback, cancellation, and a boundary that prevents an old summary from appearing beside a changed brief. Consider this synthetic brief: I need to attend a customer workshop in New York on November 12–13, 2026. Depart SFO for JFK on November 11. Economy, one traveler. Do not book a hotel because I will stay with family. Travel only if the client confirms the workshop by Friday. Plan to return November 14, but November 15 is still possible if the workshop extends. A useful summary would preserve the dates, the hotel exclusion, the confirmation condition, and the uncertainty around the return. A fluent summary that converts those details into a confirmed itinerary would be misleading. Keep the original visible beside the output. Label the result “Summary of this brief” and let the person regenerate it after editing. Continue sending the original brief to Gemini when the person selects “Suggest trip details.” Using a lossy summary as the extraction input would make any omitted condition harder for the next step to recover. The feature needs no application API key or @google/genai call. Chrome supplies the on-device model. Chrome documents that model use does not send the input to Google or a third party, while initial model setup can require a download. That statement applies to the local inference path; the application’s separate Gemini request remains a cloud operation. See Chrome’s Summarizer guide https://developer.chrome.com/docs/ai/summarizer-api . Chrome documents Summarizer support from version 138, subject to platform, hardware, storage, and model availability. It remains a limited-availability web API, so browser version alone is insufficient. Serve the feature in a secure context https://developer.mozilla.org/en-US/docs/Web/API/Summarizer and retain the ordinary text workflow. There are two checks: whether the API exists, and whether it can support the configuration you want. Use the same core options for checking availability and creating the instance. This example requests English key points in plain text. The existing demo uses a short tldr ; medium key points give this review task more room without guaranteeing that every detail survives. The examples use JavaScript and JSX. For TypeScript, Chrome recommends @types/dom-chromium-ai https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/dom-chromium-ai . Replace the demo’s handwritten Summarizer global declaration if adopting those types. In summary-client.js : js export const summaryOptions = { type: "key-points", format: "plain-text", length: "medium", expectedInputLanguages: "en" , outputLanguage: "en", }; export async function checkSummaryAvailability API = globalThis.Summarizer { if API return "unsupported"; try { const value = await API.availability summaryOptions ; return "available", "downloadable", "downloading", "unavailable" .includes value ? value : "unknown"; } catch { return "error"; } } Map the availability result https://developer.mozilla.org/en-US/docs/Web/API/Summarizer/availability static to a useful UI state: | State | What the interface should do | |---|---| | Checking | Keep the brief editable while checking support | | Unsupported or unavailable | Explain that on-device summarization cannot run here | | Downloadable | Offer the action and explain that model setup may require a download | | Downloading | Offer to wait for readiness and show progress once attached | | Available | Offer summarization immediately | | Unknown or error | Offer a readiness retry and preserve the brief | Run this capability check in a parent Effect with a cleanup guard so a late result cannot update an abandoned panel. Do not call create from that Effect. Chrome’s setup guidance https://developer.chrome.com/docs/ai/get-started requires user interaction when model downloads are needed. Let the Summarize button call creation directly. Separating the readiness probe also keeps a potentially slow check out of the click path. Availability can change. A successful probe is not a reservation; creation still needs error handling. When the language or summary configuration changes, check the new options again. For this occasional button action, one instance per request keeps resource ownership straightforward. Create it, generate the summary, and release it in finally . Add this function to the same module: js export async function summarizeBrief brief, { signal, onUpdate = = {} }, API = globalThis.Summarizer, { signal.throwIfAborted ; if API throw new Error "On-device summarization is unavailable." ; if brief.trim throw new Error "Enter a brief first." ; let session; try { session = await API.create { ...summaryOptions, signal, monitor monitor { monitor.addEventListener "downloadprogress", event = { if signal.aborted onUpdate { phase: "downloading", percent: Math.round event.loaded 100 , } ; }, { signal } ; }, } ; signal.throwIfAborted ; onUpdate { phase: "summarizing" } ; const text = await session.summarize brief, { signal } ; signal.throwIfAborted ; return text; } finally { session?.destroy ; } } The signal goes to both create https://developer.mozilla.org/en-US/docs/Web/API/Summarizer/create static and summarize . Cancelling while creation is pending must reach that operation, too. Aborting a creation signal after it resolves also destroys the resulting instance, so this signal belongs to this one request. The final abort check prevents a late result from being accepted after cancellation. The finally block releases any instance that was acquired, including when generation fails. Download progress and generation are separate phases. The Writing Assistance API specification https://webmachinelearning.github.io/writing-assistance-apis/ defines download progress as a fraction with a total of 1. A displayed 100% means the download stage reached completion; the interface should still wait for instance creation and summarization to finish. Destroying an instance releases its resources. It does not mean that the browser removes its downloaded model, nor does this code promise to cancel a shared download for every other page. Batch output keeps this first implementation small. Streaming can improve perceived responsiveness for longer output, but it also requires managing incomplete text and clearing partial results when the request becomes stale. The current demo creates an AbortController inside its click handler but does not retain it for cancellation. It also stores the resulting text without binding it to a brief version. A person can therefore edit the brief while a summary is running, or start another request before the first finishes. The last response to arrive can overwrite the display even when it belongs to older input. A dedicated summary component can own one active controller. This hook cancels earlier work and accepts updates only from the currently active request: js import { useEffect, useRef, useState } from "react"; import { summarizeBrief } from "./summary-client.js"; export function useBriefSummary brief { const active = useRef null ; const view, setView = useState { phase: "idle" } ; useEffect = = { active.current?.abort ; active.current = null; }, ; function cancel { active.current?.abort ; active.current = null; setView { phase: "cancelled" } ; } async function start { active.current?.abort ; const controller = new AbortController ; active.current = controller; const isCurrent = = active.current === controller && controller.signal.aborted; setView { phase: "preparing" } ; try { const text = await summarizeBrief brief, { signal: controller.signal, onUpdate update { if isCurrent setView update ; }, } ; if isCurrent setView { phase: "done", text } ; } catch error { if active.current == controller return; setView { phase: controller.signal.aborted ? "cancelled" : "error", message: error?.name === "QuotaExceededError" ? "This brief is too long. Choose a smaller section." : "Could not summarize on this device. Try again or keep reading the brief.", } ; } finally { if active.current === controller active.current = null; } } return { view, start, cancel }; } The hook controls request ordering within one mounted summary component. Bind that component’s identity to the source so an edit also clears an already completed summary. Trip Workbench already tracks briefVersion , editor generation, and session generation. Use them with the draft ID: js const summarySourceKey = JSON.stringify state.sessionGeneration, state.editorGeneration, state.savedDraft?.id, state.briefVersion, ;