|
|
/* ── Provider — variant-shared base ─────────────────────────────────────── */
|
|
|
|
|
|
[data-tabs] {
|
|
|
--_tabs-trigger-height: var(--tabs-trigger-height-md);
|
|
|
--_tabs-trigger-padding-inline: var(--tabs-trigger-padding-inline-md);
|
|
|
--_tabs-trigger-gap: var(--tabs-trigger-gap-md);
|
|
|
--_tabs-trigger-font-size: var(--tabs-trigger-font-size-md);
|
|
|
--_tabs-content-padding: var(--tabs-content-padding-md);
|
|
|
--_tabs-stack-gap: var(--tabs-stack-gap);
|
|
|
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--_tabs-stack-gap);
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-orientation='vertical'] {
|
|
|
flex-direction: row;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
/* ── Sizes ──────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-tabs][data-size='xs'] {
|
|
|
--_tabs-trigger-height: var(--tabs-trigger-height-xs);
|
|
|
--_tabs-trigger-padding-inline: var(--tabs-trigger-padding-inline-xs);
|
|
|
--_tabs-trigger-gap: var(--tabs-trigger-gap-xs);
|
|
|
--_tabs-trigger-font-size: var(--tabs-trigger-font-size-xs);
|
|
|
--_tabs-content-padding: var(--tabs-content-padding-xs);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-size='sm'] {
|
|
|
--_tabs-trigger-height: var(--tabs-trigger-height-sm);
|
|
|
--_tabs-trigger-padding-inline: var(--tabs-trigger-padding-inline-sm);
|
|
|
--_tabs-trigger-gap: var(--tabs-trigger-gap-sm);
|
|
|
--_tabs-trigger-font-size: var(--tabs-trigger-font-size-sm);
|
|
|
--_tabs-content-padding: var(--tabs-content-padding-sm);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-size='lg'] {
|
|
|
--_tabs-trigger-height: var(--tabs-trigger-height-lg);
|
|
|
--_tabs-trigger-padding-inline: var(--tabs-trigger-padding-inline-lg);
|
|
|
--_tabs-trigger-gap: var(--tabs-trigger-gap-lg);
|
|
|
--_tabs-trigger-font-size: var(--tabs-trigger-font-size-lg);
|
|
|
--_tabs-content-padding: var(--tabs-content-padding-lg);
|
|
|
}
|
|
|
|
|
|
/* ── List ───────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-tabs-list] {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
gap: var(--tabs-list-gap);
|
|
|
min-width: 0;
|
|
|
/* border-box so a `fitted` list's intrinsic padding (pills / segmented track)
|
|
|
is absorbed INSIDE width:100% — without it the band overflows its column by
|
|
|
the padding (the right-edge asymmetry inside a popover). */
|
|
|
box-sizing: border-box;
|
|
|
background: var(--tabs-list-bg);
|
|
|
}
|
|
|
|
|
|
[data-tabs-list][data-orientation='vertical'] {
|
|
|
flex-direction: column;
|
|
|
flex: 0 0 var(--tabs-list-width);
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
/* ── Trigger ────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-tabs-trigger] {
|
|
|
position: relative;
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: var(--_tabs-trigger-gap);
|
|
|
min-height: var(--_tabs-trigger-height);
|
|
|
padding-inline: var(--_tabs-trigger-padding-inline);
|
|
|
border: var(--tabs-trigger-border-width) solid var(--tabs-trigger-border);
|
|
|
border-radius: var(--tabs-trigger-radius);
|
|
|
background: var(--tabs-trigger-bg);
|
|
|
color: var(--tabs-trigger-fg);
|
|
|
font-family: var(--tabs-trigger-font-family);
|
|
|
font-size: var(--_tabs-trigger-font-size);
|
|
|
font-weight: var(--tabs-trigger-font-weight);
|
|
|
line-height: var(--tabs-trigger-line-height);
|
|
|
letter-spacing: var(--tabs-trigger-letter-spacing);
|
|
|
text-decoration: none;
|
|
|
cursor: pointer;
|
|
|
transition:
|
|
|
background var(--tabs-trigger-transition-duration) var(--tabs-trigger-transition-ease),
|
|
|
border-color var(--tabs-trigger-transition-duration) var(--tabs-trigger-transition-ease),
|
|
|
color var(--tabs-trigger-transition-duration) var(--tabs-trigger-transition-ease),
|
|
|
box-shadow var(--tabs-trigger-transition-duration) var(--tabs-trigger-transition-ease);
|
|
|
}
|
|
|
|
|
|
[data-tabs-trigger]:hover {
|
|
|
background-image: linear-gradient(var(--state-hover), var(--state-hover));
|
|
|
color: var(--tabs-hover-trigger-fg);
|
|
|
}
|
|
|
|
|
|
[data-tabs-trigger]:focus-visible {
|
|
|
/* Canonical focus ring — `outline` (HCM-safe, survives forced-colors, unlike the
|
|
|
2-layer box-shadow this replaced) with the shared focus tokens. Matches the
|
|
|
framework-wide focus sweep; on filled variants (pills / segmented) it hugs the
|
|
|
pill radius cleanly instead of reading as a heavy boxed input. */
|
|
|
outline: var(--tabs-trigger-focus-ring-width) solid var(--tabs-trigger-focus-ring-color);
|
|
|
outline-offset: var(--tabs-trigger-focus-ring-offset);
|
|
|
}
|
|
|
|
|
|
[data-tabs-trigger][data-disabled],
|
|
|
[data-tabs-trigger]:disabled {
|
|
|
cursor: default;
|
|
|
opacity: var(--tabs-disabled-opacity);
|
|
|
}
|
|
|
|
|
|
/* ── Content ────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-tabs-content] {
|
|
|
min-width: 0;
|
|
|
flex: 1 1 auto;
|
|
|
padding: var(--_tabs-content-padding);
|
|
|
border: var(--tabs-content-border-width) solid var(--tabs-content-border);
|
|
|
border-radius: var(--tabs-content-radius);
|
|
|
background: var(--tabs-content-bg);
|
|
|
color: var(--tabs-content-fg);
|
|
|
}
|
|
|
|
|
|
/* ── Variant: line ──────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-tabs][data-variant='line'] {
|
|
|
--_tabs-stack-gap: var(--tabs-line-stack-gap);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='line'] [data-tabs-list] {
|
|
|
padding: 0;
|
|
|
border-bottom: var(--tabs-list-border-width) solid var(--tabs-list-border);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='line'][data-orientation='vertical'] [data-tabs-list] {
|
|
|
border-bottom: none;
|
|
|
border-inline-end: var(--tabs-list-border-width) solid var(--tabs-list-border);
|
|
|
padding-inline-end: var(--tabs-line-list-padding-inline-end);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='line'] [data-tabs-trigger] {
|
|
|
border-radius: 0;
|
|
|
border-color: transparent;
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='line'] [data-tabs-trigger][data-state='active'] {
|
|
|
color: var(--tabs-active-trigger-fg);
|
|
|
}
|
|
|
|
|
|
/* In `line` variant the underline is the indicator's job (when present)
|
|
|
* — but if no indicator is rendered, fall back to a static border so the
|
|
|
* active state remains visible. The indicator's styles (below) override
|
|
|
* this when the consumer renders it. */
|
|
|
[data-tabs][data-variant='line']:not(:has([data-tabs-indicator]))
|
|
|
[data-tabs-trigger][data-state='active'] {
|
|
|
border-bottom-color: var(--tabs-active-trigger-border);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='line'][data-orientation='vertical']:not(:has([data-tabs-indicator]))
|
|
|
[data-tabs-trigger][data-state='active'] {
|
|
|
border-bottom-color: transparent;
|
|
|
border-inline-end-color: var(--tabs-active-trigger-border);
|
|
|
}
|
|
|
|
|
|
/* ── Variant: surface ───────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-tabs][data-variant='surface'] {
|
|
|
padding: var(--tabs-surface-provider-padding);
|
|
|
border: var(--tabs-provider-border-width) solid var(--tabs-provider-border);
|
|
|
border-radius: var(--tabs-provider-radius);
|
|
|
background: var(--tabs-provider-bg);
|
|
|
box-shadow: var(--tabs-surface-provider-shadow);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='surface'] [data-tabs-list] {
|
|
|
padding: var(--tabs-list-padding);
|
|
|
border: var(--tabs-list-border-width) solid var(--tabs-list-border);
|
|
|
border-radius: var(--tabs-list-radius);
|
|
|
background: var(--tabs-surface-list-bg);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='surface'] [data-tabs-trigger][data-state='active'] {
|
|
|
background: var(--tabs-active-trigger-bg);
|
|
|
color: var(--tabs-active-trigger-fg);
|
|
|
border-color: var(--tabs-active-surface-trigger-border);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='surface'] [data-tabs-content] {
|
|
|
border-color: var(--tabs-surface-content-border);
|
|
|
background: var(--tabs-surface-content-bg);
|
|
|
}
|
|
|
|
|
|
/* ── Variant: pills ─────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-tabs][data-variant='pills'] [data-tabs-list] {
|
|
|
padding: var(--tabs-list-padding);
|
|
|
border-radius: var(--tabs-list-radius);
|
|
|
background: var(--tabs-pills-list-bg);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='pills'] [data-tabs-trigger] {
|
|
|
border-radius: var(--tabs-pills-trigger-radius);
|
|
|
}
|
|
|
|
|
|
/* In pills, the indicator (when present) IS the active background.
|
|
|
* Without an indicator, fall back to a static active background. */
|
|
|
[data-tabs][data-variant='pills']:not(:has([data-tabs-indicator]))
|
|
|
[data-tabs-trigger][data-state='active'] {
|
|
|
background: var(--tabs-active-trigger-bg);
|
|
|
color: var(--tabs-active-trigger-fg);
|
|
|
border-color: var(--tabs-active-pills-trigger-border);
|
|
|
box-shadow: var(--tabs-active-pills-trigger-shadow);
|
|
|
}
|
|
|
|
|
|
/* When the indicator IS present in pills, the active trigger keeps a
|
|
|
* neutral bg but raises its color above the indicator's tint. */
|
|
|
[data-tabs][data-variant='pills']:has([data-tabs-indicator])
|
|
|
[data-tabs-trigger][data-state='active'] {
|
|
|
color: var(--tabs-active-trigger-fg);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='pills'] [data-tabs-content] {
|
|
|
padding-inline: 0;
|
|
|
padding-block: var(--tabs-pills-content-padding-block-start) 0;
|
|
|
border: none;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
/* ── Variant: segmented ──────────────────────────────────────────────────────
|
|
|
* iOS / Untitled-UI segmented control. A filled, recessed track holds equal
|
|
|
* segments; the active segment is a PRIMARY accent pill (`segmented-trigger-
|
|
|
* bg-active` = primary-solid + on-solid text + subtle shadow), NOT an accent
|
|
|
* border nor a near-invisible neutral pill — so it reads clearly in light AND
|
|
|
* dark, not as a focus-ringed box. Both the active bg and the on-solid text
|
|
|
* are themeable tokens (override `--tabs-active-segmented-trigger-bg` /
|
|
|
* `-color-active` to retint or restore a neutral pill). The trigger radius is
|
|
|
* CONCENTRIC with the track (`segmented-trigger-radius` = track radius − list
|
|
|
* padding). Pair with `fitted` for equal-width segments. */
|
|
|
|
|
|
[data-tabs][data-variant='segmented'] [data-tabs-list] {
|
|
|
gap: 0;
|
|
|
padding: var(--tabs-list-padding);
|
|
|
border-radius: var(--tabs-segmented-list-radius);
|
|
|
background: var(--tabs-segmented-list-bg);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='segmented'] [data-tabs-trigger] {
|
|
|
border-radius: var(--tabs-segmented-trigger-radius);
|
|
|
border-color: transparent;
|
|
|
}
|
|
|
|
|
|
/* Without an indicator, the active segment paints its own accent pill. */
|
|
|
[data-tabs][data-variant='segmented']:not(:has([data-tabs-indicator]))
|
|
|
[data-tabs-trigger][data-state='active'] {
|
|
|
background: var(--tabs-active-segmented-trigger-bg);
|
|
|
color: var(--tabs-active-segmented-trigger-fg);
|
|
|
box-shadow: var(--tabs-active-segmented-trigger-shadow);
|
|
|
}
|
|
|
|
|
|
/* With the indicator present, it IS the pill; the active trigger only raises
|
|
|
* its label to the on-solid colour above the accent indicator. */
|
|
|
[data-tabs][data-variant='segmented']:has([data-tabs-indicator])
|
|
|
[data-tabs-trigger][data-state='active'] {
|
|
|
color: var(--tabs-active-segmented-trigger-fg);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='segmented'] [data-tabs-content] {
|
|
|
padding-inline: 0;
|
|
|
padding-block: var(--tabs-pills-content-padding-block-start) 0;
|
|
|
border: none;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
/* ── Indicator ──────────────────────────────────────────────────────────────
|
|
|
* The wrapper writes inline `transform`, `width`, `height` based on the
|
|
|
* active trigger's offset rect. The recipe handles the slide animation,
|
|
|
* z-index, and per-variant appearance. */
|
|
|
|
|
|
/* Position + size come from soma's `--indicator-*` (the "soma positions, eidos
|
|
|
shapes" contract); the variants below own the look and may pin a cross-axis
|
|
|
dimension on top. Uses the independent `translate` property, not `transform`,
|
|
|
so a per-variant `transform` never wipes the slide.
|
|
|
|
|
|
Those four vars are soma's VALUE CHANNEL, not a theming surface: the
|
|
|
`MeasuredIndicator` layer writes the active trigger's offset rect onto this
|
|
|
element on every move, so a public `--tabs-indicator-width` on top would be a
|
|
|
knob a theme could pin and thereby break the slide — the adjudication
|
|
|
`navigation-menu` already carries for its own `indicator-w` (and the reason
|
|
|
its `-x` was retired dead). What belongs to a theme is the indicator's FORM —
|
|
|
thickness, fill, radius, shadow, the slide's duration and ease — and those
|
|
|
are the keys below.
|
|
|
|
|
|
`left` is PHYSICAL on purpose: soma measures `triggerRect.left - listRect.left`,
|
|
|
an offset that grows rightwards whatever the direction. A logical
|
|
|
`inset-inline-start` anchored the indicator to the RIGHT edge in RTL while the
|
|
|
offset kept pointing right, landing it on the wrong tab (measured: 400px off,
|
|
|
and RTL-1 could not see it because the offset used to be an INLINE transform
|
|
|
written by JS, not a CSS declaration). In LTR the two are identical. */
|
|
|
[data-tabs-indicator] {
|
|
|
position: absolute;
|
|
|
inset-block-start: 0;
|
|
|
left: 0;
|
|
|
pointer-events: none;
|
|
|
z-index: 0;
|
|
|
translate: var(--indicator-x, 0px) var(--indicator-y, 0px);
|
|
|
inline-size: var(--indicator-w, 0px);
|
|
|
block-size: var(--indicator-h, 0px);
|
|
|
transition:
|
|
|
translate var(--tabs-indicator-transition-duration) var(--tabs-indicator-transition-ease),
|
|
|
inline-size var(--tabs-indicator-transition-duration) var(--tabs-indicator-transition-ease),
|
|
|
block-size var(--tabs-indicator-transition-duration) var(--tabs-indicator-transition-ease),
|
|
|
opacity var(--duration-fast) var(--ease-out);
|
|
|
will-change: translate, inline-size, block-size;
|
|
|
}
|
|
|
|
|
|
[data-tabs-indicator]:not([data-ready]) {
|
|
|
opacity: 0;
|
|
|
}
|
|
|
|
|
|
/* `line` variant: indicator is a thin underline along the leading edge */
|
|
|
[data-tabs][data-variant='line'] [data-tabs-indicator] {
|
|
|
background: var(--tabs-active-trigger-border);
|
|
|
height: var(--tabs-indicator-line-thickness, 2px);
|
|
|
inset-block-start: auto;
|
|
|
inset-block-end: calc(var(--tabs-list-border-width) * -1);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-variant='line'][data-orientation='vertical'] [data-tabs-indicator] {
|
|
|
width: var(--tabs-indicator-line-thickness, 2px);
|
|
|
inset-inline-start: auto;
|
|
|
inset-inline-end: calc(var(--tabs-list-border-width) * -1);
|
|
|
height: auto;
|
|
|
}
|
|
|
|
|
|
/* When the recipe takes over indicator dimensions (line variant uses a
|
|
|
* fixed thickness), the wrapper's inline width/height for the cross axis
|
|
|
* are overridden by the rules above; the inline axis stays driven by JS. */
|
|
|
|
|
|
/* `pills` variant: indicator is the rounded active background */
|
|
|
[data-tabs][data-variant='pills'] [data-tabs-indicator] {
|
|
|
background: var(--tabs-active-trigger-bg);
|
|
|
border: var(--tabs-trigger-border-width) solid var(--tabs-active-pills-trigger-border);
|
|
|
border-radius: var(--tabs-pills-trigger-radius);
|
|
|
box-shadow: var(--tabs-active-pills-trigger-shadow);
|
|
|
z-index: 0;
|
|
|
}
|
|
|
|
|
|
/* `segmented` variant: indicator is the PRIMARY accent pill (no accent
|
|
|
* border), concentric with the track. */
|
|
|
[data-tabs][data-variant='segmented'] [data-tabs-indicator] {
|
|
|
background: var(--tabs-active-segmented-trigger-bg);
|
|
|
border-radius: var(--tabs-segmented-trigger-radius);
|
|
|
box-shadow: var(--tabs-active-segmented-trigger-shadow);
|
|
|
z-index: 0;
|
|
|
}
|
|
|
|
|
|
/* Triggers inside a list with an indicator need to layer above it so the
|
|
|
* label is readable. */
|
|
|
[data-tabs-list]:has([data-tabs-indicator]) [data-tabs-trigger] {
|
|
|
position: relative;
|
|
|
z-index: 1;
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
[data-tabs-list]:has([data-tabs-indicator])
|
|
|
[data-tabs-trigger][data-state='active'] {
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
/* Surface variant doesn't use the indicator — hide if accidentally rendered. */
|
|
|
[data-tabs][data-variant='surface'] [data-tabs-indicator] {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
/* ── Fitted ─────────────────────────────────────────────────────────────────
|
|
|
* List fills inline width and triggers split it equally (flex: 1 1 0).
|
|
|
* Wraps still work because flex-wrap is on the list — when wrapping
|
|
|
* happens, each row's triggers re-distribute. */
|
|
|
|
|
|
[data-tabs][data-fitted] [data-tabs-list] {
|
|
|
width: 100%; /* literal: identity — `fitted` IS "the list fills the inline axis"; a token here would be a knob that can only hold one value without unsaying the prop */
|
|
|
flex-wrap: nowrap;
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-fitted] [data-tabs-trigger] {
|
|
|
flex: 1 1 0;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
/* ── Scrollable list ───────────────────────────────────────────────────────
|
|
|
* Horizontal-only — vertical scroll is handled by the natural overflow
|
|
|
* of the column. Edge fade signals scrollability. */
|
|
|
|
|
|
[data-tabs][data-scrollable]:not([data-orientation='vertical']) [data-tabs-list] {
|
|
|
flex-wrap: nowrap;
|
|
|
overflow-x: auto;
|
|
|
scroll-snap-type: x proximity;
|
|
|
scrollbar-width: none;
|
|
|
mask-image: linear-gradient(
|
|
|
to right,
|
|
|
transparent,
|
|
|
black var(--tabs-scrollable-fade, 16px),
|
|
|
black calc(100% - var(--tabs-scrollable-fade, 16px)),
|
|
|
transparent
|
|
|
);
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-scrollable] [data-tabs-list]::-webkit-scrollbar {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-scrollable] [data-tabs-trigger] {
|
|
|
flex: 0 0 auto;
|
|
|
scroll-snap-align: start;
|
|
|
}
|
|
|
|
|
|
/* ── Collapse vertical → horizontal at narrow widths ────────────────────────
|
|
|
* `data-collapsed` is written by the wrapper via ResizeObserver when the
|
|
|
* provider is narrower than the `collapseAt` threshold. Selectors below
|
|
|
* mirror the horizontal-orientation rules so a vertical tablist gracefully
|
|
|
* adopts the horizontal layout on narrow widths. */
|
|
|
|
|
|
[data-tabs][data-collapsed][data-orientation='vertical'] {
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-collapsed][data-orientation='vertical'] [data-tabs-list] {
|
|
|
flex: 0 1 auto;
|
|
|
flex-direction: row;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-collapsed][data-variant='line'][data-orientation='vertical']
|
|
|
[data-tabs-list] {
|
|
|
border-inline-end: none;
|
|
|
border-bottom: var(--tabs-list-border-width) solid var(--tabs-list-border);
|
|
|
padding-inline-end: 0;
|
|
|
}
|
|
|
|
|
|
[data-tabs][data-collapsed][data-variant='line'][data-orientation='vertical']
|
|
|
[data-tabs-indicator] {
|
|
|
width: auto;
|
|
|
height: var(--tabs-indicator-line-thickness, 2px);
|
|
|
inset-block-start: auto;
|
|
|
inset-block-end: calc(var(--tabs-list-border-width) * -1);
|
|
|
inset-inline-end: auto;
|
|
|
inset-inline-start: 0;
|
|
|
}
|
|
|
|
|
|
/* ── Content motion presets ────────────────────────────────────────────────
|
|
|
* The active panel animates in; inactive panels are unmounted (default
|
|
|
* lazy) or hidden (`keepMounted`). All presets respect prefers-reduced-
|
|
|
* motion below.
|
|
|
*
|
|
|
* MATERIALS from the motion channel (registered, themeable keyframes) — the
|
|
|
* trigger stays these recipe rules because tabs' own `data-motion` prop
|
|
|
* ('fade'/'slide') × `data-orientation` selects the flavour, a per-component
|
|
|
* axis the generic preset trigger doesn't model. The subtle 4px nudge feeds
|
|
|
* the distance hook from the canonical scale. */
|
|
|
|
|
|
[data-tabs-content][data-motion='fade'][data-state='active'] {
|
|
|
animation: fade-in var(--duration-fast) var(--ease-out) both;
|
|
|
}
|
|
|
|
|
|
[data-tabs-content][data-motion='slide'][data-state='active'][data-orientation='horizontal'] {
|
|
|
--motion-distance-md: var(--motion-distance-sm);
|
|
|
animation:
|
|
|
slide-from-bottom var(--duration-normal) var(--ease-out) both,
|
|
|
fade-in var(--duration-normal) var(--ease-out) both;
|
|
|
}
|
|
|
|
|
|
[data-tabs-content][data-motion='slide'][data-state='active'][data-orientation='vertical'] {
|
|
|
--motion-distance-md: var(--motion-distance-sm);
|
|
|
animation:
|
|
|
slide-from-right var(--duration-normal) var(--ease-out) both,
|
|
|
fade-in var(--duration-normal) var(--ease-out) both;
|
|
|
}
|
|
|
|
|
|
/* ── Reduced motion ─────────────────────────────────────────────────────────
|
|
|
* Honour the user's OS preference: indicator snaps without slide,
|
|
|
* content panels appear instantly. */
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
[data-tabs-indicator] {
|
|
|
transition: opacity var(--duration-fast) var(--ease-out);
|
|
|
}
|
|
|
[data-tabs-content][data-motion='fade'],
|
|
|
[data-tabs-content][data-motion='slide'] {
|
|
|
animation: none;
|
|
|
}
|
|
|
}
|