forty-cdk
llms.txt
Guides

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 :root re-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):

ValuesMeaningExamples
open | closedexpand / collapseAccordion, Disclosure, Tooltip, Popover, Dialog, Menu, Drawer, Tree parents
active | inactiveone-of-N selectable in a tablistTabs trigger / content
checked | unchecked | indeterminateform-control stateSwitch, 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:

AttributeValuesPrimitive(s)
data-statevisible | hiddenScroll Area scrollbar / thumb
data-statusidle | loading | loaded | errorAvatar
data-stateindeterminate | loading | completeProgress
data-qualityoptimum | sub-optimum | even-less-goodMeter
data-selected, data-today, data-outside-monthpresent / absent (boolean)Calendar gridcell
data-selectedpresent / 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 dir input resolves keyboard meaning and reflects the resolved value back to the host's native dir attribute. For visual RTL set dir on an ancestor (the standard <html dir="rtl">). Every dir-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.

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.

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.

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.

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 carry data-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