Skip to content

CSS API

Everything in components/folder-tabs/folder-tabs.css: eight custom properties, ten classes, two state attributes. This is the complete surface — there is nothing else to learn.

Custom properties

All eight are defined on :root and overridden for dark mode. They are the only colours in the stylesheet.

PropertyLightDarkPaints
--tab-railoklch(0.20 0.02 260)oklch(0.17 0.02 260)The rail behind the tabs
--tab-rail-filloklch(0.50 0.03 260)oklch(0.26 0.02 260)An inactive tab; the arrow buttons
--tab-rail-inkoklch(0.96 0 0)oklch(0.93 0 0)An inactive tab's label; the arrow chevrons
--tab-paneloklch(1 0 0)oklch(0.40 0 0)The panel and the active tab
--tab-panel-inkoklch(0.145 0 0)oklch(0.985 0 0)Panel text; the active tab's label
--tab-edgeoklch(0.20 0.02 260)oklch(0.70 0 0)The stroke around every tab; the panel border
--tab-ringoklch(0.98 0 0)oklch(0.95 0 0)Focus outline on an inactive tab and on the arrows
--tab-ring-on-paneloklch(0.30 0 0)oklch(0.95 0 0)Focus outline on the active tab and on the panel

Required relationships

These are constraints, not preferences. See The Mechanic and Theming.

RuleWhy
--tab-panel is the lightest of the three fills, in every themeIt is the panel; if it is darker than the tab fill, selection reads as inverted
--tab-rail is the darkest of the three fillsIt is the ground everything stands on
--tab-rail-ink ≥ 4.5:1 on --tab-rail-fillWCAG 1.4.3, normal text
--tab-panel-ink ≥ 4.5:1 on --tab-panelWCAG 1.4.3, normal text
Every boundary clears 3:1 by fill or by --tab-edgeWCAG 1.4.11, component boundary. Light theme passes by fill, dark theme by edge — either route satisfies it
--tab-ring ≥ 3:1 on --tab-rail-fillWCAG 1.4.11, focus indicator
--tab-ring-on-panel ≥ 3:1 on --tab-panelWCAG 1.4.11, focus indicator

Theme selectors

Dark values are applied by all three of the common strategies, so the control follows whichever one your app already uses:

SelectorWhen it applies
.darkA class toggled on a wrapper or html
[data-theme="dark"]A data attribute toggled on a wrapper or html
@media (prefers-color-scheme: dark) scoped to :root:not(.light):not([data-theme="light"])The OS preference, unless an explicit light choice has been made

The :not() guards make an explicit choice beat an inferred one: a user who picked light mode in your UI gets light mode even on a dark-mode OS.

Fallbacks and environment blocks

BlockWhat it does
@supports not (color: oklch(0 0 0))Redefines all eight variables as sRGB hex, light and dark. Generated — do not hand-edit
@media (prefers-reduced-motion: reduce)Removes the tab transition and the smooth scroll
@media (forced-colors: active)Swaps to system colours: ButtonFace/ButtonText, Highlight on the active tab, GrayText for disabled, a real ButtonBorder in place of the drop-shadow stroke
@media printHides the arrows, sets the strip to overflow: visible, keeps hidden panels hidden

Classes

Structure

ClassOnWhat it does
.fldr-railThe role="tablist" elementThe dark band. Flex row, align-items: flex-end, padding: 4px 4px 0, border-radius: 8px 8px 0 0
.fldr-scrollA div inside the railThe scrolling strip. flex: 1 1 auto, min-width: 0, overflow-x: auto, gap: 3px, hidden scrollbar
.fldr-tabEach button[role="tab"]A trapezoid tab. clip-path polygon, edge stroke via drop-shadow, flex: 0 0 auto, white-space: nowrap
.fldr-countA span inside a tab labelA quiet pill for a number. color-mix(in oklch, currentColor 18%, transparent), tabular numerals
.fldr-arrowsA div inside the rail, after the stripThe arrow group. display: none until data-overflowing="true"
.fldr-arrowEach arrow buttonA 20×20 square button with a 1px solid var(--tab-edge) border
.fldr-panelEach role="tabpanel" elementThe sheet. --tab-panel ground, 1px solid var(--tab-edge) on three sides, border-top: 0, padding: 16px

Modifiers

ClassApplied withEffect
.fldr-panel-flush.fldr-panelTightens padding to 14px 14px 4px. Keeps the ground and the side walls — for content that brings its own card
.fldr-nestedThe control's outermost elementRenders the whole control one size down: rail padding 3px 3px 0, radius 6px, tab font 12px, panel padding 14px. Same fills
.fldr-nested-rail.fldr-rail, without a .fldr-nested ancestorThe nested size only, applied to a standalone strip. Does not touch any panel

.fldr-nested styles descend through > .fldr-rail and > .fldr-panel, so a nested control never restyles a control nested inside it twice over.

State attributes

The stylesheet reads exactly two attributes. Whatever sets them — the React build, the vanilla build, or your own code — this is the entire contract.

AttributeOnValuesEffect
data-state.fldr-tab"active" | "inactive""active" gives the tab --tab-panel as its background, --tab-panel-ink as its colour, font-weight: 600, and 3px more top padding so it stands proud of the rail
data-overflowing.fldr-arrows"true" | "false""true" switches the arrow group from display: none to display: flex

Two more states are styled without being part of that contract:

SelectorEffect
.fldr-tab:disabled, .fldr-tab[aria-disabled="true"]opacity: 0.45, cursor: not-allowedboth spellings, styled identically
.fldr-tab:focus-visible2px solid var(--tab-ring), outline-offset: -3px
.fldr-tab[data-state="active"]:focus-visibleSame, but outline-color: var(--tab-ring-on-panel)
.fldr-panel:focus-visible2px solid var(--tab-ring-on-panel), outline-offset: -2px

The two disabled spellings look the same on purpose but behave differently: disabled removes the tab from the page, while aria-disabled="true" keeps it focusable so a screen reader user can find it and be told why it is unavailable. WAI-ARIA prefers the second. See two kinds of disabled.

WARNING

data-state drives the CSS. aria-selected drives assistive technology. They are two different audiences reading the same fact, and if you are setting state yourself you must set both. See Accessibility.

Selectors you may want to override

SelectorCurrentlyNote
.fldr-tabfont-size: 13px; font-weight: 500font: inherit first, so it picks up your family
.fldr-tab[data-state="active"]font-weight: 600The weight change is a bonus signal, not the mechanic
.fldr-tab:hover:not(:disabled):not([aria-disabled="true"]):not([data-state="active"])color-mix(...) 12% toward the inkThe active tab has no hover state on purpose — it is already selected
.fldr-tab:disabled, .fldr-tab[aria-disabled="true"]opacity: 0.45Do not raise this. The low contrast is the disabled signal
.fldr-panelpadding: 16pxPrefer .fldr-panel-flush over overriding this
.fldr-arrow > svgwidth: 14px; height: 14pxSized here so any 24×24 viewBox icon fits

Two implementation details

The tab has no border. clip-path cuts a border off along the diagonal edge, so the stroke is three zero-blur drop-shadow() filters instead — left, right, and top:

css
filter: drop-shadow(1px 0 0 var(--tab-edge)) drop-shadow(-1px 0 0 var(--tab-edge))
  drop-shadow(0 -1px 0 var(--tab-edge));

There is deliberately no bottom shadow. That absence is the join.

The trapezoid is a real polygon.

css
clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 100%, 0 100%);

Ten pixels of slope on the right-hand edge, cut the way a card index is cut. Not a rounded rectangle standing in for one.