Skip to content

Commit 3dddbbe

Browse files
align with dom + html spec
1 parent 7750229 commit 3dddbbe

1 file changed

Lines changed: 66 additions & 48 deletions

File tree

OpaqueRange/explainer.md

Lines changed: 66 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@
1818

1919
The current `Range` interface methods do not support retrieving or creating ranges that represent the `value` (rather than the element itself) of `<textarea>` and `<input>` elements. As a result, if web developers want to use the `getBoundingClientRect()` method in a `<textarea>` or `<input>` element to position a popup beneath the user's current caret for delivering contextual autocomplete suggestions or marking syntax errors as users type using the [Custom Highlight API](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API), they must find workarounds. These workarounds often involve cloning these elements and their styles into `<div>`s, which is both difficult to maintain and may impact the web application's performance.
2020

21-
This proposal aims to address these issues by introducing `OpaqueRange`, a new `AbstractRange` subclass that serves as a way to reference spans of an opaque string defined by a host specification (such as HTML) while preserving encapsulation.
22-
23-
An opaque string is a host-defined text representation whose internal structure is not exposed to authors. For example, in HTML this is the element’s value string for `<textarea>` and text-supporting `<input>` types (e.g. `text`, `search`, `url`, etc.). Authors can treat it as a simple string of code units indexed by offsets, without observing or manipulating how the browser represents or stores it internally.
21+
This proposal aims to address these issues by introducing `OpaqueRange`, a new `AbstractRange` subclass that references spans of encapsulated content within host-defined elements (such as `<textarea>` and `<input>` in HTML). `OpaqueRange` has real start and end containers internally, but the `startContainer` and `endContainer` getters return `null` (via an "is opaque" flag on `AbstractRange`), ensuring that the internal DOM structure is never exposed. Authors interact with the range only through offsets into the element's value.
2422

2523
## User-Facing Problem
2624

@@ -229,7 +227,7 @@ nameField.addEventListener('input', (e) => {
229227

230228
### Goal
231229

232-
Provide a way for host specifications (such as HTML) to obtain an `OpaqueRange` - a specialized, live `AbstractRange` whose boundary points are offsets into an implementation-defined opaque string defined by the host specification (for example, the `value` of `<textarea>`, text‑supporting `<input>`, or strings defined by custom elements in the future). This enables range-based operations (e.g. getting bounding rects, setting custom highlights, etc.) while restricting standard `Range` mutations to preserve encapsulation.
230+
Provide a way for web developers to obtain ranges over the value of `<textarea>` and `<input>` elements, enabling operations like `getBoundingClientRect()` and custom highlights, without exposing the elements' internal DOM structure.
233231

234232
### Non-goals
235233

@@ -239,23 +237,50 @@ Provide a way for host specifications (such as HTML) to obtain an `OpaqueRange`
239237

240238
## Proposed Approach
241239

242-
The `OpaqueRange` interface extends `AbstractRange` and provides a controlled way for host specifications to reference parts of an opaque string they define (such as the text value of `<textarea>`, textsupporting `<input>`, or a custom element-defined string). Host specifications are responsible for creating and updating `OpaqueRange` instances - web authors obtain them through host APIs (e.g. `getValueRange()` on text controls), rather than constructing or configuring them directly. `OpaqueRange` exposes useful endpoint information while limiting mutations that would otherwise expose or depend on internal implementation details.
240+
The `OpaqueRange` interface extends `AbstractRange` and provides a controlled way for host specifications to reference encapsulated content within elements they define (such as the text value of `<textarea>`, text supporting `<input>`, or content within custom elements in the future). Host specifications are responsible for creating and updating `OpaqueRange` instances web authors obtain them through host APIs such as `getValueRange()` on text controls.
243241

244-
Unlike `StaticRange`, `OpaqueRange` is **live** — it tracks changes to the underlying opaque string and automatically updates its start and end offsets, similar to how a regular `Range` tracks DOM mutations. This ensures that operations like `getBoundingClientRect()` or `getClientRects()` always reflect the current content, even after edits.
242+
Unlike `StaticRange`, `OpaqueRange` is **live** — it tracks changes to the underlying content and automatically updates its start and end offsets, similar to how a regular `Range` tracks DOM mutations. This ensures that operations like `getBoundingClientRect()` or `getClientRects()` always reflect the current content, even after edits. See the [Supports Opaque Ranges](#supports-opaque-ranges) section for how host specifications define the update behavior.
245243

246-
`OpaqueRange`'s live-update behavior also aligns conceptually with the `InputRange()` from [Keith Cirkels Richer Text Fields proposal](https://open-ui.org/components/richer-text-fields.explainer/), but is designed as a general-purpose API for referencing opaque strings as defined by host specifications, not limited to form controls.
244+
`OpaqueRange` aligns conceptually with the `InputRange()` from [Keith Cirkel's Richer Text Fields proposal](https://open-ui.org/components/richer-text-fields.explainer/), which also proposed adding a new range subclass for `<input>` and `<textarea>` elements. `OpaqueRange` differs in that it extends `AbstractRange` (not `Range`) and is designed as a general-purpose, host-extensible API not limited to form controls.
247245

248246
### Properties and Methods
249247

248+
#### IDL
249+
```webidl
250+
[Exposed=Window]
251+
interface AbstractRange {
252+
readonly attribute Node? startContainer;
253+
readonly attribute unsigned long startOffset;
254+
readonly attribute Node? endContainer;
255+
readonly attribute unsigned long endOffset;
256+
readonly attribute boolean collapsed;
257+
};
258+
259+
[Exposed=Window]
260+
interface OpaqueRange : AbstractRange {
261+
DOMRectList getClientRects();
262+
DOMRect getBoundingClientRect();
263+
};
264+
```
265+
266+
Note that `AbstractRange`'s `startContainer` and `endContainer` are changed from `Node` to `Node?` to accommodate `OpaqueRange`. A new **is opaque** flag (initially `false`) is added to all ranges; only `OpaqueRange` objects have it set to `true`.
267+
268+
```webidl
269+
// On HTMLInputElement and HTMLTextAreaElement:
270+
[NewObject] OpaqueRange getValueRange(unsigned long start, unsigned long end);
271+
```
272+
250273
#### Properties
274+
`OpaqueRange` objects cannot be constructed directly; they are created by specifications defining elements that support opaque ranges. In HTML, they are obtained via `getValueRange()`.
275+
251276
`OpaqueRange` exposes useful endpoint information while maintaining encapsulation:
252-
- `startOffset` and `endOffset`: Nonnegative integers that index into the opaque string defined by the host specification (for example, the value string of a `<textarea>` in HTML). These offsets are updated automatically as that string changes.
277+
- `startOffset` and `endOffset`: Non negative integers that index into the element's relevant value (for example, the value of a `<textarea>` in HTML), using the same UTF-16 code unit indices as `selectionStart`/`selectionEnd`. These offsets are updated automatically as the content changes.
253278
- `collapsed`: Returns whether `startOffset` equals `endOffset`.
254-
- `startContainer` and `endContainer`: Return `null`. Container details are managed by the host specification, not stored by `OpaqueRange`.
279+
- `startContainer` and `endContainer`: Return `null`. Internally, `OpaqueRange` does store real start and end container nodes — set to the element's **opaque range internal container** when created — but these are hidden from authors because the `startContainer`/`endContainer` getters return null when the range's `is opaque` flag is true. This design ensures the internal DOM tree is never exposed while still enabling the browser to compute geometry.
255280

256281
#### Available Methods
257-
- `getBoundingClientRect()`: Returns the bounding rectangle of the rendered portion of the opaque string represented by the range, as defined by the embedding specification (e.g. the visible text inside a `<textarea>`).
258-
- `getClientRects()`: Returns a list of rectangles for the rendered portion of the opaque string represented by the range.
282+
- `getClientRects()`: Returns a list of rectangles for the rendered portion of the range.
283+
- `getBoundingClientRect()`: Returns a single rectangle that is the union of the rectangles from `getClientRects()`.
259284

260285
#### Unavailable Methods
261286
The following methods are not available on `OpaqueRange` in order to avoid exposing or mutating inner browser implementation details:
@@ -269,15 +294,13 @@ The following methods are not available on `OpaqueRange` in order to avoid expos
269294
- `cloneContents()`
270295
- `cloneRange()`
271296

272-
`OpaqueRange` does not expose the underlying opaque string directly (e.g. it does not provide a `toString()` method). Access to the host‑defined text is only available through host APIs.
297+
`OpaqueRange` does not expose the underlying text directly (e.g. it does not provide a `toString()` method). Access to the text is only available through host APIs (e.g. `element.value`).
273298

274299
Additional methods can be later introduced progressively based on developer feedback and how host specifications use `OpaqueRange`.
275300

276301
`OpaqueRange` is a separate type from `Range`, but it extends `AbstractRange` and can therefore be passed to any API that accepts `AbstractRange`, such as the [Custom Highlight API](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API).
277302

278-
`OpaqueRange` instances are not constructed or configured directly by authors. Instead, they are obtained from element methods defined by host specifications (for example, `textarea.getValueRange(start, end)` in HTML), which create and update `OpaqueRange` objects according to the rules defined in those host specifications.
279-
280-
The following sample code showcases how the new `OpaqueRange` interface would solve the main use cases laid out in the [User-Facing Problem](#user-facing-problem) section. Here, `getValueRange()` is an example method defined by HTML; how ranges are allocated is up to host specifications.
303+
The following sample code showcases how the new `OpaqueRange` interface would solve the main use cases laid out in the [User-Facing Problem](#user-facing-problem) section.
281304

282305
```html
283306
<form id="messageForm" onsubmit="return handleSubmit(event)">
@@ -327,9 +350,17 @@ textarea.addEventListener('input', (e) => {
327350

328351
This implementation simplifies obtaining the caret's position inside `<input>` and `<textarea>` elements. It also allows web developers to use the Highlight API directly on those elements. The `OpaqueRange` interface eliminates the need for cloning elements and copying styles, improving performance while maintaining the benefits of using native form controls, such as accessibility, built-in form validation, and consistent behavior across browsers.
329352

330-
A plausible host API is `getValueRange(start, end)` on `<textarea>` and on text-supporting `<input>` types, such as `text`, `search`, `tel`, `url`, and `password`, using the same offset units as `selectionStart`/`selectionEnd`.
353+
The `getValueRange(start, end)` method is defined on elements that [support opaque ranges](#supports-opaque-ranges). It uses the same UTF-16 code unit offset units as `selectionStart`/`selectionEnd`.
331354

332-
Following this same alignment with selection APIs, `startOffset` and `endOffset` are indices into `element.value`, matching the units used by [`selectionStart`](https://html.spec.whatwg.org/#the-textarea-element:dom-textarea/input-selectionstart) and [`selectionEnd`](https://html.spec.whatwg.org/#the-textarea-element:dom-textarea/input-selectionend).
355+
`getValueRange()` has the following behavior:
356+
1. If the element is an `<input>` and `getValueRange()` does not apply to it (per the input type applicability table), throw a `"NotSupportedError"` `DOMException`.
357+
2. Let _length_ be the length of the element's relevant value.
358+
3. If _start_ is greater than _length_, throw an `"IndexSizeError"` `DOMException`.
359+
4. If _end_ is greater than _length_, throw an `"IndexSizeError"` `DOMException`.
360+
5. If _start_ is greater than _end_, set _end_ to _start_ (collapse the range to _start_).
361+
6. Create a new `OpaqueRange` with start container and end container set to the element's **opaque range internal container**, start offset _start_, and end offset _end_.
362+
7. Append the range to the element's **set of associated OpaqueRanges**.
363+
8. Return the range.
333364

334365
Sample code for `<input type="text">`:
335366

@@ -377,32 +408,25 @@ input.addEventListener('input', (e) => {
377408
});
378409
```
379410

380-
## Live Range Examples
411+
### Supports Opaque Ranges
381412

382-
The examples below demonstrate how `OpaqueRange` updates in real time as the text content of the control changes, without requiring manual offset adjustments.
413+
An `Element` supports opaque ranges if its specification defines that it does. The following HTML elements currently support opaque ranges:
414+
- `<textarea>`
415+
- `<input>` with type: `text`, `search`, `tel`, `url`, or `password`
383416

384-
---
417+
Each element that supports opaque ranges has:
418+
- An **opaque range internal container** — the internal node representing the element's relevant value text.
419+
- A **set of associated OpaqueRanges** — a set of `OpaqueRange` objects, initially empty.
385420

386-
### Example 1: Popup Follows Caret Position
387-
```js
388-
<textarea id="messageArea"></textarea>
389-
<div id="popup">Popup</div>
390-
const textarea = document.querySelector("#messageArea");
391-
const popup = document.querySelector("#popup");
421+
When an element is removed from the document, its set of associated OpaqueRanges is cleared. When an `<input>` element's type changes from a selectable type to a non-selectable type, all associated OpaqueRanges have their `startOffset` and `endOffset` set to 0.
392422

393-
// Create a live, collapsed range at the caret.
394-
const range = textarea.getValueRange(textarea.selectionStart, textarea.selectionStart);
423+
When the underlying content changes, the browser automatically adjusts the offsets of all associated OpaqueRanges. For incremental edits (such as user typing or `setRangeText()`), offsets shift to reflect inserted or deleted characters. For wholesale value changes (such as setting the `value` property or changing the `type` attribute), offsets are reset.
395424

396-
textarea.addEventListener("input", () => {
397-
// Position popup under caret.
398-
const rect = range.getBoundingClientRect();
399-
popup.style.left = `${rect.left}px`;
400-
popup.style.top = `${rect.bottom}px`;
401-
});
402-
```
403-
As the user types, the popup stays positioned under the caret without manually recalculating offsets.
425+
## Live Range Example
404426

405-
### Example 2: Highlight Follows Word Through Edits
427+
The example below demonstrates how `OpaqueRange` updates in real time as the text content of the control changes, without requiring manual offset adjustments.
428+
429+
### Highlight Follows Word Through Edits
406430
```js
407431
<textarea id="messageArea">hello world</textarea>
408432
const textarea = document.querySelector("#messageArea");
@@ -525,7 +549,7 @@ However, this design had two limitations:
525549
2. **Limited extensibility**
526550
Because the design encoded form-control-specific concepts into the API surface, extending it to additional environments would have required redefining or duplicating similar ideas elsewhere.
527551

528-
`OpaqueRange` resolves these issues by providing a host-extensible abstraction. Host specifications (such as HTML) define their own opaque strings and create or update `OpaqueRange` instances accordingly, without the interface being tied to form controls or their internal mechanisms.
552+
`OpaqueRange` resolves these issues by providing a host-extensible abstraction. Host specifications (such as HTML) define which elements [support opaque ranges](#supports-opaque-ranges), create or update `OpaqueRange` instances accordingly, and specify the internal container nodes used — without the interface being tied to form controls or their internal mechanisms.
529553

530554
## Other Considerations
531555

@@ -537,10 +561,6 @@ However, this design had two limitations:
537561

538562
- There are no anticipated security concerns.
539563

540-
### Consistency
541-
542-
`OpaqueRange` is always returned by the platform from `getValueRange()` on `<textarea>` and `<input>`. Authors do not construct or set up `OpaqueRange` directly.
543-
544564
### Compatibility
545565

546566
The `OpaqueRange` interface is currently compatible with any API that utilizes `AbstractRange` objects, such as the [Custom Highlight API](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API). However, this means that `OpaqueRange` is not compatible with methods and APIs that expect a regular `Range`.
@@ -557,7 +577,7 @@ The resulting `AbstractRange` inheritance structure would look like this:
557577

558578
## Potential Future Work
559579
### Extending to Custom Elements
560-
It has been [discussed](https://github.com/whatwg/html/issues/11478#issuecomment-3113360213) that custom elements and other host specifications could also use this API to expose encapsulated ranges, enabling richer editing or selection behaviors while maintaining internal structure.
580+
As [discussed at TPAC 2025](https://www.w3.org/2025/11/11-whatwg-minutes.html) and in the [WHATWG tracking issue](https://github.com/whatwg/html/issues/11478#issuecomment-2472789362), the `OpaqueRange` design is intentionally host-extensible so that custom elements and other host specifications could also use this API to expose encapsulated ranges, enabling richer editing or selection behaviors while maintaining internal structure. The DOM spec's `supports opaque ranges` concept is defined generically, allowing future specifications to designate additional element types.
561581

562582
### Relationship to CSS Anchor Positioning
563583
As noted in the [W3C TAG early design review](https://github.com/w3ctag/design-reviews/issues/1142), some of the positioning use cases addressed by `OpaqueRange` (such as anchoring popups or highlights to caret positions) could also be explored declaratively through future extensions to [CSS Anchor Positioning](https://drafts.csswg.org/css-anchor-position/).
@@ -567,12 +587,10 @@ While `OpaqueRange` focuses on providing a programmatic mechanism aligned with e
567587
## Open Questions
568588
#### How should `OpaqueRange` behave when callers provide reversed offsets (i.e. `startOffset > endOffset`)?
569589

570-
Consider the following ideas:
590+
The current spec text collapses the range to `startOffset` (i.e. sets `endOffset` to `startOffset`), as specified in step 5 of the `getValueRange()` algorithm. We are open to feedback on this choice. All options considered:
571591
- Throw `IndexSizeError`.
572-
- Convert to a collapsed range (by clipping or reordering endpoints).
573-
- Which direction should the collapse target?
574-
- Collapse to `max(startOffset, endOffset)` (matches DOM `Range`).
575-
- Collapse to `min(startOffset, endOffset)`.
592+
- Collapse to `max(startOffset, endOffset)` (matches DOM `Range` behavior).
593+
- Collapse to `min(startOffset, endOffset)`.
576594
- Preserve a backwards range (allow `startOffset > endOffset`).
577595

578596
## References & acknowledgements

0 commit comments

Comments
 (0)