Querying Elements
Sessel is built on CSS selectors. If you know CSS, you already know most of what you need. This guide covers how to use selectors to find elements, extract data from them, and control which documents are searched.
What is a selector?
The core building block of Sessel is the selector literal: any CSS selector wrapped in ${}.
${div.item}
${h1}
${[itemprop="price"]}
${#main .content p}
Any valid CSS selector works inside ${}. Sessel also adds four content-aware pseudo-classes — :contains(), :equals(), :greater-than(), and :less-than() — but standard CSS selectors are what you'll use most of the time.
A selector always returns a list, even when only one element matches. The list may be empty if nothing matches. You cannot use a selector result directly as a single value — you always need to pull an element out of the list first.
Extracting data from elements
Once you have a list, .first() gives you the first element. From there, three methods let you read its content:
.text()— the text content of the element (all descendant text nodes joined, no HTML markup).value()— the microdata value, following WHATWG rules (for most elements this is text content, but<a>returns itshref,<img>itssrc,<time>itsdatetime, and so on).attr(name)— the value of a named attribute
The pattern is always: selector → .first() → data method.
${h1}.first().text()
Get the text of the first h1.
${[itemprop="price"]}.first().value()
Get the microdata value of the first element with itemprop="price". For a <data> or <meter> element this is the value attribute; for a plain <span> it is the text content.
${a.nav}.first().attr("href")
Get the href attribute of the first .nav anchor.
If the list is empty, .first() returns null, and calling .text(), .value(), or .attr() on null also returns null. The ?? null-coalescing operator is useful here:
${h1}.first().text() ?? "Untitled"
Expressions inside selectors
Selector literals can embed Sessel expressions in place of CSS literal values. Inside ${...}, any unquoted attribute value or pseudo-class argument is treated as a Sessel expression:
let status = "active";
${[itemprop="status"]:equals(status)} from self
Here status in the attribute selector and in :equals() both refer to the let-bound variable. Quoted values remain plain CSS literals as usual:
${[itemprop="status"]:equals("active")} from self
This works with arbitrary expressions, including method chains and sub-selects:
let category = ${[itemprop="category"]} from "/config";
${div[data-cat=category.first().text()]} from self
The Selector type
If you need to work with a selector as data — storing it in a variable, passing it to a function, or building it dynamically — use the Selector type:
let sel = new Selector { "h1" };
sel.execute() // same as ${h1} from self
sel.execute("/about.html") // same as ${h1} from "/about.html"
sel.execute(["/a/*", "/b/*"]) // same as ${h1} from "/a/*", "/b/*"
A Selector is inert until you call .execute(). You can also retrieve the underlying CSS string with .toString():
let sel = new Selector { "[itemprop=\"price\"]" };
sel.toString() // "[itemprop=\"price\"]"
This is useful when the selector is built dynamically or passed in as a variable.
The from clause
By default, a selector queries across the entire site — every document in the store. The from clause narrows that scope.
${selector} from scope
The most common scopes:
from value |
What it searches |
|---|---|
from self |
The current document only |
from "/products/shoes" |
One specific document |
from "/products/*" |
All documents matching a glob pattern |
from expr |
Documents at the path(s) the expression resolves to |
${div.item} from self
Search only the current document. This is the most common form — you'll write from self far more often than a global query.
${[itemprop="price"]} from "/products/shoes"
Search a specific document by path.
${[itemprop="price"]} from "/products/*"
Search all documents whose path matches the glob. This runs across multiple documents and returns a combined list.
${[itemprop="related"]} from ${a.nav}.first().attr("href")
Use another expression to determine the target path dynamically.
Block-level from
When several selectors need the same scope, a block-level from avoids repetition:
from self {
${h1}.first().text() == "Welcome" &&
${p}.count() > 0
}
Every bare selector inside the braces inherits the scope. A selector with its own from clause overrides the block scope. See Syntax for full details.
self and prior
Two context variables are always available: self and prior.
self resolves to the root element of the current document. You have already seen it used in from self. It is also a valid expression on its own — it evaluates to the document's root element.
prior resolves to the pre-mutation state of the document — what it looked like before the current write operation was applied. If the document is being created for the first time, prior is null. Always check before using it:
prior != null
A common pattern is to read a field from the previous version of the document:
(${[itemprop="status"]} from prior).first().value()
This is particularly useful in mutation handlers and constraints that need to compare old and new values.
Element provenance
Every element returned by a selector knows where it came from. Three methods let you inspect this:
.path()— the document path the element was found in.document()— the document element (root) of the source document.selector()— the CSS selector that matched this element
These are especially useful when querying across multiple documents with a glob from clause, so you can tell which document each result came from. See the Element reference for full details.
Typed search with Class objects
When your data is governed by schemas, there is a higher-level alternative to raw CSS selectors: the .search() method on a Class object. Instead of hand-crafting an [itemtype] selector, you ask the schema class to find its instances:
<script type="text/sessel">
@schema UserConfig url("https://example.com/UserConfig");
// Find all UserConfig instances across the entire store
UserConfig.search()
// Find only those with org-id "abc"
UserConfig.search({ "org-id": "abc" })
</script>
.search() returns a list of Instance values — the same typed objects you get from new UserConfig { ... }. Each instance supports property access (.theme, .org-id), method calls, and isa checks.
The criteria argument is a dictionary of property-name/value pairs. All criteria must match (logical AND). When no criteria are provided, every instance of the type is returned.
OR criteria
When you need to match instances that satisfy any of several conditions, pass a list of dictionaries instead of a single dictionary:
<script type="text/sessel">
@schema HostConfig url("https://pagelove.org/HostConfig");
// Find hosts where hostname OR alias matches "foo.com"
HostConfig.search([{ hostname: "foo.com" }, { alias: "foo.com" }])
</script>
Each dictionary in the list is an independent AND filter. An instance appears in the results if it matches any of the dictionaries. A single-element list behaves identically to passing the dictionary directly. An empty list matches all instances.
Polymorphic search with isa
When a schema has descendant types (schemas that declare it as their parent), you can search for instances of the base type and all its descendants in a single query by passing { "isa": true } as the second argument:
<script type="text/sessel">
@schema HostConfig url("https://pagelove.org/HostConfig");
// Strict: only HostConfig instances
HostConfig.search({})
// Polymorphic: HostConfig AND all descendant types
HostConfig.search({}, { "isa": true })
// OR criteria + polymorphic
HostConfig.search([{ hostname: "foo.com" }, { alias: "foo.com" }], { "isa": true })
</script>
Each result carries its actual schema type, so a list returned by a polymorphic search may contain instances of different types. This is useful when you have a base type and want to query across the entire type hierarchy without knowing which subtypes exist.
Note that "isa" must be quoted — isa is a reserved keyword in Sessel.
This is the recommended approach when you are working with schema-typed data. It is more readable than building [itemtype="..."] selectors by hand, and the results come back as typed Instances rather than raw Elements.
If you need polymorphic type matching in a raw CSS selector (e.g. in an HTTP Range header, a resource binding, or a ${} selector literal), use the :isa() pseudo-class:
<script type="text/sessel">
// CSS-level polymorphic matching — equivalent to the search above
${:isa('https://pagelove.org/HostConfig')}
</script>
See Selector Extensions for full :isa() reference.
Next steps
You now know how to find elements, read their content, scope queries to specific documents, and use prior to look at the previous state of a document.
The next guide covers what you can do with a list of elements — counting, filtering, mapping, and aggregating:
For detailed reference material: