styles

domonic supports browser-style inline CSS through the style attribute and a CSSOM-like CSSStyleDeclaration surface.

Styling Elements

Style properties are converted from JavaScript-style camelCase to CSS property names when the element renders.

from domonic.html import div

mytag = div("hi", _id="test")
mytag.style.backgroundColor = "black"
mytag.style.fontSize = "12px"
print(mytag)
# <div id="test" style="background-color:black;font-size:12px;">hi</div>

CSS Custom Properties

Use setProperty when the CSS name is not a Python identifier.

from domonic.html import div

panel = div("Dashboard")
panel.style.setProperty("--accent", "#0ea5e9")
panel.style.setProperty("border-inline-start", "4px solid var(--accent)")

print(panel)

Read and Remove Styles

from domonic.html import div

box = div("Status")
box.style.display = "grid"
box.style.gap = "0.5rem"

print(box.style.getPropertyValue("display"))
box.style.removeProperty("gap")
print(box)

Style Generated HTML

from domonic.html import article, h2, p

card = article(h2("Release notes"), p("DOM, Web API, and parser updates."))
card.style.maxWidth = "42rem"
card.style.padding = "1rem"
card.style.border = "1px solid #ddd"

print(card)

Shorthand and longhand

Shorthand properties expand to their longhands and reconstruct from them, the way a browser’s CSSOM does.

from domonic.html import div

box = div()
box.style.border = "1px solid red"
print(box.style.getPropertyValue("border-width"))   # 1px
print(box.style.getPropertyValue("border-color"))   # red

box.style.setProperty("margin-top", "10px")
box.style.setProperty("margin-bottom", "10px")
box.style.setProperty("margin-left", "10px")
box.style.setProperty("margin-right", "5px")
print(box.style.getPropertyValue("margin"))         # 10px 10px 10px 5px
print(box.style.cssText)                            # margin: 10px 10px 10px 5px;

getComputedStyle

window.getComputedStyle(element) (see window) returns a read-only declaration resolved through a light cascade: matching author rules from document.styleSheets (by specificity, then source order, with !important on top), then the inline style attribute, then inherited values from the parent, then each property’s initial value.

from domonic import domonic
from domonic.window import window

page = domonic.parseString(
    "<html><head><style>.card{color:red;padding:8px}"
    "#hero{font-weight:bold}</style></head>"
    "<body><div id='hero' class='card' style='color:blue'>hi</div></body>"
)
hero = page.querySelector("#hero")
computed = window.getComputedStyle(hero)
print(computed.getPropertyValue("color"))        # rgb(0, 0, 255)  (inline wins)
print(computed.getPropertyValue("padding-top"))  # 8px   (.card, shorthand expanded)
print(computed.getPropertyValue("font-weight"))  # 700   (#hero; keyword -> number)
print(computed.getPropertyValue("display"))      # inline (initial value)

Like a browser, the computed values are used values: colours are reported as rgb() / rgba(), em / rem / pt / cm lengths and calc() are resolved to px (against the element’s font-size and the containing block where one can be found without full layout), currentColor resolves to the element’s color, font-weight keywords become numbers, an out-of-flow display: inline blockifies, and inherit / initial / unset are resolved. % values that need layout, and a transform list, are reported the browser way too:

from domonic.dom import document
from domonic.style import ComputedStyleDeclaration

outer = document.createElement("div")
outer.setAttribute("style", "font-size: 20px; width: 400px")
inner = document.createElement("p")
inner.setAttribute("style", "margin: 1em; width: 50%; transform: rotate(90deg)")
outer.appendChild(inner)
document.createElement("div").appendChild(outer)

c = ComputedStyleDeclaration(inner)
c.getPropertyValue("margin")     # 20px       (1em of the 20px font-size)
c.getPropertyValue("width")      # 200px      (50% of the 400px container)
c.getPropertyValue("transform")  # matrix(0, 1, -1, 0, 0, 0)

A border’s width computes to 0px whenever its style is none/hidden (the default), and min-width/min-height normalise auto to 0px, matching a real browser. A width/height/margin that’s still auto, or a % with no resolvable containing block, is reported once a layout engine attaches real geometry via domonic.layout’s set_layout_box.

Pass a pseudo-element to read its style: window.getComputedStyle(el, "::before").

Reading it repeatedly is cheap: the resolved cascade is cached on the element and reused until something that could change it does – an attribute, a stylesheet rule, a layout box, or the viewport.

The cascade understands modern selector specificity: :where() contributes zero, :is() / :not() / :has() take the specificity of their most specific argument, and @layer order is respected (later layers win, and an unlayered rule beats any layer).

page = domonic.parseString(
    "<html><head><style>"
    "@layer base, theme;"
    "@layer theme { p { color: blue } }"
    "@layer base { p { color: red } }"
    "</style></head><body><p>hi</p></body>"
)
p = page.querySelector("p")
window.getComputedStyle(p).getPropertyValue("color")   # rgb(0, 0, 255)  (theme layer is later)

The layout boundary

getComputedStyle reports used values – it guesses at a containing block to turn % into a px string. A layout engine needs the opposite: lengths resolved, but %, auto, and anything layout-dependent left alone. domonic.layout.layout_style(element) returns that – one typed value per layout property, ready for a layout engine to consume.

from domonic.dom import document
from domonic.layout import layout_style

box = document.createElement("div")
box.setAttribute("style", "display: flex; width: 50%; margin: 10px auto; gap: 8px")

style = layout_style(box)
style.display   # Keyword('flex')
style.width     # Percent(0.5)
style.margin    # Edges(top=Length(10.0), right=AUTO, bottom=Length(10.0), left=AUTO)
style.gap       # Gap(row=Length(8.0), column=Length(8.0))

Once an engine has computed a box for an element, element.set_layout_box(box) (a domonic.layout.LayoutBox) makes getBoundingClientRect, clientWidth/clientHeight, clientTop/clientLeft, and offsetWidth/offsetHeight/offsetLeft/offsetTop report it; with no box attached they keep their existing behaviour.

ch/ex lengths resolve via the CSS-spec 0.5em approximation by default (domonic has no glyph metrics of its own). A renderer with real font metrics can register domonic.style.set_ch_ex_resolver(fn) to resolve them precisely instead.

Domonic has no opinion on legacy HTML presentational attributes (bgcolor, an <img>’s width/height, …), but a renderer that translates them can register domonic.style.set_presentational_hint_resolver(fn) to fold them into the cascade at the correct priority: weaker than any real author rule or inline style for the same property, stronger than the initial value.

CSS custom properties and var()

var() references are substituted when computing a value. Custom properties inherit, so a --token declared on :root (or any ancestor, inline or via a rule) resolves on a descendant.

from domonic.html import div

box = div("x", _style="--pad: 12px; padding: var(--pad)")
window.getComputedStyle(box).getPropertyValue("padding")   # 12px

CSS.registerProperty() registers a custom property with a syntax, an initial value, and inheritance behaviour, matching the CSS Properties and Values API.

from domonic.style import CSS

CSS.registerProperty({
    "name": "--brand",
    "syntax": "<color>",
    "inherits": True,
    "initialValue": "rebeccapurple",
})
# --brand now resolves to rebeccapurple on any element until overridden

CSS Typed OM

The numeric-value core of CSS Typed OM is available: CSSUnitValue / CSSKeywordValue (via CSSStyleValue.parse), the CSS.px() / em() / rem() / percent() / deg() / s() / fr() factories, and element.attributeStyleMap / element.computedStyleMap().

from domonic.style import CSS
from domonic.html import div

CSS.px(10) + CSS.px(5)          # CSSUnitValue 15px
CSS.px(96).to("in")            # CSSUnitValue 1in

el = div(_style="width: 10px")
el.attributeStyleMap.get("width")        # CSSUnitValue 10px
el.attributeStyleMap.set("height", CSS.px(20))

constructable stylesheets

new CSSStyleSheet({media, disabled}) and sheet.replaceSync(cssText) build a stylesheet in code; adding it to document.adoptedStyleSheets (or a shadow root’s) feeds it into getComputedStyle. Setting sheet.disabled = True on any sheet (constructed, <style>, or <link>) removes its rules from the cascade immediately, matching a real browser.

from domonic.dom import Document
from domonic.style import CSSStyleSheet

doc = Document()
sheet = CSSStyleSheet()
sheet.replaceSync(".hl { color: rebeccapurple }")
doc.adoptedStyleSheets = [sheet]

media queries

window.matchMedia(query) evaluates width / height / orientation, the range syntax ((width >= 600px), (400px <= width <= 900px)), resolution (from window.devicePixelRatio), and the discrete preference features (prefers-color-scheme, prefers-reduced-motion, hover, pointer, …). Override a preference for the session via window.mediaFeatures.

stylesheets

document.styleSheets (DOM spelling) and document.stylesheets both work.

from domonic import domonic

page = domonic.parseString(
    "<html><head><style>p { color: green }</style></head><body></body></html>"
)
sheet = page.styleSheets[0]
print(sheet.cssRules[0].selectorText)   # p