Turn browser text selections into portable, source-grounded COS Context Objects for AI features, editors, annotations, and developer tools.
Try the live Playground · npm · COS documentation · COS on GitHub
If your product reacts to text selection, COS Web Adapter gives you the source-grounded context that selection.toString() leaves behind.
A browser selection normally becomes an isolated string:
{ "text": "$49" }COS Web Adapter keeps the selection connected to its source and deterministic structure:
{
"version": "0.2",
"source": { "type": "webpage", "title": "Pricing" },
"selection": { "text": "$49" },
"context": {
"scope": "container",
"segments": [
{
"type": "table",
"text": "$49",
"selectedText": "$49",
"relations": [
{ "type": "row_header", "label": "Pro" },
{ "type": "column_header", "label": "Monthly price" }
]
}
]
}
}The adapter does not summarize content, infer intent, generate prompts, call an AI service, or transmit data. It extracts deterministic evidence from the current document and returns a plain JavaScript object.
- “Ask AI about this selection” experiences
- in-page copilots and contextual toolbars
- browser extensions and reading tools
- document comments, highlights, and annotations
- knowledge-base citations and research tools
- rich-text editors and documentation systems
- source-grounded search, RAG, and agent inputs
- structured selections from tables, lists, headings, code, and forms
It converts a browser selection into a lean COS Context Object:
window.getSelection()
↓
Web Adapter
↓
ContextObject
├── version
├── source
├── selection
├── context
│ ├── scope
│ ├── segments[]
│ └── relations?
├── meta
└── extensions[]
npm install @context-object-spec/web-adapterimport { adaptCurrentSelection } from "@context-object-spec/web-adapter";
document.addEventListener("mouseup", () => {
const cos = adaptCurrentSelection({ inputType: "mouse" });
console.log(cos);
});Use the events that match your interface. pointerup covers mouse, pen, and touch interactions; selectionchange also detects keyboard-driven selections. Debounce selectionchange in interfaces that perform expensive downstream work.
document.addEventListener("selectionchange", () => {
const cos = adaptCurrentSelection();
if (cos) console.log(cos);
});Use scoped APIs when only one area of the page should produce Context Objects:
import { adaptCurrentSelectionWithin } from "@context-object-spec/web-adapter";
const editor = document.getElementById("editor");
editor?.addEventListener("mouseup", () => {
const cos = adaptCurrentSelectionWithin(editor, {
inputType: "mouse"
});
if (!cos) return;
console.log(cos);
});The adapter returns null when the current selection is outside the scope, crosses the scope boundary, or is collapsed.
Same-origin iframe documents can be used as scopes:
const frameDocument = iframe.contentDocument;
frameDocument?.addEventListener("mouseup", () => {
const cos = adaptCurrentSelectionWithin(frameDocument, {
inputType: "mouse"
});
});Cross-origin iframe selections cannot be read by the parent page because of browser security boundaries.
Try the live COS Web Adapter Playground, or run it locally.
From this package directory:
npm run dev:playgroundThen open:
http://localhost:4173/playground/
dev:playground starts TypeScript watch mode and the no-cache playground server together.
import { useCallback, useState } from "react";
import { adaptCurrentSelection, type AdapterResult } from "@context-object-spec/web-adapter";
export function SelectionInspector() {
const [context, setContext] = useState<AdapterResult | null>(null);
const capture = useCallback(() => {
setContext(adaptCurrentSelection({ inputType: "mouse" }));
}, []);
return (
<main onMouseUp={capture}>
<p>Select text here.</p>
<pre>{JSON.stringify(context, null, 2)}</pre>
</main>
);
}import { ref } from "vue";
import { adaptCurrentSelection, type AdapterResult } from "@context-object-spec/web-adapter";
export function useSelectionContext() {
const context = ref<AdapterResult | null>(null);
function capture() {
context.value = adaptCurrentSelection({ inputType: "mouse" });
}
return { context, capture };
}createWebAdapter(options?): WebAdapter
createScopedWebAdapter(scope, options?): WebAdapter
adaptCurrentSelection(options?): AdapterResult | null
adaptCurrentSelectionWithin(scope, options?): AdapterResult | null
adaptSelection(selection, options?): AdapterResult | null
adaptSelectionWithin(scope, selection, options?): AdapterResult | null
isSelectionInside(scope, selection): booleanUseful options:
adaptCurrentSelection({
inputType: "mouse",
includeSelectedHtml: true,
includeContainerText: true,
maxContainerTextLength: 4000
});Copyable, type-checked integration recipes are available for:
They are vendor-neutral: your application controls transport, storage, authentication, and model choice.
selection.text is exactly what the user selected.
source describes where the selection came from:
{
"type": "webpage",
"id": "https://example.com/article",
"uri": "https://example.com/article",
"title": "Pricing Guide",
"app": "browser",
"language": "en"
}context.segments[] is the main value of COS. Each segment is a source-grounded piece of nearby context:
Segments with selectedText contain the actual Selection. Segments without selectedText are deterministic nearby context, such as sibling list items, neighboring paragraphs, or cells from the same table row.
{
"version": "0.2",
"source": {
"type": "webpage",
"title": "Product Table"
},
"selection": {
"text": "$19",
"range": {
"startOffset": 0,
"endOffset": 3,
"startNodeId": "price",
"endNodeId": "price",
"direction": "forward"
}
},
"context": {
"scope": "container",
"segments": [
{
"id": "price",
"type": "table",
"text": "$19",
"selectedText": "$19",
"relations": [
{ "type": "column_header", "label": "Base price" },
{ "type": "row_header", "label": "Starter" }
]
}
]
},
"meta": {
"adapter": "web-adapter",
"stages": ["selection", "source", "context", "relations"]
}
}For inline prose selections, a segment includes bounded before/after text:
{
"scope": "inline",
"segments": [
{
"id": "intro",
"type": "paragraph",
"text": "The adapter should preserve nearby context without turning the selection into a prompt.",
"selectedText": "nearby",
"beforeText": "The adapter should preserve ",
"afterText": " context without turning the selection into a prompt."
}
]
}For selections that cross multiple logical containers, context.scope is multi-container, and each covered container becomes its own segment:
{
"scope": "multi-container",
"segments": [
{
"type": "paragraph",
"selectedText": "adapter should preserve nearby context",
"text": "The adapter should preserve nearby context without turning the selection into a prompt."
},
{
"type": "list",
"selectedText": "Selections can begin in ordinary prose.",
"text": "Selections can begin in ordinary prose.",
"relations": [
{ "type": "list_item", "label": "item 1" }
]
}
]
}The adapter only emits deterministic structure signals. It does not summarize, classify intent from prose, or generate prompts.
Strong HTML signals may appear as segment.role:
code_snippet:<pre>or<code>definition:<dfn>reference:<cite>or<a href>output:<output>or<samp>
Deterministic relationships may appear in context.relations or segment.relations:
sectionrow_headercolumn_headerlabellist_item
Browser-specific DOM paths and selected HTML are kept in the io.github.context-object-spec.browser Extension for debugging and traceability.
- Modern browsers with the DOM Selection and Range APIs are supported.
- A parent page cannot inspect selections inside a cross-origin iframe.
- Collapsed selections and selections outside a configured scope return
null. - All processing is local and synchronous; the adapter performs no network requests.
- Applications remain responsible for obtaining appropriate user consent before storing or transmitting Context Objects.
- Context Object Specification repository
- Published COS documentation
- COS v0.2 JSON Schema
- COS Web Adapter repository
- Live Playground
Licensed under Apache-2.0. Copyright 2026 uncosy.