Overview
<local-chat> is a self-contained custom element: it renders its own toggle button,
transcript, input box, and controls. There's no separate element to attach it to and no server round-trip
— every response comes from Chrome's on-device Prompt API (window.LanguageModel), running
entirely on the visitor's machine.
Answers are grounded in whatever Context you provide: raw text, a CSS selector that extracts matching page elements, or both combined. If the on-device model API isn't available in the visitor's browser, the element is a complete no-op — it renders nothing and does nothing.
Install
npm install @internetarchive/local-chat
Usage
<script type="module" src="@internetarchive/local-chat"></script>
<local-chat
context-selector="main"
starters='["What can you help with?"]'
icebreakers
max-followups="3"
></local-chat>
The chat widget on this page is a live example: it's grounded in this very documentation via
context-selector="main", so you can ask it questions about
<local-chat> itself.
Attributes
| Attribute | Description |
|---|---|
instructions |
Text establishing the assistant's persona/behavior. Has a sensible built-in default (answer only from the provided Context, say so plainly when something isn't covered). Also available as a property, which takes priority over the attribute. |
context |
Raw text, or a JSON array/object of structured context, to ground answers in. Combines with
context-selector when both are set. Also available as a property, which takes priority
over the attribute — a string is parsed the same way, or pass an array/object directly (no need to
JSON.stringify it yourself).
|
context-selector |
A CSS selector. Every matching element's .innerText is concatenated into the Context. |
starters |
A host-provided opening suggestion, or a JSON array of several, shown before any message has been sent. Purely declarative — never generated by the model. Also available as a property, which accepts a single string the same way the attribute does, or an array of strings directly. |
icebreakers |
Boolean. When present, the model generates its own opening suggestions (grounded in Context) once
ready, shown alongside any starters.
|
empty-message |
Plain text shown before the first message, only while neither a starters entry nor a
generated Icebreaker exists yet to show instead. Has a sensible built-in default describing the
Widget as an on-device AI chat. Also available as a property, which takes priority over the
attribute.
|
max-followups |
Non-negative integer capping how many Follow-ups get suggested after each response. Default: 3. Set to 0 to disable Follow-ups (and Icebreakers). |
max-history |
Non-negative integer capping how many past Exchanges (a user message and its reply, saved as a pair once the reply finishes) are persisted and restored across page loads. Default: 5. Set to 0 to disable History entirely. |
storage-key |
The storage scope shared by History and by the widget's own Visual state (open/closed, dragged
position, resized size). "url" (default) scopes to the current path and query string;
"path" scopes to the path only (shared across query variations); "origin"
shares both across the entire site. Any other value is used verbatim as a custom scope, shared by
every page that sets the same storage-key.
|
collapsed |
The widget's initial visibility state, used only the very first time nothing's been persisted yet
for the resolved storage-key scope (see Visual state below). Defaults to collapsed (a
floating button) unless set to the literal string "false", which starts it expanded.
|
trigger-selector |
A CSS selector. Every matching element (queried once, at render time) becomes a trigger — clicking it
toggles the widget open/closed, the same as calling toggle(). Useful for invoking the
widget from your own existing button instead of the floating toggle.
|
hide-toggle |
Boolean. Hides the built-in floating toggle button — useful alongside trigger-selector or the methods below, if you want only your own trigger. |
logo |
Overrides the default 💬 glyph, used both for the collapsed toggle button and, in the panel header,
right before the title. Plain text/emoji renders as-is; a value that looks like an image reference
(a data:image/ URI, a URL, or a filename ending in .png/.jpg/
.gif/.svg/.webp/.avif) renders as an image
instead. Also available as a property, which takes priority over the attribute.
|
title |
Renders as a heading in the panel header, right after the logo. This is the native title
attribute/property every HTML element already has — no separate accessor needed. Defaults to
"Local Chat" when unset.
|
color-scheme |
Forces "light" or "dark" mode regardless of the OS/browser's dark-mode
preference. Unset (or any other value) follows that preference instead, via a built-in dark palette
on every themed custom property below — see Styling. Also available as a property, directly
reflected onto the attribute.
|
Methods
| Method | Description |
|---|---|
expand() |
Expands the widget (a no-op if already expanded). Call this from your own trigger element's click handler. |
collapse() |
Collapses the widget (a no-op if already collapsed). |
toggle() |
Expands if collapsed, collapses if expanded. This is what trigger-selector wires up automatically. |
Events
| Event | Detail |
|---|---|
local-chat-message-sent |
{ text } — dispatched when the user sends a message. |
local-chat-response-received |
{ text } — dispatched with the full rendered response once streaming finishes. |
local-chat-error |
{ error } — dispatched if generating a response fails. |
local-chat-expanded |
Dispatched when the widget transitions to expanded, however it was triggered. |
local-chat-collapsed |
Dispatched when the widget transitions to collapsed, however it was triggered. |
Styling
The widget renders in a Shadow DOM, themeable from the host page via CSS custom properties and
::part(). Every custom property below overrides its value in both light and dark mode at
once — the values shown are the light-mode defaults; each already has a built-in dark-mode default too,
auto-switching with the OS/browser's preference (or forced either way via the color-scheme
attribute) unless you override it here:
local-chat {
--local-chat-font-family: system-ui, sans-serif;
--local-chat-font-size: 0.9rem;
--local-chat-accent: #2563eb;
--local-chat-accent-color: #fff;
--local-chat-background: #fff;
--local-chat-color: #111;
--local-chat-border-color: #ccc;
--local-chat-radius: 0.5rem;
--local-chat-assistant-background: #f2f2f2;
--local-chat-code-background: #e5e5e5;
--local-chat-status-color: #777;
--local-chat-empty-state-color: #777;
--local-chat-shadow-color: rgba(0, 0, 0, 0.25);
}
local-chat::part(panel) {
/* every part is also addressable directly: toggle, panel, resize-handle,
panel-header, logo, title, clear, panel-close, transcript, message,
message-user, message-assistant, status, status-download, input-row,
input, send, starter(s), icebreaker(s), followup(s), empty-state */
}
Browser support
Requires a Chrome build with the on-device Prompt API (window.LanguageModel) available.
Everywhere else, <local-chat> renders nothing and does nothing — it never errors or
shows a broken widget.