Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9b84e74
init traveling indicator animation
francisrupert Apr 10, 2026
50da80e
add default transition timing function class
francisrupert Apr 10, 2026
d6a0f55
improve traveling indicator animation with scale and mode-specific tr…
francisrupert Apr 10, 2026
72b67c5
cancel indicator animations when keypressing with activation-mode="auto"
francisrupert Apr 10, 2026
34339aa
showIndicatorTransition prop, default to true
francisrupert Apr 10, 2026
21b84da
scratch.md traveling indicator stress test suite to
francisrupert Apr 10, 2026
2ecd085
segmented-control traveling indicator
francisrupert Apr 10, 2026
754b536
indicator animation logic into composable
francisrupert Apr 10, 2026
ffe9183
indicator animation logic into composable
francisrupert Apr 11, 2026
0966ee0
dial back duration
francisrupert Apr 11, 2026
4cae91a
use center-to-center delta for indicator position calculation
francisrupert Apr 11, 2026
a9ca1e1
replace direct tabs property access with $refs.tabs
francisrupert Apr 11, 2026
c82b2e4
correct from 'quint' to 'out-quint'
francisrupert Apr 12, 2026
752fb02
skip animation when tabs wrap to different rows/columns
francisrupert Apr 12, 2026
6b132de
escape value in querySelector to handle special characters
francisrupert Apr 12, 2026
c73a0eb
inline indicator animation logic into composable and remove separate …
francisrupert Apr 13, 2026
d1130bd
move cancel call before getBoundingClientRect to prevent race condition
francisrupert Apr 13, 2026
8e079c2
only extract specific computed style properties instead of entire get…
francisrupert Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/dialtone-documentation/docs/components/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Remove the bottom border of any tablist.
```vue demo
<div class="d-w100p">
<example-tabs borderless />
</div>
</div>
<!-- @code -->
<dt-tab-group borderless>
...
Expand Down
241 changes: 239 additions & 2 deletions apps/dialtone-documentation/docs/scratch.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ layout: Blank

<script setup>
import { ref, computed } from 'vue';
import ExampleTabs from '@exampleComponents/ExampleTabs.vue';
import { DtTabGroup, DtTab, DtTabPanel } from '@dialpad/dialtone-vue';
import { useThemeManager } from '@composables/useThemeManager';

const {
Expand Down Expand Up @@ -768,7 +770,10 @@ const checkRadioDisabled = ref(false);
<template v-if="showTabEndIcon" #endIcon="{ iconSize }">
<dt-icon name="box-select" :size="iconSize" />
</template>
United Kingdom
<dt-stack>
United Kingdom
<dt-text as="p" kind="body" :size="200" tone="muted">England, Scotland, Wales, Northern Ireland</dt-text>
</dt-stack>
</dt-tab>
<dt-tab id="7" panel-id="8" :label-class="resolvedTabLabelClass">
<template v-if="showIcon" #startIcon="{ iconSize }">
Expand Down Expand Up @@ -1001,5 +1006,237 @@ const checkRadioDisabled = ref(false);
</dt-stack>
</dt-stack>
</dt-stack>
<dt-stack gap="200">
<dt-text as="h1" kind="headline" :size="600">Traveling Indicator Stress Test</dt-text>
<!-- 1. ALL FOUR VARIANTS SIDE BY SIDE -->
<dt-text as="h2" kind="headline" :size="400">1. All four variants</dt-text>
<dt-stack gap="400">
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Default (::after underline)</dt-text>
<example-tabs />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Outlined</dt-text>
<example-tabs outlined />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Muted + Outlined</dt-text>
<example-tabs kind="muted" outlined />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Muted Active (background)</dt-text>
<example-tabs kind="muted" />
</dt-stack>
</dt-stack>
<!-- 2. ALL SIZES -->
<dt-text as="h2" kind="headline" :size="400">2. All sizes (width variance stress)</dt-text>
<dt-stack gap="400">
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Size 100 (xs)</dt-text>
<example-tabs size="100" />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Size 200 (sm)</dt-text>
<example-tabs size="200" />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Size 300 (md) β€” default</dt-text>
<example-tabs size="300" />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Size 400 (lg)</dt-text>
<example-tabs size="400" />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Size 500 (xl)</dt-text>
<example-tabs size="500" />
</dt-stack>
</dt-stack>
<!-- 3. SPREAD MODES -->
<dt-text as="h2" kind="headline" :size="400">3. Spread modes (indicator width morphing)</dt-text>
<dt-stack gap="400">
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">spread="none" (default)</dt-text>
<example-tabs />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">spread="grow"</dt-text>
<example-tabs spread="grow" />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">spread="equal"</dt-text>
<example-tabs spread="equal" />
</dt-stack>
</dt-stack>
<!-- 4. VERTICAL ORIENTATION -->
<dt-text as="h2" kind="headline" :size="400">4. Vertical orientation</dt-text>
<dt-stack gap="400" direction="row">
<dt-stack gap="100" class="d-fl1">
<dt-text as="p" kind="label" :size="200">Default vertical</dt-text>
<example-tabs orientation="vertical" />
</dt-stack>
<dt-stack gap="100" class="d-fl1">
<dt-text as="p" kind="label" :size="200">Outlined vertical</dt-text>
<example-tabs orientation="vertical" outlined />
</dt-stack>
<dt-stack gap="100" class="d-fl1">
<dt-text as="p" kind="label" :size="200">Muted vertical</dt-text>
<example-tabs orientation="vertical" kind="muted" />
</dt-stack>
</dt-stack>
<!-- 5. BORDERLESS -->
<dt-text as="h2" kind="headline" :size="400">5. Borderless</dt-text>
<dt-stack gap="400">
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Borderless default</dt-text>
<example-tabs borderless />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Borderless outlined</dt-text>
<example-tabs borderless outlined />
</dt-stack>
</dt-stack>
<!-- 6. AUTO ACTIVATION (keyboard rapid-fire) -->
<dt-text as="h2" kind="headline" :size="400">6. Auto activation mode (arrow keys should NOT animate)</dt-text>
<dt-stack gap="400">
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Auto mode β€” click should animate, arrows should snap</dt-text>
<example-tabs activation-mode="auto" />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Auto mode + outlined</dt-text>
<example-tabs activation-mode="auto" outlined />
</dt-stack>
</dt-stack>
<!-- 7. DISABLED -->
<dt-text as="h2" kind="headline" :size="400">7. Disabled (should do nothing)</dt-text>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Whole group disabled</dt-text>
<example-tabs disabled />
</dt-stack>
<!-- 8. showIndicatorTransition=false -->
<dt-text as="h2" kind="headline" :size="400">8. showIndicatorTransition=false (animation suppressed)</dt-text>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Should switch instantly, no slide</dt-text>
<dt-tab-group :show-indicator-transition="false">
<template #tabs>
<dt-tab id="s1" panel-id="s2" selected>First</dt-tab>
<dt-tab id="s3" panel-id="s4">Second</dt-tab>
<dt-tab id="s5" panel-id="s6">Third</dt-tab>
</template>
<dt-tab-panel id="s2" tab-id="s1"><dt-text>Panel 1</dt-text></dt-tab-panel>
<dt-tab-panel id="s4" tab-id="s3"><dt-text>Panel 2</dt-text></dt-tab-panel>
<dt-tab-panel id="s6" tab-id="s5"><dt-text>Panel 3</dt-text></dt-tab-panel>
</dt-tab-group>
</dt-stack>
<!-- 9. MULTIPLE INSTANCES (conflict test) -->
<dt-text as="h2" kind="headline" :size="400">9. Multiple instances on same page (no conflicts)</dt-text>
<dt-text as="p" kind="body" :size="200" tone="muted">Click tabs in one group while another is mid-animation. They should not interfere.</dt-text>
<dt-stack gap="400" direction="row">
<dt-stack gap="100" class="d-fl1">
<dt-text as="p" kind="label" :size="200">Group A</dt-text>
<example-tabs />
</dt-stack>
<dt-stack gap="100" class="d-fl1">
<dt-text as="p" kind="label" :size="200">Group B</dt-text>
<example-tabs outlined />
</dt-stack>
<dt-stack gap="100" class="d-fl1">
<dt-text as="p" kind="label" :size="200">Group C</dt-text>
<example-tabs kind="muted" />
</dt-stack>
</dt-stack>
<!-- 10. EXTREME WIDTH VARIANCE -->
<dt-text as="h2" kind="headline" :size="400">10. Extreme tab width differences (scale morphing stress)</dt-text>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Short vs very long labels β€” watch the scale animation</dt-text>
<dt-tab-group>
<template #tabs>
<dt-tab id="w1" panel-id="w2" selected>Medium label</dt-tab>
<dt-tab id="w3" panel-id="w4">Extremely long tab label for stressy stressful testing</dt-tab>
<dt-tab id="w7" panel-id="w8">Shrt</dt-tab>
</template>
<dt-tab-panel id="w2" tab-id="w1"><dt-text>Panel A</dt-text></dt-tab-panel>
<dt-tab-panel id="w4" tab-id="w3"><dt-text>Panel Long</dt-text></dt-tab-panel>
<dt-tab-panel id="w8" tab-id="w7"><dt-text>Panel B</dt-text></dt-tab-panel>
</dt-tab-group>
<dt-tab-group kind="muted">
<template #tabs>
<dt-tab id="w1" panel-id="w2" selected>Medium label</dt-tab>
<dt-tab id="w3" panel-id="w4">Extremely long tab label for stressy stressful testing</dt-tab>
<dt-tab id="w7" panel-id="w8">Shrt</dt-tab>
</template>
<dt-tab-panel id="w2" tab-id="w1"><dt-text>Panel A</dt-text></dt-tab-panel>
<dt-tab-panel id="w4" tab-id="w3"><dt-text>Panel Long</dt-text></dt-tab-panel>
<dt-tab-panel id="w8" tab-id="w7"><dt-text>Panel B</dt-text></dt-tab-panel>
</dt-tab-group>
</dt-stack>
<!-- 11. MANY TABS (overflow / wrapping) -->
<dt-text as="h2" kind="headline" :size="400">11. Many tabs (potential wrapping)</dt-text>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Does the animation break when tabs wrap to a second row?</dt-text>
<dt-tab-group>
<template #tabs>
<dt-tab id="m1" panel-id="m2" selected>Alpha</dt-tab>
<dt-tab id="m3" panel-id="m4">Bravo</dt-tab>
<dt-tab id="m5" panel-id="m6">Charlie</dt-tab>
<dt-tab id="m7" panel-id="m8">Delta</dt-tab>
<dt-tab id="m9" panel-id="m10">Echo</dt-tab>
<dt-tab id="m11" panel-id="m12">Foxtrot</dt-tab>
<dt-tab id="m13" panel-id="m14">Golf</dt-tab>
<dt-tab id="m15" panel-id="m16">Hotel</dt-tab>
<dt-tab id="m17" panel-id="m18">India</dt-tab>
<dt-tab id="m19" panel-id="m20">Juliet</dt-tab>
</template>
<dt-tab-panel id="m2" tab-id="m1"><dt-text>Panel Alpha</dt-text></dt-tab-panel>
<dt-tab-panel id="m4" tab-id="m3"><dt-text>Panel Bravo</dt-text></dt-tab-panel>
<dt-tab-panel id="m6" tab-id="m5"><dt-text>Panel Charlie</dt-text></dt-tab-panel>
<dt-tab-panel id="m8" tab-id="m7"><dt-text>Panel Delta</dt-text></dt-tab-panel>
<dt-tab-panel id="m10" tab-id="m9"><dt-text>Panel Echo</dt-text></dt-tab-panel>
<dt-tab-panel id="m12" tab-id="m11"><dt-text>Panel Foxtrot</dt-text></dt-tab-panel>
<dt-tab-panel id="m14" tab-id="m13"><dt-text>Panel Golf</dt-text></dt-tab-panel>
<dt-tab-panel id="m16" tab-id="m15"><dt-text>Panel Hotel</dt-text></dt-tab-panel>
<dt-tab-panel id="m18" tab-id="m17"><dt-text>Panel India</dt-text></dt-tab-panel>
<dt-tab-panel id="m20" tab-id="m19"><dt-text>Panel Juliet</dt-text></dt-tab-panel>
</dt-tab-group>
</dt-stack>
<!-- 12. RTL (logical direction) -->
<dt-text as="h2" kind="headline" :size="400">12. RTL direction</dt-text>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Does the indicator slide the correct direction in RTL?</dt-text>
<div dir="rtl">
<example-tabs />
</div>
</dt-stack>
<!-- 13. INVERTED (dark on light) -->
<dt-text as="h2" kind="headline" :size="400">13. Inverted</dt-text>
<dt-stack gap="100">
<example-tabs v-dt-mode:invert class="d-p16 d-bar8 d-bgc-primary" />
</dt-stack>
<!-- 14. RAPID CLICK STRESS -->
<dt-text as="h2" kind="headline" :size="400">14. Rapid click test</dt-text>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Click tabs as fast as possible β€” animation should cancel cleanly, no stuck states</dt-text>
<example-tabs />
</dt-stack>
<!-- 15. COMBINED EXTREMES -->
<dt-text as="h2" kind="headline" :size="400">15. Combined extremes</dt-text>
<dt-stack gap="400">
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Outlined + spread=equal + size 500</dt-text>
<example-tabs outlined spread="equal" size="500" />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Muted + vertical + borderless</dt-text>
<example-tabs kind="muted" orientation="vertical" borderless />
</dt-stack>
<dt-stack gap="100">
<dt-text as="p" kind="label" :size="200">Muted + outlined + spread=grow + size 100</dt-text>
<example-tabs kind="muted" outlined spread="grow" size="100" />
</dt-stack>
</dt-stack>
</dt-stack>
</dt-stack>
<div class="d-h-1200"></div>
<!-- ================================================================== -->
<!-- TRAVELING INDICATOR STRESS TEST β€” remove before merging -->
<!-- ================================================================== -->
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Use `d-ttf-{n}` change an element's `transition-timing-function` (aka easing) fr
```vue demo
<!-- @wrapper -->
<dt-stack direction="row" gap="200">
<dt-button kind="unstyled" class="d-p-200 d-bar8 d-bgc-moderate h:d-bgc-critical h:d-bs-md h:d-fc-critical d-t d-td300 d-ttf ">Ease</dt-button>
<dt-button kind="unstyled" class="d-p-200 d-bar8 d-bgc-moderate h:d-bgc-critical h:d-bs-md h:d-fc-critical d-t d-td300 ">Ease In, Ease Out</dt-button>
<dt-button kind="unstyled" class="d-p-200 d-bar8 d-bgc-moderate h:d-bgc-critical h:d-bs-md h:d-fc-critical d-t d-td300 d-ttf-out ">Ease Out</dt-button>
<dt-button kind="unstyled" class="d-p-200 d-bar8 d-bgc-moderate h:d-bgc-critical h:d-bs-md h:d-fc-critical d-t d-td300 d-ttf-out-quint">Ease Out Quint</dt-button>
Expand Down Expand Up @@ -76,9 +77,9 @@ Use `d-tp-{n}` change an what items within an element are transitioned.
<th scope="row" class="d-code--sm d-docsite-code">.d-{{ i }}{{ d }}</th>
<td class="d-code--sm">transition-duration: var(--td{{ d }}) !important;</td>
</tr>
<tr v-else-if="i === 'ttf'" v-for="t in ['in-out', 'out', 'quint']">
<th scope="row" class="d-code--sm d-docsite-code">.d-{{ i }}-{{ t }}</th>
<td class="d-code--sm">transition-timing-function: var(--ttf-{{ t }}) !important;</td>
<tr v-else-if="i === 'ttf'" v-for="t in ['', 'in-out', 'out', 'quint']">
<th scope="row" class="d-code--sm d-docsite-code">.d-{{ i }}{{ t ? `-${t}` : '' }}</th>
<td class="d-code--sm">transition-timing-function: var(--ttf-{{ t || 'ease' }}) !important;</td>
</tr>
<tr v-else-if="i === 'tp'" v-for="p in ['all', 'o', 'bs', 'bgc', 'transform', 'colors']">
<th scope="row" class="d-code--sm d-docsite-code">.d-{{ i }}-{{ p }}</th>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
--radius-segmented-control-item: var(--dt-button-size-radius-md);
--radius-segmented-control: calc(var(--radius-segmented-control-item) + var(--padding-segmented-control));

// -- CSS VARS: traveling indicator animation
--segmented-indicator-duration: var(--td300);
--segmented-indicator-easing: var(--ttf-ease);

gap: var(--padding-segmented-control);
padding: var(--padding-segmented-control);
border: var(--dt-size-border-100) solid var(--dt-color-border-subtle);
Expand Down Expand Up @@ -64,6 +68,17 @@
&__item {
flex: 1 1 auto;
border-radius: var(--radius-segmented-control-item);
transition: none;

// Animatable overlay for traveling indicator.
// The ::after exists invisibly so Element.animate({ pseudoElement }) can target it.
&.d-btn--active::after {
position: absolute;
border-radius: var(--radius-segmented-control-item);
content: '';
pointer-events: none;
inset: var(--dt-spacing-1-negative);
}
}

// -- DIVIDERS
Expand Down
18 changes: 15 additions & 3 deletions packages/dialtone-css/lib/build/less/components/tabs.less
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@
--tab-color-text-selected: var(--dt-action-color-foreground-base-default);
--tab-selected-indicator: var(--dt-action-color-foreground-base-default);

// -- CSS VARS: traveling indicator animation
--tab-indicator-duration: var(--td300);
--tab-indicator-easing: var(--ttf-ease);

position: relative;
display: flex;
flex-wrap: wrap;
Expand All @@ -103,6 +107,7 @@

&__item {
text-align: start;
transition: none;

&--vertical {
inline-size: 100%;
Expand All @@ -111,6 +116,16 @@
justify-content: flex-start;
}
}

// Animatable overlay for traveling indicator in outlined/muted modes.
// The ::before exists invisibly so Element.animate({ pseudoElement }) can target it.
&:is(.d-btn--outlined, .d-btn--active)::before {
position: absolute;
border-radius: inherit;
content: '';
pointer-events: none;
inset: calc(var(--dt-size-border-100) * -1);
}
}

// Sizes
Expand Down Expand Up @@ -206,9 +221,6 @@
border: var(--dt-size-border-100) solid var(--dt-action-color-border-base-default);
border-radius: var(--tab-border-radius) var(--tab-border-radius) var(--tab-border-radius-end) var(--tab-border-radius-end);
cursor: pointer;
transition-timing-function: var(--ttf-out-quint);
transition-duration: var(--td100);
transition-property: background-color, border, color, box-shadow;
fill: currentColor;

&:where(:first-of-type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,10 @@
.d-td300 { transition-duration: var(--td300) !important; }

// -- TRANSITION TIMING
.d-ttf { transition-timing-function: var(--ttf-ease) !important; }
.d-ttf-in-out { transition-timing-function: var(--ttf-in-out) !important; }
.d-ttf-out { transition-timing-function: var(--ttf-out) !important; }
.ttf-out-quint { transition-timing-function: var(--ttf-out-quint) !important; }
.d-ttf-out-quint { transition-timing-function: var(--ttf-out-quint) !important; }

// -- TRANSITION PROPERTY
.d-tp-all { transition-property: all !important; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// $ TRANSITIONS
// ----------------------------------------------------------------------------
@transition: {
ttf-ease: cubic-bezier(0.25, 0.1, 0.25, 1);
ttf-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
ttf-out: cubic-bezier(0.23, 1, 0.32, 1);
ttf-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
Expand Down
Loading
Loading