Skip to content

Styling components

The global tokens move every component at once. This page is about moving one — one component, one variant, one instance — using the three mechanisms every component exposes, in the order you should reach for them:

  1. Propsvariant, tone, size. If the look you want is a supported combination, it is already there.
  2. Theming knobs — per-component custom properties like --forte-button-radius, documented in the Theming table on each component's page.
  3. Your own CSS — targeting the stable data-forte part markers and data-* state attributes. The escape hatch with no ceiling.

Theming knobs

Every component declares its knobs on its own root element, each defaulting to a semantic token — --forte-button-radius: var(--forte-radius-control). Override one and only that component changes; leave it alone and the component keeps following the global token.

styling/knobs.tsx

The full knob list for each component is generated from its stylesheet and rendered on its page — Button's, for example.

Parts and states

CSS Modules hashes the library's class names, so they change between releases and are not yours to target. What is stable — and public API, covered by semver — are the data attributes:

  • data-forte names every part. The root element carries data-forte="button", data-forte="select"; inner parts carry data-forte="select-trigger", data-forte="dialog-title", and so on for every element a component renders.
  • data-* carries every state. data-variant, data-tone, data-size, data-disabled, data-loading, data-checked, plus whatever Base UI publishes for the primitive — open states, placement, validity.

Combined with the fact that everything the library ships sits in @layer forte.* — and layered rules lose to your unlayered CSS regardless of specificity — plain CSS can restyle anything, no !important, no specificity contest:

styling/parts.tsx

The same selectors work as Tailwind arbitrary variants — data-[variant=solid]:shadow-2 — and in styled-components, vanilla-extract, or anything else that emits CSS.

/* every select trigger, but only while its popup is open */
[data-forte="select-trigger"][data-popup-open] {
  border-color: var(--forte-color-primary-border);
}

Write your values as tokens (var(--forte-shadow-2), not a literal) and your customisations keep responding to the seed, the presets and dark mode like the library's own styles do.

The tap highlight on touch

Safari and Chrome paint a translucent grey box over whatever a touch lands on. It ignores border-radius, so it squares off a pill button and a rounded card; it covers the whole element rather than the part you pressed; and it lingers after your finger lifts, so a tapped control sits greyed while the next screen is already arriving.

The library suppresses it on every part it renders — the rule is one line in patterns.css, keyed off the data-forte marker, and the property inherits so it reaches the icons and labels inside each part too. Nothing to configure, and no per-component opt-in.

It stops there on purpose. Suppressing the highlight is only safe where something else signals the press, and the library can promise that for its own parts — Button's :active fill, the Tabs indicator, the Switch thumb, the highlighted row in every menu — but not for markup it did not write. So your own buttons, cards and links keep the platform behaviour until you ask for otherwise.

To take the same treatment app-wide, import the optional reset and switch it on with a class:

import "@forte-ui/react/theme.css";
import "@forte-ui/react/styles/reset.css";
<html class="forte-reset">

The class is what activates it — the import alone does nothing. Put it on <html> for the whole document, or on any element to cover just that subtree. The reset also sets box-sizing: border-box, and everything in it sits in forte.reset, the lowest of the library's layers, so your own CSS beats it without !important.

What you take on

The library's defaults carry guarantees your CSS can silently defeat, because winning the cascade means winning against the accessibility rules too:

When CSS keeps repeating itself

If you find the same part-selector rule in project after project — every app adds a resting shadow to solid buttons, say — that is the signal it should be a knob. Open an issue: adding one is a single declaration plus a doc comment in the component's stylesheet, and it lands in the generated theming tables automatically.