Styling
Design-system neutral by default
Components ship without decorative visual opinions. The default appearance mirrors the UA stylesheet; buttons look like buttons, inputs look like inputs. This lets them blend into any design system without fighting existing styles.
What components always emit is structural: layout, geometry, and interaction state indicators. The decorative layer is entirely up to you.
Color scheme
Components consume CSS system colors (Canvas, CanvasText, ButtonFace,
Highlight, etc.), which the browser resolves differently in light and dark
schemes. Set color-scheme on a container and every component inside it
adapts automatically:
.my-app {
color-scheme: dark;
}
No class-based theming or JavaScript needed for basic color scheme support.
Global design tokens
The optional base.css export defines semantic tokens that all components
reference. Import it once at your application root to enable the token layer:
@import '@rcarls/rc-webcomponents/themes/base.css';
These tokens fall back to CSS system colors without the import; the file is optional. Override any token at your theme boundary to adjust all components at once:
:root {
--rc-accent: oklch(55% 0.2 250); /* primary action color */
--rc-border-color: oklch(75% 0 0); /* all component borders */
--rc-control-block-size: 2.5rem; /* input / button height */
--rc-control-radius: 0.5rem; /* border radius */
--rc-font-family: 'Inter', sans-serif;
}
The semantic layer is deliberately smaller than the sum of every component's styling API. Components consume these shared decisions and expose narrower component tokens where their structure needs an additional adaptation point.
| Role | Tokens and defaults |
|---|---|
| Surfaces | --rc-surface: Canvas; --rc-field: Field; --rc-button-bg: ButtonFace |
| Text | --rc-field-text: FieldText; --rc-button-text: ButtonText; --rc-highlight-text: HighlightText; --rc-text-disabled: GrayText |
| Accent and selection | --rc-accent: Highlight (AccentColor when supported); --rc-highlight: Highlight |
| Border and elevation | --rc-border-color: ButtonBorder; --rc-border-width: 1px; --rc-border-style: solid; --rc-border: var(--rc-border-width) var(--rc-border-style) var(--rc-border-color); --rc-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) |
| Type | --rc-font-family: inherit; --rc-font-size: 1em; --rc-font-weight: 400; --rc-line-height: normal |
| Controls | --rc-control-block-size: 2.25em; --rc-control-padding-block: 0.25em; --rc-control-padding-inline: 0.5em; --rc-control-gap: 0.25em |
| Items | --rc-item-padding-block: 0.3em; --rc-item-padding-inline: 0.75em; --rc-item-gap: 0.4em |
| Item type | Unset by default, so items inherit their text style. --rc-item-font-family, --rc-item-font-size, --rc-item-font-weight, --rc-item-line-height, --rc-item-letter-spacing, and --rc-item-icon-size style menu, listbox, and adaptive-menu items. |
| Shape | --rc-radius-sm: 0.125em; --rc-radius-md: 0.25em; --rc-control-radius: var(--rc-radius-sm) |
| Popups | --rc-popup-background: var(--rc-surface); --rc-popup-color: var(--rc-field-text); --rc-popup-border: var(--rc-border); --rc-popup-radius: var(--rc-radius-sm); --rc-popup-shadow: var(--rc-shadow); --rc-popup-padding-block: var(--rc-control-padding-block). Menus, adaptive-menu popups, popovers, and select and combobox listboxes read these before their own fallbacks. |
| Focus and disabled | --rc-focus-ring: auto; --rc-focus-ring-offset: 0; --rc-disabled-opacity: 0.5 |
| Effects motion | --rc-motion-effects-duration-fast: 80ms; --rc-motion-effects-duration-default: 120ms; --rc-motion-effects-duration-slow: 200ms; matching --rc-motion-effects-easing-fast, --rc-motion-effects-easing-default, and --rc-motion-effects-easing-slow default to ease-out |
| Spatial motion | --rc-motion-spatial-duration-fast: 180ms; --rc-motion-spatial-duration-default: 300ms; --rc-motion-spatial-duration-slow: 450ms; matching --rc-motion-spatial-easing-fast, --rc-motion-spatial-easing-default, and --rc-motion-spatial-easing-slow default to cubic-bezier(0.2, 0, 0, 1) |
| Directional easing | --rc-motion-spatial-easing-enter: cubic-bezier(0, 0, 0, 1); --rc-motion-spatial-easing-exit: cubic-bezier(0.3, 0, 1, 1); --rc-motion-effects-easing-enter: ease-out; --rc-motion-effects-easing-exit: ease-in. No matching enter/exit duration tokens; pair -easing-exit with -duration-fast. |
| Compatibility motion | --rc-motion-duration: var(--rc-motion-effects-duration-default) |
Set the popup and item type tokens at the theme root rather than on individual
elements. The same applies to a theme's menu and adaptive-menu tokens.
Custom properties inherit into shadow roots, so a menu rendered inside another
component's shadow root, where element selectors such as .my-theme rc-menu
cannot reach, still gets the theme's popup surface.
Per-component tokens
Each component also exposes its own scoped tokens for fine-grained control. These are documented in the API table on each component page. Example:
/* Make the FAB match your brand */
rc-fab {
--rc-fab-bg: oklch(55% 0.2 250);
--rc-fab-color: white;
--rc-fab-radius: 0.75rem;
--rc-fab-shadow: 0 4px 12px oklch(55% 0.2 250 / 40%);
}
Some public tokens intentionally form a small family across related
components. --rc-thumb-radius aligns slider and range-slider thumbs;
--rc-fab-* tokens let rc-fab-menu reuse the FAB trigger contract. A few
composition tokens are deliberate pass-throughs: rc-bottom-sheet forwards
--rc-dialog-scrim, and rc-fab-menu forwards
--rc-menu-button-popup-z-index. These contracts are documented by every
component that consumes them and enforced by the architecture audit.
CSS parts
Components expose named parts for styling elements that are inside shadow
DOM. Use the ::part() selector:
/* Style the trigger button inside rc-select */
rc-select::part(trigger) {
font-weight: 500;
letter-spacing: 0.01em;
}
/* Style the toggle indicator */
rc-select::part(toggle-indicator) {
color: var(--rc-accent);
}
Available parts are listed in the API table under the Parts section on each component's page.
Styling hooks and ownership
Use the narrowest public styling surface that expresses the change:
- Override a shared semantic token at a real theme ancestor when the decision should flow through nested light and shadow trees.
- Override a component token on one component when changing a documented adaptation point such as geometry, color, or typography.
- Use a CSS part when styling a shadow-owned element requires selectors or declarations that cannot be represented by one token.
- Use a documented
data-rc-*marker when consumer-authored light-DOM content must declare a structural or behavioral role. - Use a theme modifier class or theme-specific data attribute only for an optional visual recipe supplied by that theme.
The data-rc-* prefix identifies ownership, not visibility. Documented markers
such as data-rc-button-icon, data-rc-field-control, and
data-rc-bottom-sheet-handle are author-facing contracts. Other markers are
written by components or shared controllers for measurement, coordination, or
style deduplication and are not consumer APIs. Do not depend on a marker unless
the component or theme documentation explicitly exposes it.
Prefer native or ARIA state selectors such as [open], :checked,
[aria-selected], and [aria-current]. Components expose documented host
data-* state when the authoritative state cannot otherwise be selected by a
theme. Theme CSS may style that state, but it must not use CSS to create a
second behavioral source of truth.
Public tokens, parts, and documented markers are versioned API. The repository architecture audit tracks their usage along with private theme overrides and ID-selector complexity. Net growth requires a documented consumer need and an explicit budget rationale; prefer consolidating an existing hook over creating an overlapping one.
Cascade layers
Shadow-DOM component styles are scoped by the shadow tree and do not share the
document's layer order. Components that must install structural light-DOM CSS
place it in the rc-base layer and deduplicate it per document or shadow root.
Consumer rules outside a layer therefore override these structural defaults
without selector specificity contests.
Each packaged theme declares its ordered layers after rc-base:
@layer rc-base, rc-theme.defaults, rc-theme.bridge, rc-theme.native,
rc-theme.components, rc-theme.css-components, rc-theme.overrides;
Naming rc-base first keeps component structural CSS below the theme even though
components inject it at runtime, after the theme stylesheet has loaded. A layer is
ordered by its first declaration, so without this an injected rc-base would become
the last and strongest layer. An application that declares its own layer order should
also list rc-base before the theme layers.
The real names include the theme package, such as
rc-theme-material.components. Defaults own the theme's raw source tokens,
the bridge maps those tokens to shared and component RC tokens. The opt-in
native layer is a convenience for styling ordinary HTML controls throughout
an application without composing every control from RC elements. Component
recipes can style native children when their presentation belongs to a themed
menu, toolbar, or other component context. components holds element-specific
recipes, parts, contextual selectors, and visual variants for registered RC elements.
The separate opt-in css-components layer styles native elements carrying documented
classes such as .rc-button, .rc-chip, .rc-select, .rc-card, and .rc-list,
plus details.rc-disclosure. These recipes provide appearance and CSS-visible states
only; they do not add behavior, events, popup replacement, or measured overflow.
Overrides hold the few contextual rules that must beat every component
variant, such as rc-adaptive-menu overflow rows, which restyle a moved rc-button
or rc-menu-button as a menu row whatever its own variant. Normal application rules
outside a layer outrank all theme layers, which provides a straightforward
consumer override path. Avoid routine
!important: important declarations reverse layer precedence.
This is also why a packaged theme's class belongs on a container, as shown
below, not on <html>. The shared base.css declares no layer at all, so
its :root values act as normal application rules and outrank every theme
layer for that same reason. On a container, a theme's own value is a direct
declaration that beats an inherited one regardless of layer; on <html>,
:root and the theme class target the same element, and :root's
layer-free value wins outright.
Native and CSS-only recipes
The native entrypoint keeps browser behavior and form semantics intact. The CSS-only recipes add appearance and CSS-visible states to ordinary HTML; they do not register elements or replace popups. Load a theme's defaults and bridge when you want its token values, then opt into the two layers:
@import '@rcarls/rc-theme-material/defaults.css';
@import '@rcarls/rc-theme-material/bridge.css';
@import '@rcarls/rc-theme-material/native.css';
@import '@rcarls/rc-theme-material/css-components.css';
native.css fills in controls that a component recipe does not style, including
consumer-authored controls inside toolbars, dialogs, cards, and rich popup
content. A component's themed presentation takes precedence for controls it
styles itself, such as menu rows and triggers. The boundary follows the
control's role in that component rather than every descendant of an RC host.
Add data-rc-native="off" to an element to opt it and its descendants out of
the native layer, for example when embedding a separately styled widget.
The same entrypoints exist for the Substrate theme README, Material theme README, Tailwind theme README, and Win31 theme README. Supported class recipes use native elements directly:
<main class="rc-theme-material">
<button class="rc-button" type="button">Save</button>
<label class="rc-chip"><input type="checkbox" /> Filter</label>
<div class="rc-chip-group"><button class="rc-chip" type="button">One</button></div>
<label for="css-choice">Choice</label>
<select id="css-choice" class="rc-select">
<option>Choose one</option>
</select>
<textarea class="rc-textarea" aria-label="Notes"></textarea>
<article class="rc-card">Card content</article>
<ul class="rc-list">
<li>
<label><input type="checkbox" checked /> Selected item</label>
</li>
</ul>
<details class="rc-disclosure">
<summary>More</summary>
<p>Details</p>
</details>
</main>
Use a[href].rc-button or a[href].rc-chip for links. A checkbox or radio
inside .rc-chip remains in the document and is visually concealed only by
the recipe, so its label, keyboard focus, and form behavior remain native.
Theme packages
Each theme package maps its design system onto the --rc-* tokens and parts above:
- Substrate, the lightweight reference theme
- Tailwind CSS v4
- Material
- Windows 3.1
See Adaptive layout for geometry contracts that change with application state, and Upgrading for token renames between releases.