Styling forty-cdk
forty-cdk ships no styles. Every primitive exposes state, ARIA, focus management, and keyboard behavior; the visual design is entirely yours. This guide explains the three hooks you style against, so the appearance is yours while the behavior stays the library's.
If you are styling an overlay (Popover, Dialog, Menu, …) start with Your first overlay, which walks one from empty markup to styled-and-animated. This page is the conceptual umbrella underneath it.
Copying an example from these docs: every example styles itself with
--ex-*properties that always carry a literal fallback (var(--ex-accent, #0e7c6b)), so the file renders on its own the moment you paste it, and defining that same--ex-*set on your:rootre-themes every example you have copied.
The three styling hooks
1. Your own class — not the directive selector
A primitive's selectors ([forAccordion], [forDialogTrigger], …) are its behavior
API: what you attach to get the wiring. They are not a styling contract. Add your
own class to each piece and style that:
<div forAccordion class="accordion">
<div forAccordionItem class="accordion__item">
<h3>
<button forAccordionTrigger class="accordion__trigger">Shipping</button>
</h3>
<section forAccordionContent class="accordion__panel">…</section>
</div>
</div>
.accordion__trigger {
/* your styles */
}
You can technically write [forAccordion] { … } (it is a valid attribute selector),
but don't:
- Selectors can change. forty-cdk is pre-1.0 and renames are in scope; a CSS file
keyed on
[forAccordionTrigger]breaks silently when the selector moves. Your own class survives. - Separation of concerns. The attribute says "this has behavior X"; the class says "this looks like Y". Keeping them distinct keeps templates readable and your design decoupled from the library's internals.
- Some pieces have no attribute to target. Overlays opened through a programmatic
manager (see below) or rendered inside the library's own view (Toast) don't expose the
[for…]attribute in the DOM at all, so a class is the only reliable hook there.
2. data-* attributes — for state
The library reflects logical state onto every piece you might want to style, as data-*
attributes. This is how you style state, not by reading signals or by toggling
classes yourself.
.accordion__trigger[data-state='open'] .chevron {
transform: rotate(180deg);
}
.accordion__panel[data-state='closed'] {
display: none;
}
Canonical data-state vocabulary (three families, used uniformly across pieces):
| Values | Meaning | Examples | |
|---|---|---|---|
open | closed | expand / collapse | Accordion, Disclosure, Tooltip, Popover, Dialog, Menu, Drawer, Tree parents | |
active | inactive | one-of-N selectable in a tablist | Tabs trigger / content | |
checked | unchecked | indeterminate | form-control state | Switch, Checkbox, Radio, Listbox / Select / Combobox / Menu items |
A few primitives use a different attribute because their spec doesn't fit the three families. These are intentional, not inconsistencies:
| Attribute | Values | Primitive(s) | |
|---|---|---|---|
data-state | visible | hidden | Scroll Area scrollbar / thumb | |
data-status | idle | loading | loaded | error | Avatar | |
data-state | indeterminate | loading | complete | Progress | |
data-quality | optimum | sub-optimum | even-less-good | Meter | |
data-selected, data-today, data-outside-month | present / absent (boolean) | Calendar gridcell | |
data-selected | present / absent (boolean) | Tree treeitem |
Boolean data-* attributes (data-disabled, data-readonly, data-highlighted,
data-selected, …) are present with an empty value when true, absent when false,
never data-disabled="false". So style the "off" state by selecting on absence:
.option[data-highlighted] {
background: #eef;
} /* keyboard-focused candidate */
.trigger:not([data-disabled]) {
cursor: pointer;
}
data-highlighted is the roving-tabindex / aria-activedescendant "current candidate"
hook. For Combobox it is the only way to style the active option, since focus stays on
the input.
ARIA attributes follow a parallel rule. Togglable widgets always emit
aria-checked/aria-pressed/aria-expanded/aria-selected as "true"/"false", but
truthy-only attributes (aria-disabled, aria-required, …) are absent when false. Style
falsy state via :not([aria-disabled]), never [aria-disabled='false'].
Axis & direction. Primitives with directional keyboard nav reflect data-orientation
(horizontal/vertical) on the root so you can flip layout:
.toolbar[data-orientation='vertical'] {
flex-direction: column;
}
3. CSS custom properties — for measured values
When the library computes a value your CSS needs (a percentage, an anchor dimension, a
drag delta), it writes it as a --for-* custom property you consume:
.progress__indicator {
width: var(--for-progress-percentage);
}
.popover {
max-width: var(--for-floating-available-width);
}
Each primitive's README lists the exact properties it sets, under CSS custom properties. Floating overlays additionally expose anchor dimensions and a transform origin. See Styling floating content.
Overlays portal to document.body → use global CSS
Every overlay that floats (Popover, Tooltip, Menu, Select, …) and every modal (Dialog,
Drawer) moves its content to document.body when open, and the Toast viewport moves
itself there once it mounts. Component-scoped styles
(Angular view encapsulation) don't reach a portaled node. Put overlay styles in a
global stylesheet (or use ::ng-deep sparingly) and target your class there.
The full set of rules for positioned content lives in
Styling floating content: animate.enter and animate.leave
both work, the positioner owns translate while transform / scale / rotate stay yours,
never set position/top/left, and the arrow recipe.
Programmatic overlays → class / classList on the config
Dialog, Drawer, and Toast can be opened imperatively through a manager
(ForDialogManager.open(), ForDrawerManager.open(), ForToastManager.show()). The
manager creates the overlay host for you, so there is no template element to add a class
to. Pass class (or classList) on the open/show config and the tokens land on the real
overlay root alongside data-state / data-side:
this.dialogs.open(ConfirmDialog, { data, class: 'dialog dialog--danger' });
Motion & internationalization
prefers-reduced-motion. The library never animates for you, so honoring reduced motion is your CSS's job. Guard transitions/animations:@media (prefers-reduced-motion: reduce) { .accordion__panel { transition: none; } }RTL. A primitive's
dirinput resolves keyboard meaning and reflects the resolved value back to the host's nativedirattribute. For visual RTL setdiron an ancestor (the standard<html dir="rtl">). Everydir-aware primitive inherits it. Style direction-sensitive layout with:dir(rtl)or[dir='rtl'], and prefer logical properties (margin-inline-start,inset-inline-end) so layout flips for free.
Per-primitive styling reference
Each primitive's README has a Styling section listing its pieces, the data-* it
reflects, and any CSS custom properties. Grouped by how you style them:
Expand / collapse & tabs
State is data-state (open/closed, or active/inactive for Tabs); flip layout off
data-orientation. Content is never [hidden], so you gate it with @if or hide closed
panels in CSS.
- Accordion · Disclosure · Tabs · Tree
Form controls
State is data-state (checked/unchecked/indeterminate); also data-disabled /
data-readonly. The host is a real <button> so :disabled / :focus-visible work too.
- Switch · Checkbox · Toggle · Radio Group
Text & value inputs
Style off data-disabled / data-readonly / data-empty, validation facets
(data-invalid, data-touched, …) on Field, and segment facets (data-highlighted,
data-placeholder) on the date/time fields and Calendar cells.
- Input · Number Input · OTP Input · Date Field · Time Field · Field · Fieldset · Calendar
Range & value display
Paint the bar/fill from a --for-*-percentage custom property; color by data-state
(Progress) or data-quality (Meter).
Trigger-anchored overlays (portal + floating-ui)
Global CSS, animate.enter and animate.leave, anchor/origin custom properties, data-state
open/closed. See Styling floating content and, for
menu checkmark alignment, Selected-indicator alignment.
- Popover · Tooltip · Hover Card · Dropdown Menu · Context Menu · Menu · Menubar · Navigation Menu · Select · Combobox
Modal overlays
Global CSS; declaratively class the surface, programmatically pass class on the config.
Both expose data-state; Drawer adds data-side / drag state.
Inline selection list & programmatic toast
- Listbox: inline (not portaled);
roving tabindex,
data-orientation, options carrydata-state+data-highlighted. - Toast: rendered by the library's
viewport, which moves itself to
document.body, so style its pieces via global attribute selectors;data-variant,data-swipe,data-paused.
Layout & display
- Date Picker: overlay composing a projected Calendar; floating rules apply.
- Separator · Aspect Ratio · Avatar · Scroll Area · Pane Resizer · Toolbar