Styling
A terminal cell holds one character, a foreground color, a background color, and a handful of attributes. Every CSS property either resolves to one of those or has no effect. This guide is the mapping. The compatibility matrix lists every property with a yes or a no; this is the why and the how.
Units #
1px and 1ch are one cell. Which axis is meant comes from the
property: width: 10px is ten columns and height: 10px is ten rows.
em, rem, and ex scale from font-size, which is 1 by default,
so 1em is one cell too. Percentages, vw, vh, and calc() work. A
length that lands between cells resolves to a whole cell.
font-size and font-family have no effect. A terminal draws one font
at one size.
Colors #
color and background-color take named colors, hex in three, four,
six, or eight digits, and rgb() and hsl() in both the comma form and
the modern space-separated form with / alpha.
A cell has no alpha. A color with alpha 0 paints nothing, and any
other alpha paints the color as if opaque, except in a gradient, where
stops composite over the color beneath them.
The system colors mean what they mean in a terminal:
| Color | Meaning |
|---|---|
Canvas | The terminal's own background. A box with background-color: Canvas clears to it, which is how a dialog or popover covers what is behind it. |
CanvasText | The terminal's own foreground. |
Highlight, HighlightText | Inverse video. The pair is what ::selection uses. |
SelectedItem, SelectedItemText | Inverse video too. |
LinkText | Blue, the color of <a href>. |
GrayText | Gray. |
A background-image with a linear-gradient() paints each cell the
color the gradient has at that cell's center. radial-gradient() and
url() have no effect. opacity, filter, box-shadow, and
mix-blend-mode have no effect either.
Color depth #
Terminals differ in how many colors they can show. The colorDepth
option says which TermDOM draws with:
colorDepth | Colors | How a CSS color is drawn |
|---|---|---|
"rgb" | Any color | Exactly |
"256" | 256 | As the closest of the 256 |
"ansi" | 8 basic colors | As the closest basic color |
Without the option, TermDOM asks the terminal when attach() runs. It
uses "rgb" if the terminal says it has 24-bit color, or if it is tmux,
which converts 24-bit color for the terminal it runs in. Otherwise it
uses "256", which every terminal in use supports. It does not guess,
because a terminal without 24-bit color can show a 24-bit color as
something else, such as hidden text.
Some terminals have 24-bit color but do not say so, among them
Alacritty, Konsole and VS Code's terminal. Most of them set the
COLORTERM environment variable to truecolor. TermDOM does not read
the environment, but a program can, and pass what it finds:
const truecolor =
process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit";
const term = new TermDOM({
transport: transportFromProcess(process, {
colorDepth: truecolor ? "rgb" : undefined,
}),
});Left undefined, the option falls back to asking the terminal.
A page can style for each depth with media queries: color-gamut
matches "rgb", and (color-index: 256) matches "256".
.banner { background: navy; }
@media (color-gamut: srgb) {
.banner { background: linear-gradient(to right, #1e3a8a, #9333ea); }
}
@media (color-index: 256) {
.banner { background: #5f00af; }
}Text attributes #
| Property | Terminal attribute |
|---|---|
font-weight at 600 or more, bold, bolder | Bold |
font-weight at 300 or less, lighter | Dim |
font-style: italic | Italic |
text-decoration-line: underline | Underline |
text-decoration-line: overline | Overline, on a terminal that draws one |
text-decoration-line: line-through | Strikethrough |
text-decoration-style: double | Double underline, on a terminal that draws one; a single underline elsewhere |
text-transform | uppercase, lowercase, and capitalize change the characters at paint time |
text-decoration-color, text-decoration-thickness, letter-spacing,
word-spacing, and line-height have no effect. A line is one row.
Terminals differ in how they draw italic and dim, and some draw neither. A design that must read the same everywhere uses color.
Borders #
border takes one cell per side and draws with box-drawing characters.
The style picks the glyph set:
| Style | Glyphs |
|---|---|
solid, and groove, ridge, inset, outset | โ โ โ โ โ โ |
double | โ โ โ โ โ โ |
dashed | โ โ |
dotted | โ โ |
hidden, none | Nothing |
The three shaded styles draw as solid because a terminal has one
weight of line. Any border-radius above zero rounds the corners to
โญ โฎ โฐ โฏ. A width above 1px is still one cell.
Borders that meet in a cell join. A rule that reaches a box's side ends
in โ or โค, a column divider crossing it makes โฌ or โด, and two
crossing rules make โผ. Table borders collapse this way by default,
and so do a fieldset and its legend.
outline draws in the ring a border would occupy when the box has a
border, recoloring it. A box without a border gets an overline along its
top row and an underline along its bottom, so the outline takes no
space. outline-offset has no effect.
visibility: hidden leaves the box's space and paints nothing in it.
Generated content #
::before and ::after with a string content work; counter() in
content is not implemented. ::marker styles a list item's bullet.
list-style-type draws โข, โฆ, and โช for disc, circle, and
square, and the counting styles, decimal, lower-roman,
upper-alpha, and the rest, draw a number followed by a dot. An
<ol start> and an <li value> set the count as in a browser.
Pseudo-classes and pseudo-elements #
:hover matches the element under the mouse, and asking for it turns
on mouse motion reporting for as long as a rule needs it. :focus
and :focus-visible match the focused element. :fullscreen,
:popover-open, :modal, :checked, :disabled, :placeholder-shown,
:dir(), and the tree-structural pseudo-classes all match as in a
browser.
::selection styles selected text, and ::highlight() styles the
ranges in CSS.highlights. ::placeholder styles a text control's
placeholder. ::backdrop styles what a modal dialog or popover covers.
::part() reaches into the built-in controls, which are shadow trees;
the API guide lists the parts each has.
Default looks #
The user-agent stylesheet gives HTML elements their terminal looks. Any of them can be overridden by an ordinary rule.
| Element | Default |
|---|---|
b, strong, th, legend | Bold |
i, em, cite, dfn, var | Italic |
u | Underline |
s | Strikethrough |
kbd | Bold and underlined |
small | Dim |
code | A dark background |
a[href] | LinkText and underlined; inverse when focused |
hr | A horizontal rule, one row |
pre | white-space: pre |
fieldset, textarea, dialog, [popover] | A solid border with 0 1ch padding |
td, th | A solid border with 0 1ch padding, collapsed with the table's |
button | The label between [ and ] |
summary | โธ closed and โพ open |
progress, meter | A 10ch bar, with a groove part and a bar part |
input, select | Inline, white-space: pre |
input[type=checkbox], input[type=radio] | 3ch wide, [x] or (x) |
dialog:modal, [popover] | Centered by position: fixed and auto margins, on a Canvas background |
math | Inline; display="block" centers it |
Headings have no default size or weight, because there is no size. Style them:
h1 { font-weight: bold; text-decoration: underline; }
h2 { font-weight: bold; }A focused element that does not edit text shows a solid outline. A focused text field or editing host shows the terminal cursor at its caret instead.
What terminals disagree on #
Some attributes depend on the terminal, and the engine finds out which by asking it rather than by its name:
- Overline is drawn only where the terminal confirms it. MathML uses it for the bar of a root when it can, and an underlined row above the radicand otherwise.
- A double underline is sent so that a terminal that does not know the style still draws a single one.
- Emoji sequences are measured by grapheme cluster when the terminal says it does the same, and by code point otherwise, so a flag or a skin-toned hand is the same width to both.
- Right-to-left text is reordered by the engine unless the terminal says it reorders itself.