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.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.
<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).
- 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.
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.
Architectural detail
An !important declaration does not survive a matching pseudo-class rule on the same element: given
.row { color: green !important; }
.row:focus { color: blue; }a focused .row renders blue, where CSS would keep it green. Interactive state is resolved in a second pass layered on top of the resting style, and that pass applies its normal declarations after the first pass has already applied its important ones. Avoid combining !important with pseudo-class rules for the same property.
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.

