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