Finding and Modifying Elements
Find elements, modify content and attributes, and update the document from C++.
On this page
You can query and modify elements using JavaScript's DOM API directly from C++, with the same call names and meaning. The examples name the View's dom::Document as document (see About the DOM API), so the code reads like page script.
Finding Elements
You can query elements by ID, CSS selector, or relationship to another element.
auto status = document.getElementById("status");
auto title = document.querySelector(".panel h1");
auto panel = title.closest(".panel");
for (auto item : document.querySelectorAll("#log li"))
item.classList.add("seen");
When a query finds nothing, it returns an empty handle— calls on it do nothing (see DOM Handles and Errors).
Changing Content
You can update or read an element's text or HTML markup by assigning to its properties.
status.textContent = "Connected";
status.innerHTML = "<b>Connected</b>";
std::string text = status.textContent;
Assigning to textContent replaces the element's children with text.
Assigning to innerHTML replaces them with parsed markup (scripts in the markup never run).
👍 Store Reads in std::string
Read string properties into an explicit
std::string. The property is a proxy, soautodoesn't compile.
🚧 Pass Untrusted Text to textContent
Pass text from users or the network to
textContent, neverinnerHTML. Markup assigned toinnerHTMLkeeps event attributes likeonclick, which run when JavaScript is enabled.
🚧 Reading innerText Triggers Layout
Reading
innerTextforces a synchronous style and layout pass when the page has pending changes. PrefertextContentwhen you don't need the rendered text, and make all changes before reading.
Classes and Attributes
You can modify an element's classes, attributes, and custom dataset properties.
auto link = document.querySelector("a.help");
link.classList.add("highlight");
link.classList.toggle("visited");
link.setAttribute("href", "https://ultralig.ht");
link.dataset["topic"] = "setup"; // sets data-topic="setup"
if (auto target = link.getAttribute("target"))
Log(*target);
getAttribute() returns a std::optional— it is std::nullopt when the attribute is missing.
Setting Styles
You can set an element's styles by assigning to properties on style using their camelCase JavaScript names.
auto bar = document.getElementById("health-bar");
bar.style.width = "50%";
bar.style.opacity = 0.5;
bar.style.left = dom::StyleValue::Px(x);
String literals are checked at compile time— a mistyped number (eg, "50pxx") causes a compile error, while a string built at runtime that fails to parse is ignored and keeps the old value.
For values you update every frame, use unit helpers like dom::StyleValue::Px() to avoid formatting strings. The renderer still validates the value against the property's CSS grammar— an invalid assignment is ignored and keeps the old value.
To read computed styles and work with units, see Reading and Writing Styles. To measure elements and control scrolling, see Element Geometry and Scrolling.
Adding and Removing Elements
You can create new elements, insert them into the document, and move or remove existing nodes.
auto log = document.getElementById("log");
auto entry = document.createElement("li");
entry.textContent = "Saved";
log.appendChild(entry);
entry.remove(); // out of the page, still valid
log.appendChild(entry); // back in
auto copy = entry.cloneNode(true);
log.insertBefore(copy, entry);
Calling remove() takes an element out of the page— the handle stays valid, so you can insert it back in later.
Calling appendChild() on an element already in the document moves it from its existing position.
Inserting Nodes and Text
You can insert nodes and text at specific positions relative to an element.
| Call | Where It Inserts |
|---|---|
append(...) |
Inside the element, after its last child |
prepend(...) |
Inside the element, before its first child |
before(...) |
Just before the element (as a sibling) |
after(...) |
Just after the element (as a sibling) |
replaceWith(...) |
In place of the element |
Each call takes any mix of elements and strings— strings are inserted as text (never parsed as markup).
auto entry = document.createElement("li");
entry.append(icon, " Saved at ", time_text);
list.prepend(entry);
entry.after(divider);
old_entry.replaceWith(entry);
Building UI from Your Data
To populate a list from a C++ vector, build the rows off the page in a document fragment and pass the fragment to replaceChildren()— it swaps out the old rows in one call.
void ShowScores(dom::Document document, const std::vector<Score>& scores) {
auto rows = document.createDocumentFragment();
for (const Score& score : scores) {
auto row = document.createElement("li");
row.append(score.name, ": ", std::to_string(score.points));
rows.appendChild(row);
}
document.getElementById("scores").replaceChildren(rows);
}
📘 Fragments Batch Tree Changes
Using a document fragment adds every child node in a single tree mutation, so a
MutationObserverreceives only one record. Appending elements individually would not run layout per row either— layout always waits for the next layout read or rendering update.
Differences from JavaScript
While the C++ DOM API mirrors JavaScript, a few types and behaviors differ.
| JavaScript | C++ |
|---|---|
Read-only properties (el.parentElement) |
Calls (el.parentElement()) |
getAttribute() returns null when missing |
Returns std::nullopt |
el.dataset.userId |
el.dataset["userId"] |
el.children is a live collection |
children() and querySelectorAll() return snapshots (query again after changes) |
| Failing calls throw (bad selector or invalid tree change) | Returns an empty value and logs the error. Pass dom::Checked for the reason (see DOM Handles and Errors). |
To react to clicks on the rows, see Handling DOM Events.