CSS Basics & Selectors
Each component carries its styles next to its markup, and those styles apply only to that component's own template — class names cannot collide across components. This page covers where styles live, which selectors match, and how values work in a terminal.
Declaring Styles
Styles are written using standard CSS rulesets inside a <style> block in your component's template string:
<div>
<span class="header">App Title</span>
</div>
<style>
.header {
color: aqua;
font-weight: bold;
}
</style>A component may declare more than one <style> block. They are concatenated in the order they appear, so a later block can override an earlier one exactly as two rulesets in a single block would — when neither selector is more specific, the one written later wins.
Supported Selectors
The CSS parser supports a wide range of standard selectors and combinators:
Tag Selectors: Targets specific tags (e.g.,
div { margin: 1; }).Class Selectors: Targets class names (e.g.,
.card { padding: 1; }).ID Selectors: Targets unique identifiers (e.g.,
#submit-btn { background-color: green; }).Compound Selectors: Combine tags, classes, and IDs simultaneously (e.g.,
div.card#active { border-color: red; }).Pseudo-classes: Targets interactive states (
:hover,:focus,:active,:disabled,:checked,:read-only) and scrollbars.:focus-withinmatches the focused element and every element containing it, so.panel:focus-withinlights up the panel holding the focused field, even when the focus is inside another component such as an<input>.:focus-visiblematches a focused element unless the mouse gave it the focus, as in a browser:button:focus-visible { outline: solid; }draws a ring when Tab reaches the button but not when it is clicked, until the keyboard is used again.<input>and<textarea>read the keyboard, so theirs always shows. The focus states example shows both, withcurrentColorborders.Structural Pseudo-classes:
:first-child: Matches the first element among its siblings.:last-child: Matches the last element among its siblings.:nth-child(even)/:nth-child(odd): Matches even or odd siblings.:nth-child(N): Matches the 1-based N-th sibling (e.g.,:nth-child(3)).:nth-child(An+B): Matches siblingA x k + Bfor every wholek >= 0—3nis every third sibling,2n+1the odd ones,n+3everything from the third on, and-n+2the first two. Whitespace is allowed around the parts and thenis case-insensitive, so2N + 1parses.:nth-last-child(...): The same arguments, counted from the last sibling backwards.:only-child: Matches an element that is its parent's only child.:empty: Matches an element with no content between its tags.:first-of-type/:last-of-type/:only-of-type/:nth-of-type(...)/:nth-last-of-type(...): The same as the-childfamily, but counting only siblings with the same tag.:not(<selector-list>): Matches an element that none of the listed selectors do, e.g.:not(.a, .b). Each entry is one compound selector — a tag,.class,#id,[attribute], a pseudo-class, or a combination such as:not(div.card)or:not(.item:first-child). Every part of an entry must match for that entry to exclude an element, so:not(.x:first-child)still matches a.xthat is not first. Combinators inside the negation (:not(div span)) are not supported and match nothing, as do an empty:not()and a list with a stray or trailing comma.:not()may nest, up to 32 levels; deeper than that matches nothing.:is(<selector-list>)/:where(<selector-list>): Matches an element that any of the listed selectors does, sobutton:is(.primary, .danger)isbutton.primary, button.dangerwritten once. Entries are compound selectors, as in:not(); a combinator inside one, an empty list or a stray comma matches nothing.:is()weighs as much as the entry that matched, and:where()adds nothing to the specificity, which makes it the way to write defaults any other rule overrides::where(.card) { color: gray; }loses even todiv { color: white; }. See the selector lists example.:has(<relative-selector-list>): Matches an element that has another element in a given relation to it, e.g..card:has(.error)for a card holding an error anywhere inside it. Each entry starts with an optional combinator, saying where to look: none for any descendant,>for a child (.list:has(> li)),+for the next sibling (h1:has(+ p)),~for any later sibling. It may then chain compounds with combinators of its own, as in.form:has(> .row input:focus), and they may carry pseudo-classes, so.form:has(input:focus)follows focus inside the form.:has()nests inside:not()(.card:not(:has(.error))). An empty or malformed entry matches nothing. Like combinators,:has()only looks at the component's own elements, including those it projects into another component's slot, and never at another component's internals.
<style>blocks and text do not count as siblings, so they never displace a:first-childnor stop an element being an:only-child. AnAn+Boffset must carry its own sign, so2n3is not2n+3; it matches nothing, as does any other argument that is not one of the forms above.A pseudo-class outside the list above matches nothing, and one bad token disqualifies the whole selector: a misspelled
span:hovrstyles no span rather than every span. Rules that quietly stop applying are easier to spot than rules that quietly apply everywhere.:emptydeparts from CSS in one way: CSS counts any text node as content, which would mean nothing is ever empty in a template where tags sit indented on their own lines. Whitespace-only text is ignored here, so<div></div>and a<div>spanning two lines with nothing between them both match.Combinators:
- Descendant combinator (space): Matches nested elements (e.g.,
div spantargets anyspaninside adiv). - Child combinator (
>): Matches direct children (e.g.,div > spantargetsspanelements immediately nested underdiv). - Adjacent Sibling combinator (
+): Matches immediate following sibling (e.g.,div + ptargets apthat is placed right after adiv). - General Sibling combinator (
~): Matches any following sibling (e.g.,div ~ ptargets anypthat shares the same parent and follows adiv).
Combinators see the component's own template, just as a rule's target is always one of its own elements.
.card .titlematches when this component wrote both the.cardand the.title, even with other components in between: a<Panel>holding the.titlein its slot is transparent. But it never matches across a component boundary. A rule in a child component cannot test a class an outer component put on an ancestor, and no rule can see a nested component's internals. To style a child from its context, pass the context in: as a custom property, which inherits through every component, or as a prop the child turns into a class of its own.- Descendant combinator (space): Matches nested elements (e.g.,
The Special self Selector
To target the component's root outer boundary tag itself (rather than one of its child elements), use the self selector keyword:
self {
display: block;
border: round;
border-color: yellow;
}This is essential for wrapping custom components in custom borders or configuring their layout growth constraints in flex containers.
Generated Content: ::before and ::after
::before and ::after add a box inside an element, before or after its content, holding the text of the content property. They suit decoration that belongs to the style rather than the template: bullets, icons, markers that follow a state.
.item::before { content: "• "; color: rgb(100, 116, 139); }
.open::before { content: "▼ "; }
.closed::before { content: "▶ "; }
a::after { content: " (" attr(href) ")"; }content takes quoted strings and attr(name) (the element's attribute, empty when it has none), concatenated. CSS escapes work inside strings, so "\25B6" is ▶. none or normal removes the box. Without a content declaration there is no box, whatever else the rule says, and content in a rule that is not for ::before or ::after is reported as a diagnostic.
The box is inline by default and takes any other property: display: block gives it its own line, and colors and decorations not set on it are inherited from the element. The pseudo-element ends the selector and may follow pseudo-classes, as in .row:first-child::before or .item:focus::before; one followed by anything else, like ::before:hover, matches nothing. The one-colon :before spelling also works. Transitions and animations do not apply to these boxes, and they are not part of the DOM, so other selectors never match them.
Selected Text: ::selection
::selection colors the text a mouse drag selects, which is otherwise shown in reverse video. It takes color and background-color (or background); anything else is reported as a diagnostic. An element's descendants inherit its ::selection colors, so one rule on a container covers all its text.
.log::selection { color: #ffffff; background-color: #2563eb; }See Selecting and Copying Text.
Styling a Nested Component's Internals: ::part()
The opening line of this page said styles apply "only to that component's own template" — that's true even for a component you instantiate yourself. <textarea linenumbers="true">'s line-number gutter is built out of <div>s inside textarea's own template; a .gutter { color: ... } rule in your component's stylesheet simply never reaches them, no matter how directly you wrote the <textarea> tag.
A component opts specific internal elements into being styled from outside by marking them with a part attribute (space-separated for more than one, like class):
<!-- inside some component's own template -->
<div class="row" part="gutter active">...</div>An outside component then targets that name with ::part(name), following whatever selector matches the instantiation site (tag, class, id — not the part element itself):
textarea::part(gutter) { color: rgb(100, 116, 139); }
.editor::part(active) { color: rgb(129, 140, 248); }This reaches through as many layers of nesting as it takes to get there — internal markup inside the component, and even further component boundaries in between. If <Foo>'s own template instantiates <Bar part="baz">, an app that only ever writes <Foo class="thing"> can still reach baz directly with .thing::part(baz), with no need for Foo to forward or re-expose it itself (there's no exportparts-style ceremony here, unlike standard CSS shadow DOM).
Built-in components that expose parts document them alongside their other attributes — see the <textarea> guide for the line-number gutter and current-line highlight parts.
The Cascade
When more than one rule sets the same property on an element, the winner is the one with the highest specificity — the same rule CSS uses. Specificity counts three things across the whole selector, ancestors included:
| Weight | Counts | Example |
|---|---|---|
| highest | id selectors | #sidebar |
| middle | classes, attributes, pseudo-classes | .card, [open], :hover |
| lowest | element names | div |
A single id outranks any number of classes, and a single class outranks any number of element names:
#panel { color: red; } /* wins */
.a.b.c { color: blue; }
section div { color: green; }Because the count runs over the entire selector, rules that look similar still rank correctly:
.a.b { color: red; } /* wins over .a — two classes beat one */
div.a { color: red; } /* wins over .a — same classes, one more type */
.wrap .a { color: red; } /* wins over .a — same classes, plus an ancestor */* contributes nothing, and self selects the component's own root rather than an element name, so neither adds weight.
Between two rules of equal specificity, the later one wins:
.a { color: red; }
.b { color: blue; } /* an element with class="a b" is blue */The style attribute is applied after every selector, so it always wins, and !important overrides even that — see !important below.
Inline Styles
The style attribute applies declarations to a single element, taking precedence over rules from <style> blocks:
<div style="padding: 1; color: red;">Highlighted</div>Values in a Terminal
Lengths are measured in character cells — padding: 1 is one cell, and because cells are roughly twice as tall as they are wide, one vertical cell looks about as large as two horizontal ones. Percentages resolve against the parent, and lengths accept arithmetic:
.sidebar { width: 25%; }
.content { width: calc(100% - 20); }
.panel { width: min(100%, 60); height: clamp(5, 50%, 20); }Custom Properties
Variables declared with --name inherit down the tree and are read with var(--name) or var(--name, fallback) — the usual way to define a theme in one place:
self { --accent: rgb(59, 130, 246); }
.card { border-color: var(--accent); }
.card-title { color: var(--accent); }!important
Appending !important to a declaration makes it win over normal declarations from later rules and over inline styles, as in standard CSS. Reach for it rarely; more specific selectors usually express intent better.
Rules with a pseudo-class (:hover, :focus, :first-child, …) take part in the same cascade: #save outranks .button:hover, and an !important declaration outranks a normal one from a :focus rule.
inherit, initial and unset
Any property accepts the CSS-wide keywords inherit, initial and unset (and revert, treated like unset). When one of them wins the cascade for a property, every declaration of that property on the element is dropped, so the property keeps the value it starts from: inherited from the parent for inherited properties such as color, the initial value for the others such as width or padding-left.
.card { color: #3b82f6; width: 30; }
.card.plain { color: inherit; width: initial; } /* parent's color, auto width */That is exactly unset. inherit and initial match it in the common cases, with two differences from browser CSS: initial on an inherited property still inherits, and inherit on a non-inherited property gives the initial value rather than the parent's. A keyword on a longhand does not undo a shorthand (margin: 1; margin-top: initial keeps the top margin).
The CSS property reference lists every supported property with its accepted values.

