You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: OpaqueRange/explainer.md
+66-48Lines changed: 66 additions & 48 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -18,9 +18,7 @@
18
18
19
19
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.
20
20
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.
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.
233
231
234
232
### Non-goals
235
233
@@ -239,23 +237,50 @@ Provide a way for host specifications (such as HTML) to obtain an `OpaqueRange`
239
237
240
238
## Proposed Approach
241
239
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>`, text‑supporting `<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>`, textsupporting `<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.
243
241
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.
245
243
246
-
`OpaqueRange`'s live-update behavior also aligns conceptually with the `InputRange()` from [Keith Cirkel’s 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.
247
245
248
246
### Properties and Methods
249
247
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
+
250
273
#### 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
+
251
276
`OpaqueRange` exposes useful endpoint information while maintaining encapsulation:
252
-
-`startOffset` and `endOffset`: Non‑negative 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`: Nonnegative 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.
-`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.
255
280
256
281
#### 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()`.
259
284
260
285
#### Unavailable Methods
261
286
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
269
294
-`cloneContents()`
270
295
-`cloneRange()`
271
296
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`).
273
298
274
299
Additional methods can be later introduced progressively based on developer feedback and how host specifications use `OpaqueRange`.
275
300
276
301
`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).
277
302
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.
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.
329
352
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`.
331
354
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**.
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`
383
416
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.
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.
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.
395
424
396
-
textarea.addEventListener("input", () => {
397
-
// Position popup under caret.
398
-
constrect=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
404
426
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.
@@ -525,7 +549,7 @@ However, this design had two limitations:
525
549
2.**Limited extensibility**
526
550
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.
527
551
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.
529
553
530
554
## Other Considerations
531
555
@@ -537,10 +561,6 @@ However, this design had two limitations:
537
561
538
562
- There are no anticipated security concerns.
539
563
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
-
544
564
### Compatibility
545
565
546
566
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:
557
577
558
578
## Potential Future Work
559
579
### 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.
561
581
562
582
### Relationship to CSS Anchor Positioning
563
583
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
567
587
## Open Questions
568
588
#### How should `OpaqueRange` behave when callers provide reversed offsets (i.e. `startOffset > endOffset`)?
569
589
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:
571
591
- 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)`.
576
594
- Preserve a backwards range (allow `startOffset > endOffset`).
0 commit comments