Chrome Built-in AI Practical Guide — Integrating Summarizer, Translator, and Writing Assistance Without Server Costs
In Chrome 138 (June 2025), Summarizer, Translator, and LanguageDetector shipped as stable, and in Chrome 148, LanguageModel (Prompt API) also reached stable. At Google I/O 2026, the "Agentic Web" keyword was introduced alongside the addition of the Gemma 197M ultra-lightweight model. In short, Chrome has embedded the Gemini Nano model directly in the browser and lets you call it using standard Web API patterns — no API key, no server, no token costs.
In practice, a news summary widget processes tens of KB of text per second in around 300ms with no cloud round-trip, and automatic translation of multilingual community comments — detection through translation — completes within a single user interaction. This article walks through the real pain points you'll hit when shipping to production: from checking availability and handling fallbacks to implementing actual summarization, translation, and writing scenarios.
Core Concepts
API Structure: Two Layers
Chrome Built-in AI can be divided into two broad layers.
| Layer | API | Role |
|---|---|---|
| General-purpose (Low-level) | LanguageModel (Prompt API) |
Free-form prompts, multimodal input, JSON output |
| Task-specific (High-level) | Summarizer, Translator, LanguageDetector, Writer, Rewriter, Proofreader |
High-level APIs optimized for specific tasks |
Entry Point History: window.ai → navigator.ai → Static Classes
In the early Origin Trial days (around Chrome 127), the form was window.ai.summarizer.create(). Between Chrome 131 and 137 the entry point moved to navigator.ai (most docs and demos still use this notation), and during the stable promotion process, each task API being exposed as a global static class became the standard.
To summarize:
window.ai.*— Early prototype syntax. Do not use. Does not work in current Chrome.navigator.ai.summarizer.create()— Widely present in Origin Trial-era docs; some paths still work.Summarizer.create()(static class) — The recommended approach in the current stable spec. All examples in this article use this form.
Write new code using the static class pattern, and flag any navigator.ai.* you find in legacy code as a migration target.
availability() — The First Thing to Check
Every API requires an availability check before use. There are four possible return values.
const availability = await Summarizer.availability();
// 'available' — Model is already downloaded and ready to use immediately
// 'downloadable' — Available but model download is required
// 'downloading' — Download currently in progress
// 'unavailable' — Not supported (hardware requirements not met or Chrome not supported)One thing that's easy to get confused about here: calling create() in the 'downloadable' state automatically starts the download and returns a session once it completes — no separate trigger needed. To show download progress to the user, you need to catch the downloadprogress event via a monitor callback. If a download is already in progress in another tab ('downloading'), create() waits for it to finish and then returns the session.
Summarizer API — Three Dimensions of Summarization
Summarizer.create() takes three options, and their combination determines the output style.
const summarizer = await Summarizer.create({
type: 'key-points', // 'tldr' | 'teaser' | 'headline' | 'key-points'
format: 'markdown', // 'markdown' | 'plain-text'
length: 'short', // 'short' | 'medium' | 'long'
});| type | Output form | Suitable use |
|---|---|---|
key-points |
List of key points | Blog/news summary widget |
tldr |
One or two sentence compression | Email threads, chat summaries |
teaser |
Curiosity-inducing introduction | Social media previews, card UI |
headline |
Short headline format | SEO meta title generation |
When a model download is required, you can display progress.
const summarizer = await Summarizer.create({
type: 'key-points',
format: 'markdown',
length: 'short',
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
console.log(`Model download: ${Math.round(e.loaded * 100)}%`);
});
},
});Translator + LanguageDetector — The Combination Is Key
The translation API can be used on its own, but it really shines when combined with automatic language detection.
const detector = await LanguageDetector.create();
const results = await detector.detect(userInput);
// results[0].detectedLanguage → BCP 47 codes like 'ko', 'en', 'ja'
// results[0].confidence → confidence score from 0 to 1
const translator = await Translator.create({
sourceLanguage: results[0].detectedLanguage,
targetLanguage: 'en',
});
const translated = await translator.translate(userInput);On first use, Translator downloads a separate dedicated translation model for each language pair. Because it is a separate model from Gemini Nano, there may be an initial wait the first time you use a new pair.
LanguageModel (Prompt API) — A Flexible General-Purpose Option
When the task APIs don't cover your use case, you can send prompts directly via the Prompt API. You can inject system context via initialPrompts and receive streaming responses with promptStreaming().
const session = await LanguageModel.create({
initialPrompts: [
{
role: 'system',
content: 'You are an assistant that helps with code reviews. Answer in English.',
},
],
});
const result = await session.prompt('Tell me what is wrong with this function: ' + codeSnippet);
const stream = session.promptStreaming('Refactor this code: ' + codeSnippet);
for await (const chunk of stream) {
outputElement.textContent += chunk;
}Version note: As of Chrome 148,
promptStreaming()emits delta (incremental) chunks. The examples in this article assume this behavior and concatenate withoutputElement.textContent += chunk. However, early Origin Trial builds have a history of emitting cumulative strings, so it is recommended to log the actual chunk format in the target Chrome version once before shipping.
Real-World Applications
Scenario 1 — News/Blog Summary Widget
The most common usage pattern. Attach an "View AI Summary" button below a long article, and generate a summary on click. Initialize once with a reusable class and render the markdown with marked.
import { marked } from 'marked';
class ArticleSummarizer {
#summarizer = null;
#initPromise = null;
async init() {
if (this.#summarizer) return true;
if (this.#initPromise) return this.#initPromise;
this.#initPromise = (async () => {
const availability = await Summarizer.availability();
if (availability === 'unavailable') return false;
this.#summarizer = await Summarizer.create({
type: 'key-points',
format: 'markdown',
length: 'short',
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
document.querySelector('#download-progress').textContent =
`Preparing model... ${Math.round(e.loaded * 100)}%`;
});
},
});
return true;
})();
return this.#initPromise;
}
async summarize(articleText) {
if (!this.#summarizer) throw new Error('Summarizer not initialized');
return this.#summarizer.summarize(articleText);
}
}
const summarizer = new ArticleSummarizer();
document.querySelector('#summarize-btn').addEventListener('click', async () => {
const btn = document.querySelector('#summarize-btn');
const output = document.querySelector('#summary-output');
const articleText = document.querySelector('#article-content').textContent;
btn.disabled = true;
try {
const ready = await summarizer.init();
const summary = ready
? await summarizer.summarize(articleText)
: await fallbackToCloudSummarizer(articleText);
output.innerHTML = marked(summary);
} catch (err) {
console.error('Summary failed:', err);
output.textContent = 'Failed to generate summary. Please try again later.';
} finally {
btn.disabled = false;
}
});init() returns immediately if a cached instance exists, and #initPromise prevents duplicate creation even on simultaneous clicks. try/catch/finally absorbs model load failures, quota exceeded errors, and timeouts.
Scenario 2 — Automatic Comment Detection and Translation
A pattern that provides a "View Translation" button when a foreign-language comment appears in a multilingual community. Cache LanguageDetector as a module-level singleton so it doesn't get recreated on every call.
let detectorPromise = null;
function getDetector() {
if (!detectorPromise) detectorPromise = LanguageDetector.create();
return detectorPromise;
}
const translatorCache = new Map();
function getTranslator(sourceLanguage, targetLanguage) {
const key = `${sourceLanguage}->${targetLanguage}`;
if (!translatorCache.has(key)) {
translatorCache.set(
key,
Translator.create({ sourceLanguage, targetLanguage }),
);
}
return translatorCache.get(key);
}
async function translateComment(commentElement) {
const originalText = commentElement.dataset.originalText;
const targetLanguage = 'ko';
const detector = await getDetector();
const [{ detectedLanguage, confidence }] = await detector.detect(originalText);
if (confidence < 0.7) return;
if (detectedLanguage === targetLanguage) return;
const availability = await Translator.availability({
sourceLanguage: detectedLanguage,
targetLanguage,
});
if (availability === 'unavailable') {
return cloudTranslateFallback(originalText, detectedLanguage, targetLanguage);
}
const translator = await getTranslator(detectedLanguage, targetLanguage);
const translated = await translator.translate(originalText);
commentElement.querySelector('.translated-text').textContent = translated;
commentElement.querySelector('.translation-badge').hidden = false;
}Scenario 3 — Email Draft Generation and Tone Adjustment
A pipeline that drafts with Writer and refines with Rewriter. Because Writer, Rewriter, and Proofreader are Origin Trial targets in the Chrome 137–148 range, both feature detection and availability checks are required.
async function draftEmail({ keywords, context, tone = 'formal' }) {
if (!('Writer' in globalThis)) {
return cloudWriterFallback({ keywords, context, tone });
}
const availability = await Writer.availability();
if (availability === 'unavailable') {
return cloudWriterFallback({ keywords, context, tone });
}
const writer = await Writer.create({
tone, // 'formal' | 'neutral' | 'casual'
format: 'plain-text',
length: 'medium',
});
return writer.write(
`Draft an email that includes the following keywords: ${keywords}`,
{ context },
);
}
async function adjustTone(existingText, targetTone) {
if (!('Rewriter' in globalThis)) return existingText;
const availability = await Rewriter.availability();
if (availability === 'unavailable') return existingText;
const rewriter = await Rewriter.create({
tone: targetTone,
format: 'plain-text',
length: 'as-is', // Rewriter's length values: 'shorter' | 'as-is' | 'longer'
});
return rewriter.rewrite(existingText, {
context: 'Rewrite to fit email format',
});
}
Rewriter'slengthaccepts three values per spec:'shorter' | 'as-is' | 'longer'. Use'as-is'to preserve the original length.'same'is not a valid value.
Scenario 4 — Onboarding Chatbot with Prompt API
Inject app state into initialPrompts to provide context-aware help. Because LanguageModel is the most hardware-demanding API, the availability() check and session destroy() are especially important.
async function createOnboardingBot(userContext) {
const availability = await LanguageModel.availability();
if (availability === 'unavailable') {
return createCloudChatFallback(userContext);
}
const session = await LanguageModel.create({
initialPrompts: [
{
role: 'system',
content: `You are the onboarding assistant for ${userContext.appName}.
Current user state:
- Plan: ${userContext.plan}
- Completed steps: ${userContext.completedSteps.join(', ')}
- Current page: ${userContext.currentPage}
Respond concisely and helpfully in English.`,
},
],
});
return {
async ask(question) {
// As of Chrome 148: promptStreaming emits delta chunks.
const stream = session.promptStreaming(question);
let response = '';
for await (const chunk of stream) {
response += chunk;
updateChatUI(response);
}
return response;
},
destroy() {
session.destroy(); // Release VRAM/memory
},
};
}Pros and Cons
Pros and Cons at a Glance
| Item | Details |
|---|---|
| Cost | No API call or token charges. Marginal cost of $0 even as traffic grows |
| Privacy | Data processed locally only, never sent to external servers |
| Offline | Works without a network connection (after model download completes) |
| Latency | Immediate response with no cloud round-trip (once model is loaded) |
| Integration complexity | Standard Promise/async-await/streaming patterns, no authentication required |
| Limitation | Details |
|---|---|
| Browser-only | Chrome exclusive. Not supported on Firefox, Safari, or Edge |
| Hardware requirements (Prompt API, Writer, Rewriter) | 22 GB+ free storage for Gemini Nano, GPU with 4 GB+ VRAM or 16 GB RAM + 4+ CPU cores. Among the task APIs, Summarizer runs on the same Gemini Nano so requirements are identical, but Translator and LanguageDetector use much smaller dedicated models so storage and memory requirements are lower. |
| No mobile support | Most APIs are desktop-only. Not supported on Android Chrome |
| Initial download | First-use model download of tens to hundreds of MB |
| Language support | Translator and Summarizer support major languages including Korean (ko), English, Spanish, Japanese, German, French, and Chinese (from Chrome 138 stable). Writer and Rewriter are expanding from an English-centric base during the Origin Trial phase, and Korean quality is still inconsistent — validation before production is needed. |
| Hallucination | Inaccurate responses possible due to the small model size. High-risk domains require a validation layer |
| Policy dependency | Subject to Google's Generative AI usage policies; content moderation is controlled by Google |
Common Mistakes in Practice
1. Skipping availability()
Calling create() directly throws an exception when the model is absent. Always check status with availability() and branch accordingly.
2. Deploying without a fallback
Even among current Chrome users, not all meet the hardware requirements. Without a path that switches to a cloud API on 'unavailable', those users lose the feature entirely.
3. Omitting session destroy()
Sessions created with LanguageModel.create() hold VRAM/memory. Multiple tabs using the same API can cause resource contention, so call session.destroy() after you're done with a session.
4. Ignoring Origin Trial status
Writer, Rewriter, and Proofreader are Origin Trial-only through Chrome 137–148. Before shipping, verify the current trial period is still valid and include feature detection like 'Writer' in globalThis.
5. Recreating Detector/Translator on every call
Calling LanguageDetector.create() or Translator.create() on every function invocation repeatedly incurs the model load cost per language pair. Reuse instances with a module-level singleton or a Map cache.
When Is This a Good Fit?
All three paths ultimately converge on a Built-in AI + cloud fallback combination. Only the priority of when and which API to fall back to differs.
Closing Thoughts
Chrome Built-in AI has become the first practical option for frontend developers to add AI features to a product without a server. The limitations — Chrome-only and high hardware requirements — are clear, but for use cases where data privacy matters or cloud API costs are a concern, it's worth trying right now.
To recap the key points:
- Follow the
availability()→create()→ use →destroy()pattern - Always prepare a cloud fallback for
'unavailable' - Cache Detector, Translator, and Summarizer instances to avoid paying the initial load cost repeatedly
- Check Origin Trial status (Writer, Rewriter, Proofreader) before shipping
If you want to get started right now, here's the recommended order:
- Enable
chrome://flags/#prompt-api-for-gemini-nanoin Chrome DevTools and check the result ofawait Summarizer.availability()in the console - Try each API directly in the Chrome AI Playground (
chrome.dev/web-ai-demos/) - Pick one text-processing spot in an existing project, run a pilot with
SummarizerorTranslatorincluding a fallback, and deploy
Standardization discussions for the Translator, Summarizer, and Writer APIs are underway at the W3C Web Machine Learning Working Group. Edge is Chromium-based so practical portability is expected to be relatively fast. Firefox already runs its own on-device translation (Firefox Translations) so API integration discussions remain, and Safari has no official roadmap yet. In short, it's Chrome-only for now, but learning the API in its current standardization-track form will lower the cost of supporting other browsers down the road.
References
- AI on Chrome — Built-in AI Official Overview
- Full List of Built-in AI APIs
- Prompt API (LanguageModel) Detailed Guide
- Summarizer API Guide
- Translator API Guide
- Writer API Guide
- Rewriter API Guide
- Proofreader API Guide
- Language Detection Guide
- Getting Started with Built-in AI
- Google I/O 2026 Top 15 Chrome Updates
- Build new features using built-in AI — I/O 2026
- New in Chrome 138
- Exploring Chrome's Built-In AI APIs: A Hands-On Guide
- Running LLMs in the Browser: WebGPU, Transformers.js, and Chrome's Built-in AI
- GoogleChrome/modern-web-guidance (GitHub)