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",()=>{constcos=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",()=>{constcos=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";consteditor=document.getElementById("editor");editor?.addEventListener("mouseup",()=>{constcos=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:
constframeDocument=iframe.contentDocument;frameDocument?.addEventListener("mouseup",()=>{constcos=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,typeAdapterResult}from"@context-object-spec/web-adapter";exportfunctionSelectionInspector(){const[context,setContext]=useState<AdapterResult|null>(null);constcapture=useCallback(()=>{setContext(adaptCurrentSelection({inputType: "mouse"}));},[]);return(<mainonMouseUp={capture}><p>Select text here.</p><pre>{JSON.stringify(context,null,2)}</pre></main>);}import{ref}from"vue";import{adaptCurrentSelection,typeAdapterResult}from"@context-object-spec/web-adapter";exportfunctionuseSelectionContext(){constcontext=ref<AdapterResult|null>(null);functioncapture(){context.value=adaptCurrentSelection({inputType: "mouse"});}return{ context, capture };}createWebAdapter(options?): WebAdaptercreateScopedWebAdapter(scope,options?): WebAdapteradaptCurrentSelection(options?): AdapterResult|nulladaptCurrentSelectionWithin(scope,options?): AdapterResult|nulladaptSelection(selection,options?): AdapterResult|nulladaptSelectionWithin(scope,selection,options?): AdapterResult|nullisSelectionInside(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.