CSS¶
Every selector, property, and value form Lumen's CSS subset accepts. For the task-level introduction see Styling; for the markup surface see Tags and attributes.
Lumen implements a subset of CSS Selectors 4, Cascade 5, and Media Queries 5, plus a few properties of its own for parts that standard CSS cannot address. Anything outside the subset is reported as a warning and the declaration is dropped; the rest of the stylesheet still applies.
Grammar¶
stylesheet := at_import* (at_media | rule)*
at_import := "@import" '"' path '"' ";"
at_media := "@media" media_query "{" rule* "}"
rule := selector_list "{" declaration* "}"
selector_list := selector ("," selector)*
selector := compound (combinator compound)*
combinator := " " | ">" | "+" | "~"
compound := tag? ("." class | "#" id | ":" pseudo_class)*
declaration := ident ":" value "!important"? ";"
/* ... */ comments are stripped anywhere. !important is matched
case-insensitively at the end of a value.
@import "other.css"; splices another stylesheet in place. Paths are
relative to the importing file. Imports must come before any rule; one
written later is an error, as is a cycle. They nest. Imported rules are
placed before the importing file's own rules, so at equal specificity the
importing file wins.
@media blocks nest up to 32 levels, as do :is(), :where(), and
:not() arguments.
@import and @media are the at-rules Lumen implements. @keyframes and
@font-face are kept as authored and handed to the web target, which writes
them back out; the desktop ignores both. A url() inside a kept block names
a file relative to the app directory, the same place an <image src> names
one from, and a web build copies that file into the site and points the block
at the copy. Any other at-rule is skipped with a warning along with its whole
block, and the rest of the stylesheet applies.
Pseudo-elements (::before and friends) are a parse error, not a warning.
Selectors¶
Simple selectors¶
| Form | Matches |
|---|---|
tag |
Elements with that tag name. |
.class |
Elements listing that class. |
#id |
The element with that id. |
* |
Any element. |
A compound writes the tag first: button.primary#save.
Combinators¶
| Combinator | Meaning |
|---|---|
A B |
B with an A ancestor. |
A > B |
B whose immediate parent is A. |
A + B |
B immediately preceded by a sibling A. |
A ~ B |
B preceded anywhere by a sibling A. |
Each step takes the nearest candidate that matches it and does not
reconsider: .a .b .c binds .b to the closest matching ancestor, and
.a ~ .b ~ .c binds .b to the closest matching earlier sibling. A
selector that would only match through a farther candidate misses.
Sibling steps resolve when the stylesheet is applied to the document.
Restyles that run against one element on its own (a <for> row, a class
change from a script, an OS theme flip) walk ancestors only, so a rule
that needs a sibling step is not reapplied there.
Pseudo-classes¶
State pseudo-classes select a routing slot rather than filtering elements: a rule written with one always applies, and the runtime swaps the value in when the element enters that state.
| Pseudo-class | Matches |
|---|---|
:hover |
Pointer over the element. |
:focus |
Element has keyboard focus. |
:focus-visible |
Focus arrived from the keyboard. |
:active |
Element is pressed. |
:disabled |
Element is disabled. |
:checked |
<toggle>, <switch>, or <checkbox> is on. |
:selected |
Tab strip button of the active tab. |
:drag-over |
An acceptable in-app drag hovers this drop target. |
Structural pseudo-classes filter on where the element sits in the tree:
| Pseudo-class | Matches |
|---|---|
:root |
The document root, or any element whose tag is root. |
:first-child |
First element child of its parent. |
:last-child |
Last element child. |
:only-child |
The sole child. |
:empty |
No element children and no text. |
:nth-child(an+b) |
1-based position. odd, even, n, -n, and plain integers are accepted. |
:is(list) |
Any selector in the list. |
:where(list) |
Any selector in the list, contributing zero specificity. |
:not(list) |
No selector in the list. |
Sibling position is read from the live tree, so a restyle that runs
against one element on its own still selects the same elements a
whole-document pass would: after a theme flip, a class change from a
script, or a <for> rebuild, .row:last-child is still only the last
row.
:is(), :where(), and :not() take a comma-separated list of
selectors, each of which may use combinators. The element being tested is
the subject of every argument, so .row:not(.list > .row) matches a
.row that is not a child of a .list.
Specificity¶
Specificity is the triple (a, b, c) compared lexicographically:
acounts#idselectors.bcounts classes and pseudo-classes.ccounts tag names.
:is() and :not() take the specificity of their most specific
argument. :where() contributes nothing.
Cascade¶
Declarations are ordered by, in decreasing priority:
- Origin. The built-in skin sheets are user-agent origin; your CSS is author origin and wins, whatever the specificity.
- Importance, per declaration.
!importantapplies to the one declaration it is written on, not to its siblings in the same rule. - Specificity.
- Source order, then position within a selector list. The last matching declaration wins.
Markup attributes beat CSS, for every property both surfaces can set.
<tile width="50px"/> keeps its width against .t { width: 100px }.
flex-direction is the one property CSS wins: <row>, <column>,
<scroll>, and the other layout tags carry a direction that comes from
the tag rather than from anything you wrote, so a stylesheet can change
it. <overlay> and <dialog> take position: absolute and a full
inset from their tag too, but those two count as markup and a
stylesheet cannot move them.
One exception keeps skins from repainting your surfaces: when you set a
resting bg (in CSS or as an attribute), a skin's :hover and :active
background for that element is dropped. :checked, :selected,
:disabled, and :drag-over backgrounds from the skin still stand until
you name that state yourself.
Inheritance¶
These properties inherit from parent to child: text-color (color),
selection-color, selection-text-color, caret-color, font-size,
font-family, font-weight, line-height, text-align, wrap, and
max-lines. Custom properties inherit through their own scope. Every
other property applies only where it matches.
An inherited value acts as a zero-specificity base, so any matching rule or markup attribute on the child overrides it.
Error recovery¶
A declaration with an unknown property, an unparseable value, or an
unresolvable var() is dropped and reported as a warning naming the
selector and the property. The rest of the stylesheet applies. Duplicate
warnings are reported once.
A declaration under a state pseudo-class that is not one of the state-routable properties below is accepted and has no effect.
Custom properties¶
Any declaration whose name starts with -- is a custom property. It is
stored, not applied, and inherits to descendants.
:root {
--accent: #33c7ce;
--panel: #0c2c4a;
}
.card {
bg: var(--panel);
border: 1px solid var(--accent);
}
var(--name) substitutes the value; var(--name, fallback) uses the
fallback when the name is unset. References resolve recursively up to 16
levels; a cycle or an unknown name with no fallback drops the
declaration with a warning.
Custom properties cascade like other declarations, including
!important, and a rule matched on an ancestor puts its values in scope
for descendants. That makes :root.theme-dark { --panel: ... } a working
theme switch.
The runtime reads two custom properties directly: --lumen-tooltip-delay
(milliseconds) and --lumen-tooltip-offset (pixels), because <tooltip>
collapses at parse time and no selector can reach it. The built-in skins
define the --lumen-* token set they theme themselves with; redeclaring
those names in your own :root block retints the skin.
@media¶
Features are joined with and. Comma-separated query lists, or, and
not are not supported.
| Feature | Values | Matches when |
|---|---|---|
prefers-color-scheme |
dark, light, no-preference |
The OS color scheme equals the value. With no known scheme, only no-preference matches. |
prefers-reduced-motion |
reduce, no-preference |
Always no-preference; see below. |
prefers-contrast |
more, less, custom, no-preference |
Always no-preference; see below. |
min-width |
length in px | The viewport is at least that wide. |
max-width |
length in px | The viewport is at most that wide. |
width |
length in px | The viewport is that wide. |
Lumen reads the OS color scheme and the window size. It does not read the
motion or contrast preference from the OS yet, so prefers-reduced-motion
and prefers-contrast only ever match no-preference. Give people an
in-app switch for anything heavily animated or low-contrast.
Value forms¶
| Form | Accepted |
|---|---|
| length | auto, <n>, <n>px, <n>%. A bare number is pixels. |
| number | <n>, <n>px. The px suffix is accepted and stripped. |
| color | #rrggbb, #rrggbbaa. |
| edges | 1 to 4 terms in top-right-bottom-left rotation; each <n>, <n>px, or <n>%. |
| border width | thin (1px), medium (3px), thick (5px), or a px length. Percentages are rejected. |
| duration | <n>ms or <n>s. |
| easing | linear, ease, ease-in, ease-out, ease-in-out, cubic-bezier(a, b, c, d). |
| track size | <n>, <n>px, <n>fr, auto, min-content, max-content, minmax(<min>, <max>). |
Gradients¶
The bg property accepts three gradient functions. Each stop is a colour
with an optional position, given as a percentage or a 0..1 number. Stops
without a position are distributed evenly, and all stops are sorted by
position.
.a { bg: linear-gradient(90deg, #101014, #202028 60%, #303040); }
.b { bg: radial-gradient(#ffffff, #000000); }
.c { bg: conic-gradient(from 45deg, #ff0000, #00ff00, #0000ff); }
linear-gradient([<angle>deg,] <stop>, <stop>, ...). The angle defaults to180deg, top to bottom. At least two stops.radial-gradient(<stop>, <stop>, ...). The centre is fixed at the middle of the box and the radius covers it. At least two stops.conic-gradient([from <angle>[deg],] <stop>, <stop>, ...). The start angle defaults to0. At least two stops.
Shadows¶
inset draws the shadow inside the box. Multiple shadows are comma
separated and paint in order. shadow is an accepted synonym.
Property catalogue¶
The Lumen short names and the standard CSS names below are
interchangeable: color for text-color, background and
background-color for bg, border-radius for radius, flex-grow
for grow, flex-shrink for shrink, justify-content for justify,
object-fit for fit, and white-space for wrap.
Layout¶
| Property | Values | Default |
|---|---|---|
display |
flex, grid, none |
flex |
width, height |
length | content size |
min-width, min-height, max-width, max-height |
length | auto (see below) |
aspect-ratio |
number | none |
padding, margin |
edges | 0 |
inset |
edges | auto |
position |
relative, absolute |
relative |
box-sizing |
border-box, content-box |
border-box |
flex-direction |
row, column, row-reverse, column-reverse |
from the tag |
flex-wrap |
nowrap, wrap, wrap-reverse |
nowrap |
flex |
none, auto, initial, or <grow> [<shrink>] [<basis>] |
|
grow, flex-grow |
number | 0 |
shrink, flex-shrink |
number | 1, or 0 on a scroll container's direct child |
flex-basis |
length | auto |
gap |
<both> or <row> <column>; each may be a percentage |
0 |
row-gap, column-gap |
number or percentage | 0 |
align, align-items |
start/flex-start, end/flex-end, center, stretch, baseline |
stretch |
align-self |
same as align-items |
from the container |
align-content |
start/flex-start, end/flex-end, center, stretch/normal, space-between, space-around, space-evenly |
stretch |
justify, justify-content |
start, end, center, between/space-between, around/space-around, evenly/space-evenly |
start |
justify-items |
same as align-items |
grid only |
justify-self |
same as align-items |
from justify-items |
z-index |
integer or auto |
document order (auto is 0) |
overflow, overflow-x, overflow-y |
visible, hidden, scroll |
visible |
layout-boundary |
true, yes |
automatic |
overflow: scroll makes an element a live scroll container. overflow:
hidden cuts its descendants off at the element's box without making it
scrollable; inside a scroll container that clip travels with the element as
the content scrolls.
An element's own text lays out as an item of its own, sized to the words,
so justify-content places it along the main axis the same way it places
a child: justify-content: center on a button centres its label. It
applies on a horizontal main axis only, and an authored text-align wins
where both are set.
Minimum sizes and shrinking¶
Items on a flex line that does not fit shrink, in proportion to their
size, until they reach their minimum size. Left at the default auto,
that minimum is the smaller of the element's own content and its
authored width / height: a box with nothing in it shrinks to
nothing, a box holding a 250px child stops at 250px, and a box whose
child is wider than its authored width still stops at that width.
Two properties change it:
min-width: 0/min-height: 0drops the floor entirely, so the element shrinks past its content and the content overflows. This is what a long unbreakable label or a nested scroll pane usually wants.shrink: 0takes the element off the shrinking list, so it keeps its authored size and the line overflows instead.
overflow: hidden and overflow: scroll also drop the automatic
floor to zero, since the element clips its own content.
Positioning¶
Every element is a containing block. position: absolute takes an
element out of the flow and resolves its inset against its parent's
padding box, whichever element that parent happens to be; there is no
position: static and no search up the tree for a positioned ancestor.
So the wrapper you want an element pinned inside is the element you
write it under, and no position: relative is needed on that wrapper.
An out-of-flow child contributes nothing to its parent's content size.
A parent that is content-sized on an axis and has only absolutely
positioned children therefore measures zero on that axis, and a child
with inset: 0 fills that zero. Nothing appears:
Size the parent to fix it: give it a height (or width on the
horizontal axis), grow: 1 so it takes the free space on its own line,
or an inset of its own if it is absolutely positioned too. One in-flow
child works as well, since the parent then measures that child. With
overflow: hidden on the parent, as above, the collapsed subtree is
clipped away entirely rather than spilling into view, so the symptom is
a blank window.
lumenc lint reports this shape as collapsed_containing_block and
names the child it found.
Logical properties¶
Each takes a px number and resolves against the element's writing
direction, which comes from dir in the markup or, with none authored,
from the app's locale.
padding-inline-start,padding-inline-end,padding-block-start,padding-block-endmargin-inline-start,margin-inline-end,margin-block-start,margin-block-endinset-inline-start,inset-inline-end,inset-block-start,inset-block-endborder-inline-start-width,border-inline-end-width,border-block-start-width,border-block-end-width
Grid¶
| Property | Values | Default |
|---|---|---|
grid-template-rows, grid-template-columns |
whitespace-separated track sizes | none |
grid-row, grid-column |
<start> or <start> / <end> |
auto placement |
Track lists take plain track sizes. repeat(), named lines, and
span are not supported. Line numbers outside the range of a 16-bit
integer are clamped with a warning.
Visuals¶
| Property | Values | Default |
|---|---|---|
bg |
color or gradient | transparent |
radius |
1 to 4 numbers, top-left / top-right / bottom-right / bottom-left | 0 |
border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius |
number | from radius |
opacity |
number, clamped to 0..1 | 1 |
shadow, box-shadow |
shadow list | none |
fit, object-fit |
fill, cover, contain, none, scale-down |
fill; <image> only |
disabled-opacity |
number, clamped to 0..1 | 0.5 |
knob-color |
color | built-in knob fill |
knob-inset |
number | 4 |
thumb-size |
number | 16 |
popup-gap |
number | 4 |
draggable |
true, yes |
false |
disabled-opacity sets how much a disabled element dims when neither
:disabled { bg } nor :disabled { opacity } was authored.
knob-color, knob-inset, thumb-size, and popup-gap reach widget
parts that have no selector of their own.
Borders and outlines¶
| Property | Values | Default |
|---|---|---|
border |
<width> \|\| <style> \|\| <color> in any order, or none |
none |
border-width |
1 to 4 border widths | medium when a style is set |
border-style |
none, hidden, solid |
none |
border-color |
color | the element's text color, else black |
border-top-color, border-right-color, border-bottom-color, border-left-color |
color | from border-color |
border-top, border-right, border-bottom, border-left |
per-side shorthand | none |
border-top-width, border-right-width, border-bottom-width, border-left-width |
border width | 0 |
focus-outline |
<width> <#color> |
none |
outline-offset |
number | 0 |
hover-border, focus-border |
border shorthand | none |
solid and none are the supported border styles; dashed, dotted,
and the rest are rejected with a warning. A shorthand that omits the
style but names a width or a colour resolves to solid. Without a style
there is no border: the computed width is zero and nothing paints.
Borders participate in layout under box-sizing.
Text¶
| Property | Values | Default | Inherited |
|---|---|---|---|
text-color |
color | the built-in text color | yes |
font-size |
number | 16 |
yes |
font-family |
family list | platform sans-serif | yes |
font-weight |
normal (400), bold (700), 1..1000 |
400 |
yes |
line-height |
number (multiplier) or <n>px |
runtime ratio | yes |
text-align |
start/left, center, end/right |
start |
yes |
wrap |
none/nowrap, word/normal, glyph/char |
none |
yes |
max-lines |
non-negative integer | unlimited | yes |
text-overflow |
clip, ellipsis |
clip |
no |
The shaper resolves the first available family in a font-family list,
honouring the generic keywords, and falls back to the platform
sans-serif. lighter and bolder are rejected: they need the parent's
computed weight.
Any font-weight from 1 to 1000 is accepted. A variable font renders the
weight you author wherever its weight axis covers the value; a family
built from separate faces renders the nearest weight it ships, chosen the
way CSS chooses it (from 400-500 look up to 500 first, then down; below
400 look down first; above 500 look up first). So font-weight: 650
against a family with a regular and a bold face draws the bold face, and
measures as that face.
text-overflow: ellipsis elides overflowing single-line text unless you
also author wrap and max-lines, in which case your multi-line clamp
is ellipsized instead.
Text input¶
These apply to <input> and <textarea>.
| Property | Values | Default |
|---|---|---|
selection-color |
color | text fill at reduced alpha |
selection-text-color |
color | selected glyphs keep their fill |
caret-color |
color | the text fill |
caret-width |
number | 2 |
caret-blink |
duration | 530ms |
password-character |
exactly one character | a bullet |
The caret blink phase is shared by the whole app, so caret-blink sets
one period for every input; it is applied when styles are reapplied, not
at startup.
Interaction states¶
State-routable properties, and where each state's value lands:
| Property | :hover / :focus / :focus-visible |
:active |
:checked |
:selected |
:disabled |
:drag-over |
|---|---|---|---|---|---|---|
bg |
yes, one shared slot | yes | yes | yes | yes | yes |
text-color |
separate slot per state | yes | no | no | yes | yes |
opacity |
separate slot per state | yes | no | no | yes | yes |
shadow, box-shadow |
separate slot per state | yes | no | no | yes | yes |
border |
yes; :focus wins over :hover |
no | no | no | no | no |
outline |
:focus and :focus-visible only |
no | no | no | no | no |
outline-offset |
:focus and :focus-visible only |
no | no | no | no | no |
bg under :hover, :focus, and :focus-visible shares one slot, so
the three cannot differ. outline under :focus-visible gets its own
slot and wins over :focus while focus came from the keyboard.
On a <toggle> or <switch> track, :disabled { bg } wins over
:checked { bg }, so a disabled control that happens to be on shows the
disabled fill rather than the checked one.
The same swaps are reachable as plain properties, which is often shorter:
| Property | Equivalent |
|---|---|
hover-bg |
:hover { bg } |
press-bg |
:active { bg } |
hover-border |
:hover { border } |
focus-border |
:focus { border } |
focus-outline |
:focus { outline } |
Progress¶
| Property | Values | Default |
|---|---|---|
progress-duration |
integer milliseconds, greater than zero | 1200 |
progress-chunk |
number 0..1 | 0.3 |
progress-duration is the indeterminate sweep period and
progress-chunk the fraction of the track the moving chunk covers.
Transitions¶
| Property | Values | Default |
|---|---|---|
transition |
<property> <duration> [<easing>], comma separated |
none |
transition-property |
comma-separated property names, all, or none |
none |
transition-duration |
comma-separated durations | 0 |
transition-timing-function |
comma-separated easings | ease |
Animatable properties: opacity, background-color (background,
bg), color (text-color), and border-color. all stands for all
four. Any other name in a transition list is ignored with a warning;
layout properties are deliberately excluded so a transition never re-runs
layout every frame.
An entry that names no easing gets ease. A second duration in an entry
is a delay, which Lumen warns about and ignores: a transition always
starts on the tick the value changes. transition-delay is warned about
and ignored for the same reason.
The transition shorthand resets the longhands. Otherwise
transition-property defines the list and the duration and easing lists
cycle over it; a duration list without a property list produces nothing.
Animations¶
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: spin 2s linear infinite;
}
| Property | Values | Default |
|---|---|---|
animation |
the CSS shorthand, comma separated | none |
animation-name |
comma-separated @keyframes names |
none |
animation-duration |
comma-separated durations | none |
animation-timing-function |
comma-separated easings | none |
animation-delay |
comma-separated durations | none |
animation-iteration-count |
comma-separated counts or infinite |
none |
animation-direction |
comma-separated directions | none |
animation-fill-mode |
comma-separated fill modes | none |
animation-play-state |
running or paused |
none |
These reach the web target and do nothing on the desktop, which has no
keyframe engine; transition is what animates a desktop app. Values are
written out as authored, so the browser reads the standard CSS meaning of
each one.
A @keyframes block is browser CSS, not Lumen's dialect: its body is
carried through untouched, so a keyframe declares background,
transform and opacity rather than bg. A block written inside
@media is emitted inside the same query.
Scrollbars¶
| Property | Values | Default |
|---|---|---|
scrollbar-color |
auto, or <thumb> [<track>] colors |
auto |
scrollbar-width |
auto, thin, none |
auto |
scrollbar-thickness |
number | 8 |
scrollbar-thickness-thin |
number | 4 |
scrollbar-margin |
number | 2 |
scrollbar-min-thumb |
number | 24 |
scrollbar-track-hover |
color | no track tint |
scrollbar-hover-boost |
number | 1.6 |
scrollbar-fade-delay |
duration | 1s |
scrollbar-fade-duration |
duration | 250ms |
scrollbar-color and scrollbar-width are the standard CSS properties;
the rest are Lumen's own geometry and timing controls for the overlay
scrollbar.
Scrolling and focus¶
| Property | Values | Default |
|---|---|---|
scroll |
x, y, both |
none |
sensitivity |
number | 1.0 |
inertia |
number | 0.4 |
tab-index |
integer | from the tag |
Web target mapping¶
Built for the web, an app ships its stylesheet as real CSS and the browser runs the cascade. Most of what you write arrives unchanged; this section is the rest.
Selectors¶
A tag selector becomes the class the element carries in the document,
inside :where(): button is emitted as :where(.lm-button). The
wrapper keeps the tag weighing nothing, so it still loses to a class the
way it does everywhere else.
The four states no browser selector covers are mirrored onto attributes:
| Lumen | Emitted as |
|---|---|
:selected |
[data-lm-selected] |
:checked |
:is(:checked, [data-lm-checked]) |
:disabled |
:is(:disabled, [data-lm-disabled]) |
:drag-over |
[data-lm-drag-over] |
:checked and :disabled keep the browser's own state beside the
mirror, because a <checkbox> is a real checkbox there and a <toggle>
is a button. Every other selector, :hover and the structural
pseudo-classes included, is emitted as written.
Rules are written out in the order Lumen's cascade puts them in, so where the browser cannot tell two rules apart it picks the one Lumen picks.
Property names¶
The Lumen short names are emitted under their standard spelling:
| Lumen | Emitted as |
|---|---|
bg |
background |
text-color |
color |
radius |
border-radius |
grow |
flex-grow |
justify |
justify-content |
fit |
object-fit |
shadow |
box-shadow |
align |
align-items |
wrap |
white-space and overflow-wrap |
scroll |
overflow-x, overflow-y |
max-lines |
the -webkit-line-clamp block |
justify: between, around, and evenly are emitted as
space-between, space-around, and space-evenly.
States written as properties¶
A property that names a state becomes a rule of its own, written beside the rule it came from:
| Property | Emitted as |
|---|---|
hover-bg |
:hover { background } |
press-bg |
:active { background } |
hover-border |
:hover { border } |
focus-border |
:focus { border } |
focus-outline |
:focus { outline } |
selection-color |
::selection { background } |
selection-text-color |
::selection { color } |
Lengths¶
A bare number is pixels in Lumen and nothing at all in CSS, so
padding: 8 16 is emitted as padding: 8px 16px.
A length that reaches a property through var() is var(--gap) whatever
the token holds, so the unit goes on the definition instead: a custom
property the stylesheet uses only in places where a bare number means
pixels is emitted with px on it, and --gap: 8 becomes --gap: 8px.
Writing the unit yourself is still clearer, and a token that already has
one is left alone.
A token used both where a number means pixels and where it means a plain
number cannot be both, so it is emitted as authored and lumenc web
names it in a warning. Split it into two tokens, or write the unit on the
one that needs it.
Gradient stop positions travel as written; write them as percentages.
Properties with no browser equivalent¶
The knobs that reach widget parts no selector can address are emitted as
custom properties, --lm- plus the property name: knob-color becomes
--lm-knob-color. The browser ignores them and Lumen reads them back off
the element, so they keep working, and you can read one yourself with
var(--lm-knob-color).
This covers knob-color, knob-inset, thumb-size, popup-gap,
caret-width, caret-blink, password-character, disabled-opacity,
progress-duration, progress-chunk, sensitivity, inertia, and the
scrollbar-* geometry and timing properties. The ones measured in pixels
carry px like any other length. scrollbar-color and scrollbar-width
are standard CSS and are emitted as themselves; a scrollbar-color
naming only a thumb gains transparent for the track, which CSS
requires.
knob-color, knob-inset and thumb-size are also what the emitted
stylesheet draws a control's parts from. A <toggle>, a <switch> and a
<slider> are one element in a page and a track plus a knob on the
desktop, so the knob is drawn as a pseudo-element from those three
values, and a <checkbox>, a <radio> and a <progress> are drawn from
the state the runtime marks them with. Anything an app's own CSS says
about the control still wins.
What is not emitted¶
| Property | Why |
|---|---|
tab-index |
The document already carries it as tabindex. |
draggable |
The document already carries it as draggable. |
transition-delay |
Lumen ignores it: a transition starts on the tick the value changes. |
layout-boundary |
A hint to Lumen's layout engine; the browser lays the page out itself. |
Selector matching differs in one way worth knowing. Lumen's matcher takes the nearest candidate for each step and does not reconsider, and a browser's does; a selector that relies on that difference matches more elements on the web than it does on the desktop.