中文版见 tycss-reference.md。
This document is the authoritative syntax reference for the TyControls v1 style language (.tycss), written for theme authors and control developers.
Every rule stated here has been checked against the engine implementation (source/tyControls.Css.Lexer.pas, Css.Parser.pas,
Css.Values.pas, StyleModel.pas, Painter.pas). Where other documents disagree, this one wins.
Loading entry points: TTyStyleModel.LoadFromFile(filename) / LoadFromCss(source string).
Syntax errors raise ETyCssError (with line/column numbers); invalid property values (such as an unparseable color) raise at load time;
unknown property names are silently ignored and never reported.
The library carries a built-in base layer, generated from themes/light.tycss, that is always present under the loaded theme. It
is not the theme named default: that one is themes/auto.tycss, which, like every built-in skin, dresses some keys itself and
leaves the rest to the base layer.
How ResolveStyle combines the two depends on where a rule comes from:
- A theme (
ThemeName,ThemeFile,LoadTheme,LoadThemeCss) takes over every typeKey it writes a plain rule for (no class, no state). The built-in layer is then disabled for that key entirely: the theme alone decides everything, and properties it leaves blank do not leak through. A key the theme writes no plain rule for falls back to the built-in layer whole. A complete theme therefore renders exactly as it did before this feature existed, while a partial theme never leaves uncovered controls invisible. - An additive patch (
LoadThemeCssAdditive, the controller'sStyleOverride) takes over no key. It changes only the properties it declares, whether the key comes from the theme or from the built-in layer (§8.1).
Upgrading an existing theme? Read §8.1 first. v3 split the 51 controls that borrowed another control's typeKey into keys of their own. Because override semantics are all-or-nothing per typeKey, any new key your theme never mentions falls back to the built-in light values as a whole key.
.tycss is a deliberately small CSS dialect:
- The top level has exactly two constructs:
:rootvariable blocks and style rules; - Selectors have one shape only:
type [.variant] [:state], with comma-separated lists allowed; - Descendant, child, wildcard, and every other combinator selector are not supported;
- Variables are evaluated lazily at their use sites; both
var(--x)and bare--xwork; - Colors support the
rgb/rgbaconstructors plus thelighten/darken/alpha/mixadjustment functions, nested to any depth.
Here is a minimal but complete theme, usable as-is:
/* my-theme.tycss - a minimal working theme */
:root {
--accent: #3B82F6;
--surface: #FFFFFF;
--on-surface: #1F2937;
--border: #D1D5DB;
--danger: #EF4444;
--radius: 6px;
--focus-ring: var(--accent); /* focus ring color; overridable on its own */
}
TyButton {
background: var(--surface);
color: var(--on-surface);
border-color: var(--border);
border-width: 1px;
border-radius: var(--radius);
padding: 6px;
font-size: 10px;
font-weight: 400;
}
TyButton:hover { background: darken(--surface, 4%); }
TyButton:focus { border-color: var(--accent); outline: 2px var(--focus-ring); }
TyButton:active { background: darken(--surface, 10%); }
TyButton:disabled { opacity: 0.5; }
TyButton.primary { background: var(--accent); color: #FFFFFF; border-color: var(--accent); }
TyButton.primary:hover { background: lighten(--accent, 8%); }
TyButton.primary:active { background: darken(--accent, 8%); }
TyLabel {
background: alpha(#FFFFFF, 0); /* fully transparent */
color: var(--on-surface);
font-size: 10px;
}
TyEdit, TyComboBox {
background: var(--surface);
color: var(--on-surface);
border-color: var(--border);
border-width: 1px;
border-radius: var(--radius);
padding: 4px;
font-size: 10px;
}
TyEdit:focus, TyComboBox:focus { border-color: var(--accent); outline: 2px var(--focus-ring); }
TyPanel {
background: var(--surface);
border-color: var(--border);
border-width: 1px;
border-radius: var(--radius);
padding: 8px;
}For fuller examples see the bundled themes: themes/light.tycss, themes/dark.tycss, themes/builtin/showcase.tycss.
A .tycss file is a sequence of :root blocks and style rules, in any order, freely interleaved:
:root { --a: #fff; }
TyButton { background: var(--a); }
:root { --a: #000; } /* multiple :root blocks are allowed; the later definition of a name wins */Only block comments /* ... */ are supported; they may span lines. // line comments are not supported.
| Token | Form | Notes |
|---|---|---|
| Color literal | #rgb #rrggbb #rrggbbaa |
After # only hex characters are consumed; the length must be 3, 6, or 8, anything else raises at evaluation time |
| Number | 10 0.45 .5 |
Unsigned; decimals allowed. Lexically a number carries no minus sign (see "Negative values" below) |
| Identifier | bold --accent -2px |
First character [a-zA-Z_-], later characters may include digits; - may lead, so --accent and -2px are each a single identifier |
| Function name | var( lighten( url( |
An identifier immediately followed by ( (no space between) opens a function call |
| String | "Noto Sans" 'a' |
Single or double quotes; no escape sequences; cannot contain its own quote character |
| Unit / symbol | px % deg |
px and deg are ordinary identifiers, glued onto a preceding number to form 6px, 90deg; % is a standalone symbol bound to the number before it |
| Punctuation | : ; { } . , ( ) |
Negative values: -2px lexes as a single identifier token, but the length/number evaluation functions parse it correctly,
so negative offsets such as shadow: 0px -2px 4px #0000002E; are legal.
- Every declaration is
property: value;and the semicolon is mandatory, including after the last declaration in a block; - A value runs to the next semicolon or
}; commas and colons inside parentheses do not cut it short; - Type names, variant names, pseudo-class names, property names, and function names all match case-insensitively (by convention, follow the spellings used in this document and the bundled themes);
- If the same property appears twice in one rule block, the later one wins.
:root {
--accent: #3B82F6;
--radius: 6px;
--hover-bg: lighten(--accent, 8%); /* variables may reference other variables */
}- Variable names must start with
--; anything else is a parse error; - A value is stored as raw text and evaluated only at its use site (lazy evaluation). Variables may therefore reference other variables forward or mutually, and may hold color functions, gradient argument fragments, or any other expression;
- Redefining a name overrides the earlier definition (across multiple
:rootblocks too); - Referencing an undefined variable raises when a rule that uses it is evaluated, i.e. when the stylesheet loads.
In color, length, and number expressions the following two spellings are fully equivalent:
TyButton {
background: var(--accent); /* standard CSS spelling */
border-color: --accent; /* bare variable form, equally valid */
border-radius: var(--radius);
padding: --radius;
}Both forms also work inside function arguments; the bundled themes use the bare form heavily for brevity:
TyButton:hover { background: lighten(--accent, 8%); } /* bare --x */
TyButton:focus { border-color: darken(var(--border), 10%); } /* var(--x) */Note: a var(...) form is only usable as a whole value or a whole function argument; there is no string concatenation.
Besides --focus-ring (§5.14), the built-in themes define several other semantic tokens in :root for sub-part typeKeys
and text controls to reference. They are ordinary -- variables and theme authors may override them wholesale:
| Token | Built-in value (light) | Meaning / use |
|---|---|---|
--selection |
alpha(var(--accent), 0.30) |
Text selection highlight band (accent-tinted, semi-transparent); referenced by TyTextSelection, so Edit/Memo selections share their look with selected list rows |
--muted |
alpha(var(--on-surface), 0.5) |
De-emphasized foreground (placeholder / hint text); referenced by TyTextHint |
--overlay-hover |
alpha(var(--on-surface), 0.12) |
Semi-transparent hover overlay (e.g. the rounded backdrop behind a tab's close ×); referenced by TyTabClose |
--input-bg |
var(--surface) |
The well of the field controls: edit, combo box, list box, memo, spin edit, calendar, date picker, tree, list view |
--input-border |
var(--border) |
The frame of the same field controls; separate from --border, so a skin can frame its fields without also recolouring dividers, grid lines and panel frames |
--grid-bg |
var(--surface) |
The data grid's body |
:root {
--selection: alpha(var(--accent), 0.30);
--muted: alpha(var(--on-surface), 0.5);
--overlay-hover: alpha(var(--on-surface), 0.12);
}Changing these tokens restyles what they name globally, with no per-control edits. The controls a skin writes no rule for are drawn by the built-in base layer; a skin that points
--input-bg,--input-border,--grid-bgand the--radiusfamily at its own edit's values gets those controls looking like that edit. Like--accentand--on-surfaceabove they are evaluated lazily at use sites and may reference other variables.
:root also carries tokens that controls read. Most are lengths, for insets, gaps and slot
sizes, such as --listview-text-margin and --treeview-margin; a value may be 6px, 6 or
var(--x). Some take a word or a colour, such as the tree's --treeview-lines: classic and
--treeview-lines-color: #a0a0a0 (§3.5).
For the whole application, set them in the theme or in the controller's StyleOverride:
:root {
--listview-text-margin: 6px;
--treeview-margin: 6px;
}For one control, set them in that control's own StyleOverride; they apply to it alone and
win over the theme:
--treeview-margin: 10px; --treeview-icon-gap: 6px;
The -- declarations in a control's StyleOverride are also variables for the block's other
declarations, e.g. --w: 2px; border-width: var(--w);. A value that does not evaluate (a wrong
unit, an undefined variable) has no effect and the theme's value stands. For a token that takes a
word, a value that is only a reference (var(--x) or --x) reads as what it refers to; one that
refers to nothing leaves the theme's value standing too.
Controls that read their tokens this way so far: TTyListView (and TTyShellListView), for every
length token it reads -- --listview-*, --row-height and --scrollbar-size; TTyTreeView (and
TTyShellTreeView), for --treeview-margin, --treeview-icon-gap and the four tree-line tokens of
§3.5; TTyMemo, for --memo-tab-size; TTyPopover, for its six --popover-* tokens (it is not a
control, but its StyleOverride follows the same rule). Other controls take the theme's values only,
for now -- except for --over-parent-chrome (§3.6), which every control reads.
Versions before 3.0.4 ignore --
declarations in a control's StyleOverride, so a form saved with them still loads there.
Four tokens decide how a tree draws its lines and its expanders. Like those of §3.4, they can go
in the theme or in one tree's own StyleOverride.
| Token | Values | Default | Meaning |
|---|---|---|---|
--treeview-lines |
guides / classic / none |
guides |
guides: a vertical line under each ancestor's expander, through the rows of its descendants, and nothing at the node's own level, so no line meets an expander. classic: the full connectors, including the node's own line down from the row above and across to its content. none: no lines. |
--treeview-lines-style |
solid / dotted / dashed / one or two lengths |
solid |
The dash. dotted is 1px 1px and dashed is 3px 3px. Lengths work as in SVG's stroke-dasharray, dash first, then gap; one length gives a gap as long as the dash, e.g. 6px 3px for a long dash. Logical pixels, scaled with the DPI. |
--treeview-lines-color |
a colour | the tree's border colour | The lines' colour; var() and the colour functions work. |
--treeview-expander |
chevron / plus-minus |
chevron |
plus-minus is a boxed +/- drawn where the connectors meet; classic lines stop at its edges. It is hit exactly like the chevron. |
Of the built-in themes, the classic and xp skins use plus-minus, classic and dotted; the
others keep the defaults.
A dotted or dashed pattern is anchored to the tree's content origin, not restarted for every segment: the verticals of consecutive rows run on as one line, a corner joins on the same pattern, and the pattern moves with the content when it scrolls.
The ShowTreeLines property is still the master switch: False draws no lines whatever the
tokens say. Whether there is an expander at all is still up to ShowButtons.
/* a classic tree throughout the application */
:root {
--treeview-expander: plus-minus;
--treeview-lines: classic;
--treeview-lines-style: dotted;
}To leave the lines off one tree only, put this in its StyleOverride:
--treeview-lines: none;
Before 3.0.4 there were no such tokens, and a tree drew solid full connectors with a chevron.
When a child sits on a container's border, rounded corner or separator line, the container draws those again after the child has painted, so the line is not cut where the child is. These containers do it:
| Container | What is drawn on top |
|---|---|
TTyPanel, and TTyExPanel, TTyPaintPanel, TTyScrollBox, TTyScrollPanel, TTyControlBar, TTyCoolBar, TTyGridPanel, TTyRelativePanel |
border, focus ring, the parent's background outside the rounded corners |
TTyCard |
the same, plus the lines under the title and over the actions |
TTyGroupBox, and TTyRadioGroup, TTyCheckGroup, TTyToolGroupPanel |
border, focus ring, the parent's background outside the rounded corners, plus the caption and the gap it makes in the top border |
TTyPageControl |
the page area's border, focus ring and the parent's background outside its rounded corners (the active page used to cover three of its sides) |
TTyToolBar |
the bottom line |
Only a visible border or focus ring counts: a panel with border-width: 0 and no focus ring
shows no rounded shape, so the square corners of its children are left alone. A scroll bar
along the edge of a scroll box already draws the box's frame over itself, so the box does
not draw it again.
To keep one child out of this, put this in its StyleOverride:
--over-parent-chrome: 1;
Put in :root of a theme or of the controller's StyleOverride, it turns this off for the
whole application. 0, false, none or no value at all leave it on. Code can turn it all
off too: TyChromeOverlayEnabled := False; (unit tyControls.Base, True by default).
Only TyControls' own controls get the frame drawn over them; a native LCL control placed on a frame still covers it. This behaviour starts with 3.0.5.
selector ::= type-name [ "." variant-name ] [ ":" state-name ]
selector-list ::= selector { "," selector }
- Type name: the control's typeKey (see section 8), e.g.
TyButton; - Variant name (optional): any identifier, matching a token in the control's
StyleClassproperty, e.g..primary; at most one variant per selector; - State name (optional): only the five names
hover,active,focus,disabled,selectedare allowed (checkedis an alias ofselectedand parses to the same state); any other pseudo-class name is a parse error. At most one state per selector; chaining such as:selected:disabledis not allowed.
TyButton { ... } /* base style for the type */
TyButton.primary { ... } /* variant */
TyButton:hover { ... } /* state */
TyButton.primary:hover { ... } /* variant + state */
TyEdit:focus, TyComboBox:focus { ... } /* comma list: one declaration group registered under several selectors */All of the following either fail to parse or never match; the engine has no combinator selectors at all:
- Descendant / child selectors:
TyPanel TyButton,TyPanel > TyButton - Wildcard:
* - Multiple variants:
TyButton.primary.large(a selector allows exactly one.variant) - Bare class / bare state selectors:
.primary,:hover(a selector must start with a type name) - Attribute selectors,
!important, and@mediaare all unsupported (@importand@modeare supported at-rules, they are just not selectors;@importmust appear before all:rootblocks and rules)
A control computes its current state set on every repaint:
disabled(controlEnabled = False) is exclusive: a disabled control has only the disabled state; hover / focus / active are all ignored;hover: the mouse is over the control;active: the left mouse button is held down (for CheckBox and the like this still means "pressed", not "checked");focus: the control has keyboard focus. Controls based onTTyGraphicControl(theTTyLabelfamily,TTyProgressBar, the gauge family, and so on) have no focus state; every windowed control that can take focus supports it;selected: a selectable part (item / cell / tab) is in its selected state. It is supplied by the sub-part's resolve site (e.g.TyListViewItem,TyCalendarCell,TyGridCell,TySegmentedItem), not by the control's own momentary state.
Several states can hold at once (hovering over a focused button = hover + focus).
Which states a key supports depends on what its resolve site passes. Section 8 lists the pseudo-classes the built-in theme
actually writes for each key, and calls out keys whose resolve sites pass an empty state set, making their pseudo-class
selectors dead (e.g. TyLinkLabelLink).
A control's final style merges several TTyStyleSet layers in a fixed order; a later layer overrides only the properties
it explicitly declares:
- Type base layer:
Type { }; - Variant layer: each whitespace-separated token in the control's
StyleClass, applyingType.variant { }in the order the tokens appear in theStyleClassstring (StyleClassmay hold several variants, e.g."primary large"); - State layer: for each state currently in effect, processed in the fixed order
selected → hover → focus → active → disabled;
within each state,
Type:state { }applies first, thenType.variant:state { }in variant order. Puttingselectedfirst is deliberate: it is a static resident layer, so hover/active can override its conflicting properties one by one, while properties only selected declares (say, aborder-colorno one else sets) still survive.
It follows that a rule with a state always applies later than (beats) a rule without one, and a variant state rule always applies later than the plain type rule of the same state.
TyButton { background: #FFFFFF; }
TyButton:hover { background: #F0F0F0; } /* state layer */
TyButton.primary { background: var(--accent); } /* variant layer */Hovering over a button whose StyleClass = primary, the merge order is
TyButton → TyButton.primary → TyButton:hover.
The state layer comes after the variant layer, so the #F0F0F0 from TyButton:hover overrides the accent from .primary,
and the primary button washes out on hover. The fix is a matching state rule for the variant, which applies after
TyButton:hover:
TyButton.primary:hover { background: lighten(--accent, 8%); }For each (type, variant, state) combination the engine applies all matching rules in source order, each overriding the
previous per property, exactly like browser CSS. This is also what lets @import / appended loads layer per property:
TyButton { background: #FF0000; color: #111111; }
TyButton { background: #00FF00; } /* overrides background only; color is still #111111 */TestDuplicateRuleLastWins in tests/test.StyleModel.pas guards this behavior.
Even so, avoid defining the same selector twice in one file; when you need parallel declarations, put them in one rule
block so readers do not have to scan the whole file for overrides.
A third-party control that overrides GetStyleTypeKey to report a key of its own (say TagButton) can register, in Pascal,
the key it derives from (see section 2 of subclassing.en.md):
TyRegisterTypeKeyParent('TagButton', 'TyButton'); // in tyControls.StyleModelFrom then on TagButton starts from everything TyButton has (built-in layer, theme layer, variants, states, exactly as
when resolving TyButton itself) and lays its own rules over it, property by property. A theme that never mentions
TagButton therefore draws it exactly like a button; one that does only writes the differences:
TagButton { border-radius: var(--radius-pill); }The child's rules are interleaved with the parent's by the stages of §4.4: plain rule → variants → each state (selected → hover → focus → active → disabled), parent first and child second within a stage. So:
- a child rule overrides what the parent wrote at the same or an earlier stage:
TagButton { }beatsTyButton { },TagButton.primarybeatsTyButton.primary,TagButton:hoverbeatsTyButton:hover; - what the parent wrote at a later stage still applies:
TyButton:hoverandTyButton:disabledbeatTagButton { }andTagButton.primary, andTyButton:disabledalso beatsTagButton:hover.
TagButton { background: #DCFCE7; } /* green at rest */
/* on hover, TyButton:hover's fill */
TagButton:hover { background: #BBF7D0; } /* write this for a hover colour of its own */This matches the browser intuition that state and class selectors win: one plain background for the child key does not swallow the parent's hover and disabled looks.
"What the parent wrote" means the parent's own result. If a theme turns PropertyCascade on and writes a plain
TyButton { background }, that background has already replaced the built-in :hover fill in TyButton's own result, so
for TagButton it counts as written at the plain stage too.
Chains can have several links, e.g. FancyTag → TagButton → TyButton: within each stage the topmost parent goes
first and the child wins ties. A chain holds at most 8 keys.
Below the chain root, each stage takes the built-in layer first and then the user layer. The root (the topmost parent)
resolves as §4.4 says: the whole built-in layer (every stage), then the whole user layer. The keys below it go stage by stage,
built-in then user within each stage. So when a built-in key is registered as a child and PropertyCascade is on, its
built-in :hover beats a plain rule the theme writes for it (a later stage); the root never does that (its user layer comes
after its entire built-in layer). Third-party keys have no built-in rules, so for them the difference never shows.
The interleave goes by property group, not by declaration. "Which stage wrote this value" is recorded per property group, the same groups a control's StyleOverride merges by: any declaration in a group counts as writing the whole group, and the group yields as one. Usually one declaration is one group; these are the ones to know:
-
outlineandoutline-offsetare one group (the focus ring). If the child writes onlyoutline-offsetand the parent writesoutlineat a later stage (typically:focus), the parent has the whole group at that stage and the child's offset yields with it:TyButton:focus { outline: 2px var(--focus-ring); outline-offset: 1px; } TagButton { outline-offset: 6px; } /* no effect while focused: the group is TyButton:focus's */ TagButton:focus { outline-offset: 6px; } /* same stage: the child wins */
-
background-sizeandbackground-blurgo withbackground: when the parent's:hoverreplaces the fill, the child'sbackground-sizeyields too. -
The glass effect,
glass-blur/glass-tint, is a group of its own and does not go withbackground: a parent:hoverthat only changes the fill leaves the child's glass alone; a parent:hoverthat writesglass-tinttakes the child'sglass-blurwith it while hovered. -
The four corners of
border-radiusare one group: they yield back to the parent's values together. -
shadow's colour, blur and offset are one group.
outline-offset is not split out as a property of its own: the groups are the ones StyleOverride merges by, so a theme author
learns one set of rules; splitting it would add a property flag to the style set that every reader of Present has to learn,
and the difference only shows when a parent and a child write the two halves of one focus ring at different stages -- writing
the offset in the same rule as the ring avoids it.
The built-in layer yields per key (§8.1). A plain TagButton { } (no variant, no state) disables only TagButton's own
built-in rules; TyButton's built-in rules still apply underneath. Third-party keys have no built-in rules anyway, so this
only shows when a built-in key is registered as a child.
Sub-part keys are registered separately. Sub-part keys a control builds as GetStyleTypeKey + 'Fill' and the like do
not follow on their own: TagMeter's fill resolves TagMeterFill, which needs its own registration to inherit TyMeterFill.
Registration rules:
- Register in the unit's
initializationand undo it infinalizationwithTyUnregisterTypeKeyParent. The registry is process-wide; a registration made after controls are on screen does not repaint them. - An exception in
initializationstops the program before it starts -- two packages registering the same key with different parents, and the one loaded second takes the whole application down. In a package, register withTyTryRegisterTypeKeyParent: on an error it returnsFalse, leaves the registry unchanged and raises nothing (the control just does not inherit the parent's rules); it returnsTruewhen the link is registered or already was. - Key names are case-insensitive and must be identifiers (a letter or
_, then letters, digits,_and-). - These raise
ETyCssErrorand leave the registry unchanged: an invalid name; a cycle (including a key registered as its own parent); a chain longer than 8 keys; a key that already has a different parent (registering the same pair again is fine). - Queries:
TyTypeKeyParent,TyTypeKeyChain,TyGetRegisteredTypeKeys.
The tools follow the chain too: the design-time StyleClass drop-down lists the parent's variants (TagButton gets
primary, danger, ghost); the controller-level StyleOverride editor lists registered keys both in selector completion and in its reference list.
Lint does not check type key names, so a child key is never reported.
A typeKey with no registered parent works as before: it gets only the rules a theme writes for it.
The engine recognizes 23 properties (plus background-color as an alias of background); every other property name is
silently ignored. This section covers the 16 general-purpose ones. The other 7 are window-chrome / material properties this
manual does not yet cover individually (render-style is covered in §9 item 17; for the rest,
background-size, background-blur, glass-blur, glass-tint, and
background-under-titlebar, the source TyApplyDeclaration is currently the reference).
All lengths are logical pixels (96 DPI baseline), scaled to the control's actual DPI when drawing.
A length may be written 6px or bare 6 (the px suffix is optional), or as var(--x) / bare --x.
background: <color-expression> ;
background: linear-gradient(<angle>deg, <stop1>, <stop2>[, …]) ;
- Solid: any color expression (
#hex,var(), bare--x, color functions; see section 6); - Gradient: see section 7; the angle must be the first argument, and the value parses as a gradient only when it starts with
linear-gradient(; 2 or more stops are supported (with optional positions).
TyPanel { background: var(--surface); }
TyButton { background: linear-gradient(90deg, lighten(--accent, 10%), var(--accent)); }background-color is an alias of background (solid colors only, no gradients); the two behave identically:
TyEdit { background-color: rgb(255, 255, 255); } /* same as background: rgb(255, 255, 255); */background-image: url(<path>) slice(<top> <right> <bottom> <left>) [repeat] ;
url()andslice()are both mandatory; omitting either raises;- The four
slicevalues are integer pixels in the fixed order top right bottom left (the CSS four-value order), giving the width of the non-stretching border on each edge of the source image; - An optional trailing
repeat(v3) tiles the four edges and the center (1:1 repetition) instead of stretching them, for bitmap skins with patterned or textured borders; without it, they stretch (the old behavior). The four corners are always 1:1 either way; - The path may be quoted or unquoted, but must never contain spaces: the lexer inserts spaces into unquoted paths
(
panel.pngbecomespanel. png) and the engine recovers by deleting all spaces unconditionally, so real spaces are deleted too and the path silently reads wrong. Keep directory and file names free of spaces; - Relative paths resolve against the process's current working directory (there is no "relative to the theme file" resolution); if the file does not exist, the fill is silently skipped (no error, nothing drawn);
var()is not supported.
TyPanel { background-image: url(assets/panel.png) slice(8 8 8 8); }color: <color-expression> ;
Used for text and for tier-b monochrome glyphs (the CheckBox check mark, ComboBox dropdown arrow, SpinEdit/ScrollBar arrows, tab close ×, and so on; see §8.7).
TyButton.primary { color: #FFFFFF; }Note: sliders, knobs, and other tier-a colored surfaces no longer borrow the control's color; their color comes from
the background of their own sub-part typeKey (TyScrollThumb, TyToggleKnob, and so on). See §8.2 / §8.7. Track and rail
backgrounds still come from the parent control's background.
border-color: <color-expression> ;
border-width: <length> ;
border-style: none | solid | outset | inset ;
The three work together: no border is stroked when border-width is 0 (or border-color is undeclared).
A solid border is stroked along the inside of the rounded rectangle.
border-style values, default solid:
none— forces no border, even whenborder-width > 0;solid— single-color stroke (along the inside of the rounded rect);outset/inset(v3) — two-color 3D bevel: top/left and bottom/right use a lighter and a darker edge color,outsetraised,insetrecessed. The light and dark colors are derived automatically fromborder-color(lighten for top/left, darken for bottom/right). Bevels are inherently square-cornered (the corner radius is not applied), suiting classic-style controls (pair withborder-radius: 0).
dashed, dotted, groove, ridge, and other styles are not supported. A tree's dotted or
dashed lines have a token of their own, --treeview-lines-style (§3.5).
TyEdit { border-color: var(--border); border-width: 1px; }
TyEdit:focus { border-color: var(--accent); }
TyPanel { border-style: none; } /* explicitly no border, even with a border-width */
/* classic raised button: face color as border-color; highlight/shadow edges derived automatically */
TyButton.classic { border: 2px outset #C0C0C0; border-radius: 0; background: #C0C0C0; }border: <width> [<style>] <color> ;
Sets border-width, border-style, and border-color in one declaration, fully equivalent to writing them separately.
Width comes first, color last, with an optional solid / none / outset / inset style between them (solid when omitted).
The color may be any color expression (#hex, var(--x), rgb(...) / rgba(...); see section 6):
TyButton { border: 2px solid var(--accent); } /* width=2px, style=solid, color=accent */
TyEdit { border: 1px var(--border); } /* style omitted, same as solid */
TyPanel { border: 0px none var(--border); } /* style=none → forces no border */border-radius: <length> ;
border-radius: <TL> <TR> <BR> <BL> ;
Shapes the background fill, the border, and the focus ring alike. 0px gives square corners.
- 1 value: the same radius on all four corners (backward compatible with the original behavior);
- 4 values: the CSS order top-left, top-right, bottom-right, bottom-left (
TL TR BR BL), space-separated. Each value may be a length orvar(--x)/ bare--x;0means square.
TyButton { border-radius: var(--radius); } /* same radius on all corners */
TyTitleBar { border-radius: 6px 6px 0 0; } /* round top, square bottom (title bar / tabs) */
TyTabControl { border-radius: var(--radius) var(--radius) 0 0; }The built-in themes use var(--radius) var(--radius) 0 0 on title bars (TyTitleBar) and tabs (TyTab) for the
round-top, square-bottom look. Write a tab's corners as it sits on a top band: with TabPosition at the
bottom or on a side, they turn with the band and stay on the edge the tab sticks out from.
Not supported: per-corner longhands (border-top-left-radius etc.), percentage radii, and the
elliptical two-radius form (<a> / <b>).
padding: <all> ;
padding: <vertical> <horizontal> ;
padding: <top> <horizontal> <bottom> ;
padding: <top> <right> <bottom> <left> ;
Space-separated, 1 to 4 values, with the same meaning as in CSS; any other count raises.
TyButton { padding: 6px; }
TyPanel { padding: 8px 12px; }
TyLabel { padding: 2px 6px 4px; }
TyEdit { padding: 4px 8px 4px 8px; }font-family: <font-name> ;
The value's raw text is used directly as the font name. Do not quote it: quote characters are kept as part of the name and the font is then not found. Write multi-word names bare (runs of spaces normalize to a single space):
TyLabel { font-family: Noto Sans CJK SC; } /* correct */
/* font-family: "Noto Sans CJK SC"; wrong: the quotes become part of the name */font-size: <number>[px] ;
The number is interpreted as points (pt): drawing converts it to pixels as round(n × 96 / 72), then applies DPI
scaling. A px suffix is allowed but merely stripped; the number is still treated as pt. font-size: 10px is really 10pt
(about 13 pixels at 96 DPI). The built-in themes follow this spelling.
TyButton { font-size: 10px; } /* = 10pt */font-weight: bold | normal | <number> ;
bold= 700,normal= 400; plain numbers work too (e.g.600);- Rendering knows only bold and regular: values ≥ 600 draw bold, everything else regular; there are no intermediate weights.
TyTitleBar { font-weight: 700; }opacity: <0..1 decimal> ;
Applies uniformly to the control's entire drawn output (text included). Its usual home is the disabled state:
TyButton:disabled { opacity: 0.5; }Opacity takes effect through the shared framework paint path (DrawFrame). In v1.1 the render paths of TyCheckBox and
TyRadioButton were changed so opacity and shadow work there too; all typeKeys (including TyLabel) now support it.
shadow: <x-offset> <y-offset> <blur-radius> <color> ;
- Four space-separated parts in fixed order: X offset, Y offset, blur radius (all logical pixels; offsets may be negative), color;
- The color must be a single token:
#hex(preferably#rrggbbaawith alpha),var(--x), or bare--x. Because the value splits on spaces, color functions containing commas (alpha(...),mix(...), etc.) cannot be used; for translucency write hex-alpha directly; - A color with alpha 0 draws no shadow;
- Shadows also go through the DrawFrame path and have no effect on
TyCheckBox/TyRadioButton.
TyButton { shadow: 0px 2px 4px #0000002E; }
TyPanel { shadow: 0px 1px 3px var(--shadow-color); }outline: <width> <color> ;
outline-offset: <length> ;
Draws a focus indicator ring inside the control. TyControls controls are fully self-drawn and their output is clipped to the control rectangle, so the focus ring always shifts inward rather than expanding outward.
outline: <width> <color>: width first (starts with a digit), color second (a color expression). Independent ofborder; affects neither layout norpadding. Setting this property addstpOutlineto thePresentset;outline-offset: <length>: how far the ring is inset from the control's outer edge; default0(hugging the edge).outline-offsetalone does not settpOutline; the ring draws only when anoutlinerule is also present;outline: 0disables the ring in a specific rule (the engine handles only theoutlineshorthand andoutline-offset; anoutline-widthproperty is not recognized and is silently ignored);- The ring's corners follow the four-corner
border-radius(each corner reduced byoutline-offsetand clamped to a minimum of 0).
Usually written inside a :focus rule, referencing the --focus-ring semantic token:
TyButton:focus { outline: 2px var(--focus-ring); }
TyEdit:focus { border-color: var(--accent); outline: 2px var(--focus-ring); outline-offset: 2px; }This is not a property name but a semantic variable the built-in themes define in :root:
:root {
--focus-ring: var(--accent);
}Every built-in theme (light.tycss, dark.tycss, and the rest) defines --focus-ring in :root and draws the focus ring
with outline: 2px var(--focus-ring) in the :focus rules of all focusable controls.
Theme authors can customize it as follows:
| Goal | How |
|---|---|
| Change the focus ring color | :root { --focus-ring: #FF8800; } |
| Disable focus rings globally | write outline: 0; in each :focus rule, or omit outline |
| Disable the ring for one control | TyEdit:focus { outline: 0; } |
| Return to square focus rings | write border-radius: 0; on the control |
A color expression is #hex | var(--x) | bare --x | rgb(...) / rgba(...) | one of the functions below (whose color
arguments are themselves color expressions, nested to any depth). Function names are case-insensitive. The % suffix on
percentage arguments is optional (8% equals 8); the one exception is alpha, see below.
Every position that accepts a color (color, background / background-color, border-color, the border: shorthand,
color function arguments, and so on) accepts all of these forms.
Lightens toward white: per channel ch + (255 − ch) × p/100; alpha is unchanged.
TyButton.primary:hover { background: lighten(--accent, 8%); }Darkens toward black: per channel ch × (1 − p/100); alpha is unchanged.
TyButton:active { background: darken(--surface, 10%); }Replaces the color's alpha with the given value (RGB unchanged); 0 fully transparent, 1 opaque.
The opacity can also be a percentage: alpha(#fff, 50%) equals alpha(#fff, 0.5).
Trap: a number without
%is read as 0..1, not as a percentage.alpha(#fff, 50)clamps to fully opaque; write0.5or50%.
TyCaptionButton:hover { background: alpha(#FFFFFF, 0.18); }
TyLabel { background: alpha(#FFFFFF, 0); } /* transparent background */Linear blend; the percentage is the share of the second color: result = color1 × (1−p/100) + color2 × p/100.
All four channels (RGB and alpha) participate.
:root { --tint: mix(--surface, --accent, 20%); } /* 80% surface + 20% accent */Builds a color from RGB components. r / g / b are integers 0..255; the fourth rgba argument a is the opacity,
written either as a 0..1 decimal or as a percentage with % (0.6 equals 60%).
rgb(...) equals rgba(...) with alpha 1. Usable anywhere a color is accepted (including border: and background).
TyEdit { background-color: rgb(255, 255, 255); }
TyLabel { color: rgba(0, 0, 0, 0.6); } /* 60% opaque black */
TyButton { border: 2px solid rgb(59, 130, 246); }TyButton:hover {
background: lighten(mix(--surface, var(--accent), 15%), 4%);
}background: linear-gradient(<angle>deg, <stop1>, <stop2>[, …<stopN>]) ;
- The angle must be the first argument (angle-first); the
degsuffix is optional and the angle may have decimals; - It is followed by 2 or more color stops (multi-stop since v3, for gloss effects and the like); fewer than 2 raises;
- Each stop is
color [position]: the color is a full color expression (functions and variables allowed; internal commas do not mis-split), the position an optional percentage or 0..1 number (e.g.#fff 0%,var(--accent) 50%); - Positions normalize by the CSS rules: a missing first position becomes
0, a missing last becomes1, missing middles interpolate evenly between the neighboring positioned stops, and positions are forced non-decreasing (a smaller position is clamped up to its predecessor); - Keyword directions such as
to rightare not supported (direction comes from the angle alone; see 7.1).
A multi-stop example (classic glass gloss: bright top band):
TyButton.primary {
background: linear-gradient(90deg, #FFFFFF 0%, #EAEAEA 48%, #DADADA 52%, #EDEDED 100%);
}The gradient axis passes through the control's center. Direction is a mathematical angle in screen coordinates
(x right, y down): dx = cos θ, dy = sin θ, with the start color at the endpoint opposite the angle:
| Angle | Direction (start color → end color) |
|---|---|
0deg |
left → right |
90deg |
top → bottom |
180deg |
right → left |
270deg |
bottom → top |
This disagrees with the browser CSS convention (0deg up, 90deg right).
The linear-gradient(90deg, …) seen throughout the built-in showcase theme is a top-to-bottom gradient:
TyButton.primary {
background: linear-gradient(90deg, lighten(--accent, 10%), var(--accent));
/* lighter at the top, accent at the bottom */
}Gradient endpoints sit where the gradient axis crosses the control's bounding box; the gradient fills the whole control rectangle.
The type name in a selector is the typeKey returned by the control's GetStyleTypeKey (sub-part typeKeys included).
A third-party control's own key can register a parent key and inherit all its rules; see §4.5.
This section is the theme author's authoritative key table. Every entry is taken from themes/light.tycss, the single
source of truth; source/tyControls.DefaultTheme.pas (the built-in base layer compiled into the library) is generated from
it and kept in byte-for-byte sync.
Current scale:
| Count | |
|---|---|
typeKeys defined in light.tycss |
183 |
| Keys controls resolve but the base layer deliberately leaves undefined (§8.4) | 8 |
| Dead keys defined in themes but resolved by no code at all (§8.6) | 3 |
The big change in v3. Previously 51 controls resolved another control's typeKey, leaving them unreachable from the theme layer (commits
b824a49,d58eada). Each of these controls now has its own key, added as a parallel selector to the original donor's rule block, so under the built-in themes not a pixel moves, yet each one is now an independently skinnable hook. Existing third-party.tycssfiles must add the new selectors; the next subsection explains why.
For a theme, the default ResolveStyle behavior (with PropertyCascade off) is all-or-nothing per typeKey:
- The theme has no plain rule for a typeKey → the whole key falls back to the built-in light base layer. Rules the theme
writes for the key's classes or states (
TyFoo.x,TyFoo:hover) apply on top of the built-in ones; - The theme writes a plain rule for the typeKey (no class, no state) → the built-in layer is disabled for the key entirely; properties the theme did not write do not leak through.
The test is TTyStyleModel.UserHasTypeKey, which only asks whether such a rule exists, never which properties you covered.
An additive patch is not a theme. LoadThemeCssAdditive and the controller's StyleOverride take over no key. For a key the
theme leaves to the built-in layer, the built-in rules for it are copied in first and the patch goes on top, so
TyCascader { padding: 6px 13px; } on the default theme changes only the padding. The key's :hover and :focus rules still
apply over the patch, as a theme's own do. Before 3.0.4 such a patch disabled the built-in layer for the key, and the control
lost every property the patch did not write.
This rule is the top upgrade trap and the single most important thing this section has to say:
Splitting keys means your theme now has keys it never wrote. One rule,
TyGauge { background: url(...) }, used to govern fourteen gauge controls at once. NowTTyMeterresolvesTyMeter, a name your theme never mentions, soTyMeterfalls back to the built-in light values as a whole key. On an image skin that shows up as an opaque gray box in the middle of the photo. If you ever skinnedTyGauge, you must now nameTyMeter,TyRating,TyDial, and the rest alongside it.
The built-in themes handle this with parallel selectors; third-party themes should copy the pattern:
/* one rule, eighteen separately overridable names */
TyGauge, TyMeter, TyLevelMeter, TyDial, TyGearDial, TyAnalogClock, TyCircularProgress,
TyActivityIndicator, TyActivityBar, TyGearActivityIndicator, TySparkline, TyRating,
TyLColorPicker, TyHSColorPicker, TyMeterTick, TyAnalogClockHand, TyGearDialTeeth, TyColorArea {
background: var(--surface-sunk);
border-color: var(--border);
}To audit: the alias guard in tests/test.themes.pas compares each new key against the donor key it was split from, entry by
entry, and covers every stylesheet under themes/ and examples/theming/. Home-grown themes can run the same self-check.
- What it paints — which pixels the key owns on the control. "Text ink" means only
coloris read from the key, "background" means only the background is read; no such note means the whole box-model property set applies. - Resolved by — the control class that resolves the key. One key may be resolved by several controls (sharing is intentional; see §8.5).
- States / variants — the pseudo-classes and
.variantsthe built-in light theme actually writes. Absence does not mean the control never reports the state: any key can take:hover/:active/:focus/:disabled/:selected, and the rule matches whenever the control reports that state on repaint (§4.3). Conversely, a few keys resolve with an empty state set (e.g.TyLinkLabelLink), making their pseudo-class selectors dead; the tables call these out.
| typeKey | What it paints | Resolved by | States / variants |
|---|---|---|---|
TyForm |
Form background; written into the form's Color via ApplyChromeTheme. Built-in background: var(--form-bg), slightly darker than content surfaces. Window corners (border-radius) and shadow (window-shadow): see ttyform.md §8. On macOS the system draws the window's corners and border-radius has no effect there; window-shadow still applies |
TTyForm |
— |
TyFormSurface |
Deliberately undefined (§8.4) | TTyFormSurface |
— |
TyTitleBar |
The self-drawn title bar strip (shares one rule block with TyRibbonQuickAccess) |
TTyTitleBar |
— |
TyCaptionButton |
Title bar system buttons | TTyCaptionButton |
:hover :active; variants .close (:hover/:active), .min (:hover), .max (:hover) |
The following six keys share one rule block with byte-identical values, each overridable separately:
| typeKey | What it paints | Resolved by |
|---|---|---|
TyButton |
Regular button frame + text | TTyButton (and the family members that do not override the key: TTyGlyphButton, TTyColorButton, TTyDropDownButton, TTyMenuButton, TTyTransferArrowButton) |
TySpeedButton |
Toolbar speed buttons | TTySpeedButton |
TyGlyphContainerButton |
Buttons with a glyph container | TTyGlyphContainerButton |
TyRibbonAppMenu |
The application menu button at the ribbon's top-left | TTyRibbonAppMenu |
TyButtonGroup |
The outer frame of a segmented button group | TTyButtonGroup |
TyUpDown |
Up-down spinner frame, center seam, and arrow ink | TTyUpDown |
States: :hover :focus :active :disabled.
Variants: .primary (:hover/:active), .danger (:hover/:active),
.ghost (transparent at rest; full set of :hover/:active/:selected/:focus/:disabled).
One more family member sits in a block of its own:
| typeKey | What it paints | Resolved by | States |
|---|---|---|---|
TyBadge |
The numeric badge capsule at a button's top-right | TTyBadge, TTyButton |
none |
The following seven keys share one rule block (transparent background + --on-surface ink + base font size):
| typeKey | What it paints | Resolved by |
|---|---|---|
TyLabel |
Static caption text | TTyLabel |
TyHtmlLabel |
Rich text block (drawn run by run, underline/strikethrough bands) | TTyHtmlLabel |
TyLinkLabel |
The link label's box (font/content, not the link ink) | TTyLinkLabel |
TyShadowLabel |
Text with a drop shadow | TTyShadowLabel |
TyGlowLabel |
Text with a glow | TTyGlowLabel |
TyDivider |
Divider line and its embedded caption | TTyDivider |
TyCharImage |
Glyph / icon character block | TTyCharImage |
States: :disabled only (opacity).
| typeKey | What it paints | Resolved by | Note |
|---|---|---|---|
TyLinkLabelLink |
Link ink, reads color only |
TTyLinkLabel |
Resolves with an empty state set; TyLinkLabelLink:hover / :disabled are dead selectors; the hover color is synthesized in code via TyLighten(col, 15) |
The
<a>ink insideTTyHtmlLabelgoes through no typeKey at all:RenderTocallsResolveOverride('color: var(--accent);')directly, pinning it to--accent. The only way to change it is to change--accent.TyHtmlLabelLinkis a deferred item and does not exist (§8.4).
| typeKey | What it paints | Resolved by | States / variants |
|---|---|---|---|
TyEdit |
Single-line input | TTyEdit and the family members that do not override the key (TTyMaskEdit/TTyCurrencyEdit/TTyURLEdit/TTyNumericEdit/TTyCalcEdit/TTyValueEdit/TTyComboEdit/TTyTrackEdit); the display strip of TTyCalculator also resolves it explicitly |
:hover :focus :disabled |
TySpinEdit |
Numeric spinner input | TTySpinEdit |
:hover :focus :disabled |
TyMemo |
Multi-line text box | TTyMemo |
:hover :focus :disabled |
TyComboBox |
Dropdown field (the dropdown arrow uses color) |
TTyComboBox and the 11 other members of its family; TTyTreeSelect shares it deliberately (§8.5) |
:hover :focus :disabled |
TyCascader |
Cascading select field | TTyCascader |
:hover :focus :disabled |
TyDateTimePicker |
Date-time field | TTyDateTimePicker |
:hover :focus :disabled |
TyTextSelection |
Text selection highlight band, reads background only |
TTyEdit / TTyMemo / TTyDateTimePicker |
none |
TyTextHint |
Placeholder / hint text, reads color only |
TTyEdit / TTyTreeSelect |
none |
| typeKey | What it paints | Resolved by | States |
|---|---|---|---|
TyCheckBox |
The check box square (the control itself is transparent) | TTyCheckBox, TTyCheckListBox, TTyDateTimePicker |
:hover :active :focus :disabled |
TyRadioButton |
The radio circle | TTyRadioButton |
:hover :active :focus :disabled |
TyToggleSwitch |
The switch track (:active is added while Checked=True) |
TTyToggleSwitch |
:hover :active :focus :disabled |
TyToggleKnob |
The switch knob (a tier-a colored surface) | TTyToggleSwitch |
none |
TyTrackBar |
Slider track | TTyTrackBar |
:focus :disabled |
TyRangeTrackBar / TyMultiTrackBar / TyGradientTrackBar |
The multi-thumb sliders themselves; parent key TyTrackBar, so without rules of their own they look like TTyTrackBar |
TTyRangeTrackBar / TTyMultiTrackBar / TTyGradientTrackBar |
:focus :disabled |
TyTrackThumb |
Slider thumb (tier-a). The :focus outline is drawn inside the thumb (outline-offset moves it in); the default theme draws a ring in --on-accent. The multi-thumb sliders add two variants after a thumb's own class: .removing (a thumb dragged off the track that goes on release; the control multiplies its opacity into that thumb's colours, --disabled-opacity in the default theme) and .stop (a stop of TTyGradientTrackBar, filled with its own colour; the default theme gives it a 1px --on-surface edge). A theme that writes its own TyTrackThumb rules has to write these two as well (§8.1) |
TTyTrackBar, TTyRangeTrackBar, TTyMultiTrackBar, TTyGradientTrackBar, TTyTrackEdit |
:hover :active :focus (the thumb the arrow keys move, while the control has the focus) |
TyTrackSelection |
Slider selection range (SelStart..SelEnd) and the ranges of TTyRangeTrackBar, reads background and border-radius |
TTyTrackBar, TTyRangeTrackBar |
none |
TyTrackSegment |
A stretch of TTyMultiTrackBar between two thumbs, resolved with the StyleClass of its Segments item; reads background and border-radius. Without a class it is not drawn; the default theme has .success, .warning and .danger variants |
TTyMultiTrackBar |
none |
TyProgressBar |
Progress bar track | TTyProgressBar |
:disabled |
TyProgressFill |
Progress fill segment (tier-a); only the leading corners are rounded while partially filled | TTyProgressBar |
none |
The following seventeen keys share one rule block (surface color + 1px border + --radius + --pad-container).
Every one of them except TyPanel used to be reachable only by editing TyPanel, which meant touching every container
in the application:
| typeKey | What it paints | Resolved by |
|---|---|---|
TyPanel |
General-purpose container panel | TTyPanel; TTyRelativePanel / TTyPaintPanel share it deliberately (§8.5) |
TyScrollBox |
The scroll well (by convention sunk below panels) | TTyScrollBox; TTyScrollPanel inherits |
TyScrollContent |
The scroll container's viewport (the windowed child that clips the content). Paints background only, no border (the frame belongs to the outer scroll box). This key must have a value and must be opaque: the control paints nothing but this fill, and an unresolvable key exposes the host's unthemed erase color (on gradient skins such as aero in dark mode, a bright patch) |
TTyScrollContent (the viewport of TTyScrollBox / TTyScrollPanel) |
TyExPanel |
The collapsible card's frame (its padding.right also sets the header's right inset) |
TTyExPanel |
TyChart |
Chart frame, title, legend, tick labels, both axes, and grid lines | TTyChart |
TyCalculator |
Calculator backplate (the display strip resolves TyEdit; the keys are real TTyButtons) |
TTyCalculator |
TyBevel |
Raised/sunken decorative line (draws only the highlight and shadow tracks, no face) | TTyBevel |
TySizeBox |
The resize grip in the bottom-right corner | TTySizeBox |
TyControlBar |
Dockable toolbar container and its decorative grippers | TTyControlBar |
TyCoolBar |
Draggable banded toolbar (grippers are interactive) | TTyCoolBar |
TyColorGrid |
Color swatch matrix and selection ring | TTyColorGrid |
TyShape |
Vector primitives (ellipse/rectangle/triangle/line) | TTyShape |
TyStarShape |
Star primitive | TTyStarShape |
TyArrow |
Arrow primitive | TTyArrow |
TyImageView |
The letterbox backing behind a zoomed/panned image | TTyImageView |
TyImage |
Image box (framed only when Transparent=False) |
TTyImage |
TyPreviewBox |
The file dialog's preview well + empty-state hint text | TTyPreviewBox |
TyListGroupPanel |
The navigation accordion's sidebar backing | TTyListGroupPanel |
This block has no state rules. To fade a disabled image you must write
TyImage:disabled { opacity: 0.5; } yourself; the built-in theme declares no state rules for this family.
| typeKey | What it paints | Resolved by | States |
|---|---|---|---|
TyExPanelHeader |
The collapsible panel's header band: arrow + caption ink and font; background is optional (declared: fills the band; undeclared: the panel surface shows through) |
TTyExPanel |
:hover (hover within the band; not in the built-in theme, works as soon as you write it) |
TyGroupBox |
Group box (must declare background, used to mask the border line behind the caption) |
TTyGroupBox, TTyRadioGroup, TTyCheckGroup; the group caption bands of TTyOfficeListBox/TTyOfficeComboBox also resolve it |
— |
TyToolGroupPanel |
Ribbon-style command cluster (shares a block with TyGroupBox) |
TTyToolGroupPanel |
— |
TyGridPanel |
Deliberately undefined (§8.4) | TTyGridPanel |
— |
| typeKey | What it paints | Resolved by | States |
|---|---|---|---|
TyScrollBar |
Scroll bar track background; end arrow ink uses color (tier-b) |
TTyScrollBar |
:hover :active :focus :disabled |
TyScrollThumb |
The thumb's own fill and radius (tier-a) | TTyScrollBar |
:hover :active |
TySplitter |
Splitter bar (built-in background: none, gripper ink only) |
TTySplitter |
:hover |
TyStatusBar |
Status bar strip | TTyStatusBar |
— |
TyToolBar |
Toolbar strip | TTyToolBar, TTyToolBarEx |
— |
TyToolSeparator |
Toolbar separator: border-color draws the line, background blends it into the strip (shares a block with TyToolBar) |
TTyToolSeparator |
— |
| typeKey | What it paints | Resolved by | States |
|---|---|---|---|
TyListBox |
List frame | TTyListBox and 14 family members that do not override the key; the dropdown bodies of TTyComboBox/TTyValueListEditor, TTyPopupSurface, and TTyGalleryGrid (the gallery popup grid) also resolve it |
:hover :focus :disabled |
TyValueListEditor |
Property grid frame (shares a block with TyListBox) |
TTyValueListEditor |
:hover :focus :disabled |
TyRibbonGallery |
The ribbon gallery's in-ribbon row frame (shares a block with TyListBox) |
TTyRibbonGallery |
:hover :focus :disabled |
TyListItem |
A single row: background sets the row fill, color the text |
TTyListBox; the tiles of TTyRibbonGallery still resolve it too |
:hover :active (= the selected row) |
TyValueListEditorRow |
One property grid row (shares a block with TyListItem) |
TTyValueListEditor |
:hover :active |
TyValueListEditorGroup |
A heading row of the property grid (TTyValueRow.Heading, the property grid's categories) at rest: its band, ink and weight; hung on TyValueListEditorRow |
TTyValueListEditor |
— (hovered or selected it is drawn as TyValueListEditorRow) |
TyValueListEditorKey / -Value / -Divider / -Expander are resolved by code but deliberately undefined in the base layer; see §8.4.
| typeKey | What it paints | Resolved by | States |
|---|---|---|---|
TyTreeView |
Tree frame (shares a block with TyListView) |
TTyTreeView; TTyShellTreeView and the dropdown tree of TTyTreeSelect inherit |
— |
TyListView |
List view frame | TTyListView; TTyShellListView inherits |
— |
TyTreeNode |
One tree node row | TTyTreeView |
:hover :selected :disabled |
TyListViewItem |
One list view item (shares a block with TyTreeNode) |
TTyListView |
:hover :selected :disabled |
TyTreeHeader |
The tree's column header band (drawn inside its own frame, no border) | TTyTreeView |
— |
TyListViewHeader |
The list view's column header band | TTyListView |
— |
TyListViewGroupHeader |
The list view's group band (previously shared the header band's key and could not be styled apart) | TTyListView |
— |
TyHeaderControl |
Standalone header strip: it is the frame, so border-radius/shadow are meaningful here (shares a block with the three above) |
TTyHeaderControl |
— |
TyTreeHeaderSection |
One column header cell | TTyTreeView; TTyHeaderControl still shares it (its own section key is deferred) |
:hover :selected |
TyListViewHeaderSection |
The list view's column header cell (shares a block with the above) | TTyListView |
:hover :selected |
TyTreeCheckBox |
Tree node check box | TTyTreeView |
:active :selected :disabled |
TyListViewCheckBox |
List view check box (shares a block with the above) | TTyListView |
:active :selected :disabled |
TyListViewLine / TyListViewMarquee are resolved by code but deliberately undefined in the base layer; see §8.4.
The grid has a complete key set of its own and borrows nothing from the tree/list keys. The base layer defines the full
set, so a new skin renders correctly without a single grid rule. TTyDrawGrid / TTyStringGrid deliberately share TyGrid
(§8.5). Every grid resolve carries the control's StyleClass, so .variants work on the sub-part keys too;
ListView / ValueListEditor do not do this.
| typeKey | What it paints | States |
|---|---|---|
TyGrid |
Grid frame | — |
TyGridCell |
Body cell (built-in background: none, letting the surface show through) |
:hover :selected |
TyGridCellSelectedInactive |
The selection while unfocused (HideSelectionWhenInactive) |
— |
TyGridActiveCell |
The focused cell (marks the cursor cell in whole-row selection mode) | — |
TyGridCellMarked |
The translucent layer when the selection covers a user-colored cell | — |
TyGridSelectionFrame |
Selection frame + bottom-right fill handle (color = the handle's stroke) |
— |
TyGridCellAlt |
Zebra-stripe alternate row fill | — |
TyGridFixed |
Frozen rows/columns area | — |
TyGridIndicator |
Row header slot | — |
TyGridHeader |
Column header band | — |
TyGridHeaderSection |
One column header cell | :hover :selected :active |
TyGridHeaderGroup |
Grouped header band (an upper caption spanning several columns) | — |
TyGridFilterRow |
The inline filter row (fill follows the surface, border follows inputs, signaling "you can type here") | — |
TyGridGroupRow |
Group band. The built-in background: none is the intended current look; declare a chrome fill yourself if you want one |
— |
TyGridSummaryRow |
Summary/footer band. Deliberately has no border-color: RenderFooter never reads it, so writing one is a dead property |
— |
TyGridLine |
Grid lines, reads background only; set it transparent to remove the lines |
— |
TyGridCheckBox |
Check box cell | :selected |
TyGridHyperlink |
Hyperlink cell ink | — |
TyGridCommentMark |
Comment corner mark ink | — |
TyGridProgress |
Progress cell track | — |
TyGridProgressFill |
Progress cell fill | — |
TyGridRating |
The rating cell's filled star | — |
TyGridRatingEmpty |
The rating cell's hollow star | — |
TyGridButton |
Button cell (its own key, so styling grid buttons does not ripple into every button in the library) | :hover :active |
Name collision warning: two unrelated things claim
TyGridCell. Besides the data grid's body cell, the layout cellTTyGridCellofTTyGridPanelalso returns'TyGridCell'fromGetStyleTypeKey. The latter'sPaintdraws nothing, so there is no visible conflict today; but it is not an independently addressable hook, and aTyGridCellrule written for the data grid nominally covers it too.
| typeKey | What it paints | Resolved by | States |
|---|---|---|---|
TyPageControl |
The multi-page control's page container frame | TTyPageControl |
:hover :focus :disabled |
TyTabSheet |
The page body: an opaque surface fill only, no border, so the OS form background cannot show through | TTyTabSheet |
— |
TyTabSet |
A tab strip with no page body | TTyTabSet |
:hover :focus :disabled |
TyTab |
A single tab header (:active here means selected, not pressed) |
The tab strip engine (TTyPageControl / TTyTabSet); ribbon tab headers resolve it too |
:hover :active |
TyTabClose |
The rounded backdrop behind a hovered close × (the × stroke itself uses TyTab's color, tier-b) |
The tab strip engine | none |
Because
HasPageBody = False,TyTabSetskipsDrawFrameand draws only a baseline rail, so it actually consumes justborder-colorandborder-width; writingbackground/border-radius/shadowdraws nothing. A dedicatedTyTabSetRailis deferred and does not exist (§8.4).
| typeKey | What it paints | Resolved by | States |
|---|---|---|---|
TyMenuBar |
The application menu bar at the top | TTyMenuBar |
— |
TyMenuView |
The dropdown/context popup body | TTyMenuView |
— |
TyMenuPopup |
The popup host (a mirror key holding the same values as TyMenuView) |
TTyMenuView |
— |
TyMenuItem |
One menu row / menu bar cell; the base-state border-color is the separator ink |
TTyMenuBar / TTyMenuView |
:hover :active :disabled |
| typeKey | What it paints | Resolved by | States |
|---|---|---|---|
TyCalendar |
Calendar frame | TTyCalendar, the dropdown calendar of TTyDateTimePicker |
— |
TyCalendarTitle |
The month/year title at the top (bold) | TTyCalendar |
:hover |
TyCalendarWeekday |
The weekday header row (muted ink) | TTyCalendar |
— |
TyCalendarCell |
One date cell | TTyCalendar |
:hover :selected :disabled |
TyDateTimePicker |
See §8.2.4 | TTyDateTimePicker |
:hover :focus :disabled |
| typeKey | What it paints | Resolved by | States |
|---|---|---|---|
TyRibbon |
The command ribbon surface | TTyRibbon; TTyRibbonPage shares it deliberately (§8.5); it is also the default StyleKey of TTyPopupSurface |
— |
TyRibbonGroup |
The captioned command group box | TTyRibbonGroup |
— |
TyRibbonBackstage |
The backstage view's content pane (shares a block with TyRibbon) |
TTyRibbonBackstage |
— |
TyRibbonQuickAccess |
The quick access strip (shares a block with TyTitleBar) |
TTyRibbonQuickAccess |
— |
TyRibbonAppMenu |
See §8.2.2 | TTyRibbonAppMenu |
same as the button family |
TyRibbonGallery |
See §8.2.8 | TTyRibbonGallery |
:hover :focus :disabled |
Two places still borrow; that is deferral, not design. The backstage view's sidebar and command rows resolve
ResolveStyle('TyButton', 'primary')(with:hover/:active), so the sidebar can only be the primary button color; the gallery's tiles still resolveTyListItem, and its popup grid still resolvesTyListBox.TyRibbonBackstageSidebar/-Item/-Back/-Separator,TyRibbonGalleryItem/TyRibbonGalleryPopupall do not exist (§8.4).Also: the name
TyRibbonTabhas never been used; ribbon tab headers resolveTyTab. Earlier documentation listed it as "reserved"; that reservation is gone. Do not write rules for it.
Previously fourteen unrelated controls all hard-returned TyGauge and all resolved the same TyGaugeFill, so a star
rating, an analog clock's second hand, and a progress ring were the same color by construction. Each now has its own
key; they share one rule block.
Faces / backing (one block with TyGauge: sunken fill + border):
| typeKey | What it paints | Resolved by | States |
|---|---|---|---|
TyGauge |
Gauge face (this family's definer, not a borrower) | TTyGauge |
:disabled |
TyMeter |
Meter reading area and text | TTyMeter |
:disabled |
TyLevelMeter |
Level meter track | TTyLevelMeter |
:disabled |
TyDial |
Dial face | TTyDial |
:disabled |
TyGearDial |
Gear dial face | TTyGearDial |
:disabled |
TyAnalogClock |
Clock face and ticks (color) |
TTyAnalogClock |
:disabled |
TyCircularProgress |
The circular progress track ring | TTyCircularProgress |
:disabled |
TyActivityIndicator |
The busy indicator's track ring | TTyActivityIndicator |
:disabled |
TyActivityBar |
The marquee progress bar's track frame | TTyActivityBar |
:disabled |
TyGearActivityIndicator |
The gear busy indicator's punched hole (role reversed from the gauges: this paints the hole, not the track) | TTyGearActivityIndicator |
:disabled |
TySparkline |
Sparkline backing | TTySparkline |
:disabled |
TyRating |
Rating control backing | TTyRating |
:disabled |
TyLColorPicker |
The lightness bar's frame and cursor (the bar body is HSV-generated pixels, no sub-part key) | TTyLColorPicker |
:disabled |
TyHSColorPicker |
The hue/saturation square's frame and crosshair cursor | TTyHSColorPicker |
:disabled |
TyMeterTick |
Meter ticks, reads color only |
TTyMeter |
empty state set |
TyAnalogClockHand |
Hour + minute hands, reads color only |
TTyAnalogClock |
empty state set |
TyGearDialTeeth |
Gear teeth, reads background only |
TTyGearDial |
empty state set |
TyColorArea |
The color dialog's HSV square / hue bar frame and cursor | TTyHSVSquare / TTyHueBar |
empty state set |
Lit parts (one block with TyGaugeFill: background: var(--accent)):
| typeKey | What it paints | Resolved by |
|---|---|---|
TyGaugeFill |
Gauge fill. The only sub-part key resolved with a hard-coded literal (ResolveStyle('TyGaugeFill', …)); all the others are GetStyleTypeKey + 'suffix' |
TTyGauge |
TyMeterNeedle |
Meter needle | TTyMeter |
TyLevelMeterFill |
The level bar | TTyLevelMeter |
TyLevelMeterPeak |
The peak marker line. Give it a contrasting color; it used to be the same color as the bar it marks | TTyLevelMeter |
TyDialPointer |
Dial pointer | TTyDial |
TyGearDialPointer |
Gear dial pointer | TTyGearDial |
TyAnalogClockSecondHand |
Second hand + center hub | TTyAnalogClock |
TyCircularProgressFill |
The circular progress arc | TTyCircularProgress |
TyActivityIndicatorFill |
The spinning arc | TTyActivityIndicator |
TyActivityBarFill |
The traveling block | TTyActivityBar |
TyGearActivityIndicatorFill |
Teeth and disc body | TTyGearActivityIndicator |
TySparklineFill |
The polyline | TTySparkline |
TySparklineDot |
The polyline endpoints | TTySparkline |
TyRatingStar |
The star shape | TTyRating |
TyRatingStaris the family's only key with a new state axis: during preview hover it resolves with:hover(Rating.pas), and the built-in theme deliberately omitsTyRatingStar:hoverso pixels stay put; a skin adds that one rule to color the preview.
:disabled { opacity }is a no-op on most of this family:Opacityis applied only insideDrawFrame, and of this family only ActivityBar, LevelMeter, Sparkline, and the linear style ofTTyGaugego throughDrawFrame. The tables above faithfully list the:disabledrules the theme writes, but most of them render no effect today.
| typeKey | What it paints | Resolved by | States / variants |
|---|---|---|---|
TyCard |
Card surface (header/footer are bands painted on it) | TTyCard |
:hover :disabled |
TyCardHeader |
Header band: separator line and caption ink only, no background, so the card's own surface shows through | TTyCard |
— |
TyCardActions |
The bottom action band's separator line | TTyCard |
— |
TyTag |
Tag capsule | TTyTag |
:disabled; variants .accent .danger |
TyTagClose |
The tag's close × | TTyTag |
:hover |
TyAlert |
Inline alert strip | TTyAlert |
:disabled; variants .info .success .warning .error (hard contract: AlertType maps directly onto these four names) |
TyAlertClose |
The alert's close × | TTyAlert |
:hover |
TyNotification |
Corner overlay notification | TTyNotification |
:hover; variants .info .success .warning .error (the variant's color is the icon ink) |
TyNotificationClose |
The notification's close × | TTyNotification |
:hover |
TyEmpty |
Empty-state placeholder (transparent, resting on the empty list's own surface) | TTyEmpty |
:disabled |
TyEmptyImage |
The empty-state illustration's ink (fainter than body text, hence its own key) | TTyEmpty |
— |
TyHint |
The themed tooltip surface (replacing the native LCL hint) | TTyHintWindow, TTyBalloonHint |
— |
TyPopover |
An overlay surface that hosts controls (the arrow is cut from the same surface, so it needs no key of its own) | TTyPopover |
— |
TyPopoverTitle |
Popover title | TTyPopover |
— |
TyChartTooltip |
The in-chart hover tooltip. The chart does not draw this box unless this key supplies a background | TTyChart |
— |
| typeKey | What it paints | Resolved by | States |
|---|---|---|---|
TySegmented |
The segmented control's trough | TTySegmented |
:focus :disabled |
TySegmentedItem |
One segment (its radius is deliberately smaller than the trough's, reading as "a piece inside the trough") | TTySegmented |
:hover :selected :disabled |
TyPagination |
The pagination strip (transparent; the page numbers themselves are the chrome) | TTyPagination |
:disabled |
TyPaginationItem |
One page number cell | TTyPagination |
:hover :selected :disabled |
TySteps |
The steps bar | TTySteps |
:disabled |
TyStepsItem |
A step marker. State is semantics: completed = no pseudo-class, current = :selected, not yet reached = :disabled, so the base rule paints the completed look |
TTySteps |
:hover :selected :disabled |
TyStepsConnector |
The line between steps, reads background only (deliberately declares no color); takes the state of the step it leads to |
TTySteps |
:selected :disabled |
TyBreadcrumb |
The breadcrumb strip (separators use the strip's own ink) | TTyBreadcrumb |
:disabled |
TyBreadcrumbItem |
One breadcrumb segment; :selected = the last segment (the current location, read as plain ink rather than a link) |
TTyBreadcrumb |
:hover :selected :disabled |
TyTransfer |
The transfer box frame (the two list faces and the move buttons reuse TyListBox / TyButton) |
TTyTransfer |
:disabled |
TyTransferTitle |
The title band on each side of the transfer box | TTyTransfer |
— |
TyCascaderPanel |
The cascading multi-column panel | TTyCascaderPanel |
— |
TyCascaderItem |
One row in the cascader panel | TTyCascaderPanel |
:hover :selected :disabled |
TyListGroupHeader |
The navigation accordion's group row (no fill, just muted ink + a right-side arrow; :selected = the group is expanded) |
TTyListGroupPanel |
:hover :selected |
TyListGroupItem |
A navigation item; :active = selected, drawn as a soft inset rounded pill |
TTyListGroupPanel |
:hover :active :disabled |
TTyTreeSelecthas no key of its own, and should not: it resolvesTyComboBox(it is a combo box with a dropdown tree), and its popup body is a realTTyTreeViewcolored byTyTreeView. WritingTyTreeSelect { … }is dead CSS that never matches.
Any identifier can be a variant; it matches as soon as the control's StyleClass property contains the corresponding token
(space-separated, several allowed). The "variants" columns above list only what the built-in theme actually defines:
- The button family's
.primary/.danger/.ghost; TyCaptionButton's.close/.min/.max(assigned to the three title bar buttons automatically by the form chrome);TyTag's.accent/.danger;- The
.info/.success/.warning/.errorofTyAlert/TyNotification: these four are a hard contract; the control'sAlertType/NotificationTypemaps directly onto the four names and picks the icon accordingly.
Remember §4.4: the variant layer applies before the state layer, so a variant's hover must be written Ty….variant:hover.
Code resolves the following keys, but the base layer deliberately leaves them undefined. They are not omissions but optional hooks: each has a well-defined fallback on the paint side, and the fallback is state-dependent, so hard-coding a value here would move pixels rather than preserve them. Skins may declare them freely; the base layer stays silent so the fallbacks keep working.
| typeKey | Resolved by | Why undefined / the fallback |
|---|---|---|
TyGridPanel |
TTyGridPanel |
A grid layout host is not a slab. With no themed background, DrawFrame takes TyFillParentBg, so the gaps between cells take the parent container's color instead of punching a white panel into whatever hosts it. tests/test.gridpanel.pas asserts that no built-in theme gives this key a background. For a visible grid face, write TyGridPanel { background: …; border: … } yourself |
TyFormSurface |
TTyFormSurface |
It has no CSS of its own. The surface renders the host form's form background (see its Paint) and hosts children that resolve their own styles. The point of the neutral key: keep TyForm's padding / border from accidentally applying to the surface's own layout |
TyListViewLine |
TTyListView |
background → otherwise the list view frame's border-color |
TyListViewMarquee |
TTyListView |
background → otherwise the TyListViewItem:selected background (hue follows the theme, alpha is a constant) |
TyValueListEditorKey |
TTyValueListEditor |
color → otherwise the row color for the row's current state |
TyValueListEditorValue |
TTyValueListEditor |
Same as above |
TyValueListEditorExpander |
TTyValueListEditor |
Same as above |
TyValueListEditorDivider |
TTyValueListEditor |
background → otherwise the row color at alpha 0x28 |
The last four are sharp edges: the fallback ink on a selected row is
--on-accent, so a flatcolor: var(--on-surface)here makes the key names on selected rows unreadable. If you declare them, declare the:active/:hovervariants too.
b824a49 / d58eada covered box keys only. A batch of sub-part keys proposed by the same audit was
deliberately deferred: they are an extension of skinnability rather than coverage gaps, and several of them
would move pixels (the code currently invents a color where the key ought to supply one).
Do not write rules for the following names; they match nothing today:
TyHtmlLabelLink, TyButtonGroupItem, TyUpDownButton,
TyChartTitle / TyChartLegend / TyChartAxis / TyChartGrid / TyChartLabel / TyChartSeries1..8,
TyCalculatorDisplay / TyCalculatorExpression, TyBevelHighlight / TyBevelShadow,
TySizeBoxDot, TyControlBarGripper, TyCoolBarGripper, TyColorGridCell,
TyRibbonBackstageSidebar / -Item / -Back / -Separator,
TyRibbonGalleryItem / TyRibbonGalleryPopup, TyTabSetRail,
TyHeaderControlSection / -SortMark / -Divider.
The corresponding parts of these controls cannot currently be skinned separately; they either borrow keys already in the tables above, or their colors and geometry are Pascal constants.
About forty controls still share another control's typeKey, and every case was audited and documented (rationale in the plan document above and the two commit messages). The criterion: should a skin ever be able to treat them differently? Do not "fix" them.
| Sharer | Key used | Why sharing is right |
|---|---|---|
TTyMaskEdit, TTyCurrencyEdit, TTyURLEdit, TTyNumericEdit, TTyCalcEdit, TTyCalcCurrencyEdit, TTyValueEdit, TTyComboEdit |
TyEdit |
An input with a mask or a calculator dropdown is an input. Box, border, padding, and states are all TyEdit's; the differences are input filtering and trailing-button behavior. The shared key is exactly why they sit seamlessly in one form row, and why TyEdit.small dresses them all at once |
TTyFontComboBox, TTyFontSizeComboBox, TTyMRUComboBox, TTyColorComboBox, TTyCheckComboBox, TTyComboBoxEx, 11 in all |
TyComboBox |
Same reasoning; they are all combo box fields |
TTyTreeSelect |
TyComboBox |
The three marks it paints are identical to TTyComboBox's, one for one; the dropdown body is a real TTyTreeView colored by TyTreeView |
TTyRadioGroup, TTyCheckGroup |
TyGroupBox |
A group box is "a captioned frame"; inside it are real controls with keys of their own |
TTyShellTreeView |
TyTreeView (whole family) |
The shell adapter paints not a single pixel itself; a directory tree is a tree |
TTyShellListView |
TyListView (whole family) |
Same as above |
TTyDrawGrid, TTyStringGrid |
TyGrid (whole family) |
In the three-layer hierarchy only TTyCustomGrid paints pixels |
TTyRibbonPage |
TyRibbon |
The page paints a strict subset of the ribbon box and deliberately no border; a different color here would expose a 1px ring of ribbon color around every page |
TTyRelativePanel |
TyPanel |
It paints not one pixel (no Paint, no RenderTo); everything comes from TTyPanel.RenderTo. It is only a layout policy, and a layout policy is not a visual identity a skin needs to distinguish |
TTyPaintPanel |
TyPanel |
Byte-for-byte compatible with a plain panel; it is a panel |
TTyScrollPanel |
TyScrollBox (inherited) |
What it adds is gestures, not surface; it draws nothing extra, so sharing the scroll well's key is the correct outcome. Note it is no longer TyPanel; the base class moved |
TTyGlyphButton |
TyButton |
It is a push button with a partitioned content area |
TTyGalleryGrid (the gallery popup grid) |
TyListBox |
See the deferral note in §8.2.14 |
The column header cells of TTyHeaderControl |
TyTreeHeaderSection |
See §8.2.9; not yet split, so this one is not "by design" |
The following names are defined in some bundled stylesheets, but source/ contains no resolve site for them.
Rules written for them have no effect. (Coverage is uneven: TyGridSelection appears only in light.tycss, while
TyTabControl and TyDateTimeButton each appear in 6 files, so "every theme has them" would be false.)
| typeKey | Situation |
|---|---|
TyTabControl |
The class TTyTabControl has been deleted (commit d201419); the key survives only as a parallel selector alongside TyTabSet |
TyDateTimeButton |
The dropdown button of TTyDateTimePicker never resolves it |
TyGridSelection |
The grid actually uses TyGridSelectionFrame |
TyTreeSelect |
TTyTreeSelect returns 'TyComboBox' (intentional sharing, §8.5). Its rule block was removed from light.tycss in 3.0, so it is no longer even "defined"; a purely dead name |
Small parts inside a control that are not the "control body frame" get their colors by one of two routes, according to whether the part is an independent colored surface. This is the official convention, not a legacy gap:
-
tier-a — colored surfaces: thumbs, knobs, fills, pointers, needles: each is a solid face that needs its own color. Each has a dedicated sub-part typeKey (
TyScrollThumb,TyToggleKnob,TyTrackThumb,TyProgressFill, plus the fourteen…Fill/ pointer keys of §8.2.15). Color comes from that typeKey'sbackground, radius from itsborder-radius, independent of the parent control'sbackground/color. Theme authors can color them (and their:hover/:active) separately. -
tier-b — monochrome glyphs: the check mark / radio dot, ComboBox dropdown arrow, SpinEdit up/down arrows, ScrollBar end arrows, tab close ×, and the like are a single stroke of ink with no face of their own. They borrow the
color(TextColor) of the owning control's (or sub-part's) style as their ink. This is the official convention, not "each should have had a typeKey and one was forgotten". To recolor such a glyph, setcoloron the owning typeKey (e.g.TyScrollBar { color: … }recolors the end arrows,TyTab { color: … }recolors the close ×).
An occasional tier-a part carries a tier-b glyph on top: the backdrop of a tab's close × is tier-a (
TyTabClose'sbackground), while the × stroke on it is tier-b (TyTab'scolor).
Engine-level limitations (each expanded in the sections above):
-
No combinator selectors: descendant / child / wildcard / multi-variant / bare-class / bare-state selectors are all unsupported (§4.2).
-
Duplicate selectors: last one wins, merged per property (§4.4), same as browsers. The genuinely counter-intuitive part is the base layer's all-or-nothing rule: once a theme mentions a typeKey at all, the built-in default layer goes dark for that key entirely (§8.1), so newly split keys must each be named.
-
url()paths cannot contain spaces: spaces are deleted unconditionally when the file name is reassembled (§5.2); paths resolve against the process working directory, and missing files are skipped silently. -
shadowcolors must be a single token:#hex/var(--x)/ bare--x; comma-bearing color functions are unusable; for translucency use#rrggbbaa(§5.12). -
opacityandshadowwork on all controls (v1.1): v1.1 fixed the render paths ofTyCheckBoxandTyRadioButtonso they too supportopacityandshadow; all typeKeys are covered. -
Without
%, the secondalpha()argument is a 0..1 decimal:alpha(#fff, 50)is fully opaque; for half transparency write0.5or50%(§6.3). -
Gradient angle directions differ from CSS:
0degleft→right,90degtop→bottom (§7.1); gradients are linear only, and the direction is an angle; keywords such asto rightare not supported (§7). -
font-sizenumbers are interpreted as pt; thepxsuffix is decoration (§5.9).font-weightrenders in just two steps: ≥600 bold, everything else regular (§5.10). -
Do not quote
font-family; the quotes are kept as part of the name (§5.8). -
Sub-part coloring splits into tier-a / tier-b (Batch ④): colored surfaces such as thumbs / knobs / fills are colored by the
backgroundof a dedicated sub-part typeKey (TyScrollThumb,TyToggleKnob,TyTrackThumb,TyProgressFill); monochrome glyphs such as check marks / arrows / close × borrow the owning control'scolor(TextColor) as their ink. Both are official conventions (§8.7). The defaults ofTyScrollThumb/TyToggleKnobare identical, value for value, to the earliercolor-borrowing rendering. -
Tab overflow scrolls horizontally (v1.10): when the combined width of all tab headers exceeds the control width, the tab strip enters overflow mode and can scroll horizontally:
tgArrowLeft/tgArrowRightarrow buttons render at the strip's two ends, the mouse wheel scrolls over the strip, and switching the selected tab auto-scrolls it into view. While drawing, tab headers are clipped to the visible band between the two arrows (the arrows always draw on top). This strip engine is shared byTTyPageControlandTTyTabSet; details in controls/pagecontrol.md. (An earlier revision namedTyTabControlhere; that control class has been deleted and the typeKey is now dead, see §8.6.) -
@media,!important, escaped strings, and//line comments are unsupported.@import(which must precede all:rootblocks and rules) and@modeare supported. -
Border / box-model trade-offs (v1.6; 3D bevels added in v3):
border-stylesupportsnone/solid/outset/inset(the latter two are two-color 3D bevels, see 5.4), with nodashed,dotted,groove, orridge; there is nomarginproperty (use container layout for outer spacing). Thebordershorthand merely combinesborder-width/border-style/border-colorand adds no extra capability. -
Unsupported
border-radiusforms: per-corner longhands (border-top-left-radiusetc.), percentage radii, and the elliptical two-radius syntax (<a> / <b>) are all unsupported (§5.6).outline-offsetdeclared alone does not activate the focus ring; anoutlinedeclaration must accompany it (§5.13). -
Metric tokens (v3): themes still cannot do general layout (no
width/height/margin/gapproperties; control size and placement belong to.lfm/LCL), but the intrinsic geometry of some controls can be tuned with named length tokens in:root. Controls read them by name and fall back to built-in constants when unset (so goldens do not change):--checkbox-size/--checkbox-gap— check box indicator side length / gap to the caption (defaults 16 / 6);--radio-size/--radio-gap— the same for radio buttons (the dot scales proportionally with the box);--groupbox-caption-height— the GroupBox top caption band height (default 16).--track-remove-distance— how far past the control's edge, across the track, a thumb of a multi-thumb slider has to be dragged to go on release (default 24).--listview-text-margin/--listview-icon-gap— where a list view's report row starts its content / the gap between its icon and its caption (defaults 4 / 2);--treeview-margin/--treeview-icon-gap— a tree's main-column inset / the gap in front of the caption (defaults 4 / 2). These two pairs can also be set for one control alone (§3.4).--memo-tab-size— the distance between a memo's tab stops, as CSS readstab-size: a number is that many spaces, a length (such as32px) is pixels (default 8 spaces). It can also be set for one memo alone (§3.4).--popover-move-distance/--popover-show-duration/--popover-hide-duration— how far a popover moves in (default 8), and its show and fade-out times in milliseconds (defaults 150 / 100, unitless). They can also be set for one popover alone (§3.4).--line-height— the line box height (logical px) of multi-line captions. No built-in constant: when unset, the font's own line box is used, so nothing moves until you add the token; once set, the text block isline count × value, with the extra height split evenly above and below each line. The control's derived minimum height (text block height + vertical padding) follows along: shrinkfont-sizeand--line-heighttogether and the minimum drops with them. Values may be16,16px, orvar(). (Later versions will promote more hard-coded geometry to metric tokens.)
-
Glyph override tokens (v3): built-in vector glyphs (check marks, radio dots, and so on) can be replaced with icon font code points. In
:rootwrite--glyph-<slot>: "font family" "\<codepoint>";where the family must be a font installed on the system or registered by the application (loading a bundled.ttfviaurl()is a future extension) and the code point is hexadecimal (the leading\is optional). Slots currently supported:--glyph-check/--glyph-check-indeterminate— check box check mark / indeterminate state;--glyph-radio— radio button dot;--glyph-close/--glyph-minimize/--glyph-maximize/--glyph-restore— title bar button icons;--glyph-arrow-up/--glyph-arrow-down/--glyph-arrow-left/--glyph-arrow-right— SpinEdit / scroll bar arrows;--glyph-chevron-down/--glyph-chevron-right/--glyph-chevron-left/--glyph-chevron-up— expand/collapse chevrons: tree view, grid tree column, list view groups, ListGroupPanel, ExPanel, the inspector (ValueListEditor / PropertyGrid), a menu's submenu mark, a cascader's branch rows, the ribbon's collapse button (-up/-down).-leftis the mirror partner of-right: in an RTL grid the collapse chevron points along the reading direction and takes this slot (seedocs/controls/grid.md);--glyph-dropdown— a drop field's indicator V: the combo box family, cascader, tree select, date picker, drop-down / split buttons, tool bar drop buttons, ribbon gallery, grid drop-down cells (all throughTyDrawDropChevron, so one rule reaches every one of them).
Size and ink of the expand marks: both groups above are one mark -- an open 90° chevron with round ends and a round join, a stroke of 5/28 of its size, its apex on a whole pixel.
--glyph-chevron-sizeis the long side of its centre line (default 7, 8 at modern density; it used to size the drop V alone, default 9), and the slot a control reserves places the mark without sizing it.--glyph-chevron-alpha(0..255, default 200) draws it a little lighter than the text beside it; 255 gives the text colour. The values sit where Windows 11, macOS and GNOME draw the same marks (ink about 0.9x the cap height of the body text beside it).The drop fields' button zone: the combo box family, the combo edit, the date picker (drop-down mode), the tree select and the cascader reserve
--field-button-widthplus--field-drop-padon each side (default 5, so 18 + 10 = 28). The drop V is centred in it, about 10 px from the right border (Windows 11 about 16, GNOME 13, macOS 6.5-12; centred in the bare 18 it was 5). A spin edit's buttons use--field-button-widthalone.The text fields' side padding: in the bundled themes (light, auto, dark, system, green) the edit (and what is built on it: the combo edit, the mask edit, the numeric, currency and float spin edits and so on), the combo box family, the spin edit, the cascader and the date picker all take
padding: var(--pad-field), default4px 9px(8px 12pxat modern density). With the 1 px border the text starts 10 px from the outer edge, as far as the drop V keeps from the right one (they used--pad-control's 4 px, so the text started at 5 px; the date picker used--pad-datetimeand started at 7). One token in:rootadjusts every field; check boxes, tabs and menus still read--pad-controland don't move. The built-in skins keep their own field padding.Token names follow the
--glyph-<kind>convention, and internal glyph drawing funnels through one overridable entry point, so the remaining glyph slots (calendar paging and so on) can each be wired up with one line if ever needed; nobody skins those internal glyphs in practice, so they are not wired yet. Example:--glyph-check: "Segoe MDL2 Assets" "\e73e";. Unset slots use the built-in vector glyphs. Note: once a valid override is set, the glyph always renders through the icon font (even if that font/code point renders blank); only an unset or malformed token falls back to the vector glyph. -
render-stylefamily presets (v3): the per-control propertyrender-style: flat | bevel3d | inset3d;. Withbevel3d/inset3d,DrawFrameautomatically applies the raised/recessed two-color 3D bevel + square corners + the default border width (2) + a border color derived from the (solid) background when unset, so you do not repeatborder-style/border-width/border-color/border-radiuson every control.flat(the default) is the original behavior. Applies only to control frames drawn throughDrawFrame(buttons / inputs / panels / progress tracks / forms and so on), and to tabs (TyTab), scroll thumbs (TyScrollThumb) and track thumbs and grooves (TyTrackThumb/TyTrackGroove); other separately drawn sub-parts such as the check box indicator are unaffected. For an example skin seeexamples/theming/classic.tycss(classic 3D style).