Tabs
<template>
<PeachyTabs.Tabs v-model:tab="tab">
<PeachyTabs.Title>Tabs</PeachyTabs.Title>
<PeachyTabs.List>
<PeachyTabs.ListItem id="zero">Tab 0</PeachyTabs.ListItem>
<PeachyTabs.ListItem id="one">Tab 1</PeachyTabs.ListItem>
<PeachyTabs.ListItem id="two">Tab 2</PeachyTabs.ListItem>
</PeachyTabs.List>
<Transition name="tabs">
<PeachyTabs.Panel v-if="tab == 'zero'" key="zero" id="zero">
Tab 0 Panel
</PeachyTabs.Panel>
<PeachyTabs.Panel v-else-if="tab == 'one'" key="one" id="one">
Tab 1 Panel
</PeachyTabs.Panel>
<PeachyTabs.Panel v-else-if="tab == 'two'" key="two" id="two">
Tab 2 Panel
</PeachyTabs.Panel>
</Transition>
</PeachyTabs.Tabs>
</template>
.peachy-tabs__title {
line-height: 1;
font-size: 120%;
font-weight: var(--fw-bold);
margin: 0;
}
.peachy-tabs__list {
display: flex;
gap: calc(var(--gap) * 2);
margin-top: 1rem;
margin-bottom: 0.75rem;
}
.peachy-tabs__list-item {
cursor: pointer;
border: none;
background: transparent;
padding: 0.5em;
border-bottom: 3px solid transparent;
font-size: 90%;
color: var(--dimmed-text);
margin-bottom: var(--gap);
transition:
border-color 0.25s,
color 0.25s;
}
.peachy-tabs__list-item:hover,
.peachy-tabs__list-item:focus-visible {
color: var(--text);
}
.peachy-tabs__list-item[aria-selected="true"] {
color: var(--text);
border-color: var(--brand-color-dimmed);
}
.peachy-tabs__panel {
width: var(--component-width);
padding: 1em;
border-radius: var(--border-radius);
background: var(--bg);
}
/* ===== Transition ===== */
.tabs-move,
.tabs-enter-active,
.tabs-leave-active {
transition: opacity 0.25s;
}
.tabs-enter-from,
.tabs-leave-to {
opacity: 0;
}
.tabs-leave-active {
position: absolute;
}
Anatomy
<template>
<PeachyTabs.Tabs>
<PeachyTabs.Title />
<PeachyTabs.IntervalController>
<template #pause />
<template #resume />
</PeachyTabs.IntervalController>
<PeachyTabs.NavigationButton />
<PeachyTabs.List>
<PeachyTabs.ListItem id="<tab-id>" />
</PeachyTabs.List>
<PeachyTabs.Panel id="<tab-id>" />
</PeachyTabs.Tabs>
</template>
<script lang="ts" setup>
import { PeachyTabs } from "typeach";
</script>
CAROUSEL DESCRIPTION
When creating a carousel, a localized value for aria-roledescription
should be provided for the panels.
Props & Emits
Tabs
Props
Name | Default | Type |
---|---|---|
tab | string | |
followFocus | false | boolean? |
interval | 0 | number? |
followFocus
USES v-show
INSTEAD OF v-if
When followFocus
is set the panels will be rendered but stay hidden with v-show
, rather than delay render with v-if
- to preload the content to ensure a smooth experience for all users quickly shifting through.
Emits
@ | Payload |
---|---|
update:tab | string |
List
Props
Name | Default | Type |
---|---|---|
orientation | Orientation | string? |
type Orientation = "horizontal" | "vertical";
ListItem
Props
Name | Default | Type |
---|---|---|
id | string |
id
SHOULD MATCH id
OF IT'S Panel
To create a relation between a ListItem
and Panel
, id
's are used. If you provide different values, the tabs will not work as expected, and the accessibility will be compromised.
id
WILL BE MODIFIED
A suffix will be added to the id
. If you pass in "tabs"
, the id
rendered will be "tabs-list-item"
. This is purely to make our lives easier when declaring the ARIA relation between a ListItem
and Panel
.
Panel
Props
Name | Default | Type |
---|---|---|
id | string |
id
SHOULD MATCH id
OF IT'S ListItem
To create a relation between a ListItem
and Panel
, id
's are used. If you provide different values, the tabs will not work as expected, and the accessibility will be compromised.
NavigationButton
Props
Name | Default | Type |
---|---|---|
to | "next" , "previous" |
Styling
CSS Selectors
Follows our CSS classes convention.
State selectors
Selector | Description | For |
---|---|---|
[data-paused="<boolean>"] | Selection based on if it's paused or not. |
|
[aria-selected="<boolean>"] | For the selected state of a tab. |
|
[data-to="<Direction>"] | For the direction of a navigation button. |
|
type Direction = "next" | "previous";
Accessibility
Resources: APG Tabs Pattern, APG Carousel Pattern with tabs
CAROUSEL DESCRIPTION
When creating a carousel, a localized value for aria-roledescription
should be provided for the panels.
Keyboard interactions
Key | Action |
---|---|
Space or ↵ on ListItem | Opens the associated tab. |
→ on a ListItem | Moves focus to the next tab item. |
← on a ListItem | Moves focus to the previous tab item. |
Home on a ListItem | Moves focus to the first tab item. |
End on a ListItem | Moves focus to the last tab item. |
Hover and keyboard interactions are synced. E.g. in the example tab, if you hover over "Tab 1", and then press arrow right - the focus will move to "Tab 2".
Automatic rotating with interval
When Tabs
has an interval, it will automatically rotate through the tabs - except for when a user is hovering or has focus inside Tabs
.
ALLOW USERS TO STOP AUTOMATIC ROTATION
If you set an interval, you need to provide a way for the user to stop the automatic rotation. This can be done by providing an IntervalController
.
Creating a carousel
<template>
<PeachyTabs.Tabs
v-model:tab="slide"
:interval="3000"
follow-focus
data-carousel="true">
<PeachyTabs.Title>Carousel</PeachyTabs.Title>
<PeachyTabs.List>
<PeachyTabs.ListItem id="carousel-zero">Slide 0</PeachyTabs.ListItem>
<PeachyTabs.ListItem id="carousel-one">Slide 1</PeachyTabs.ListItem>
<PeachyTabs.ListItem id="carousel-two">Slide 2</PeachyTabs.ListItem>
</PeachyTabs.List>
<div class="target-wrapper">
<PeachyTabs.NavigationButton to="previous" aria-label="Previous slide">
<ChevronLeftSvg />
</PeachyTabs.NavigationButton>
<TransitionGroup name="tabs">
<PeachyTabs.Panel
v-show="slide == 'carousel-zero'"
key="carousel-zero"
id="carousel-zero"
aria-roledescription="carousel">
Slide 0 Content
</PeachyTabs.Panel>
<PeachyTabs.Panel
v-show="slide == 'carousel-one'"
key="carousel-one"
id="carousel-one"
aria-roledescription="carousel">
Slide 1 Content
</PeachyTabs.Panel>
<PeachyTabs.Panel
v-show="slide == 'carousel-two'"
key="carousel-two"
id="carousel-two"
aria-roledescription="carousel">
Slide 2 Content
</PeachyTabs.Panel>
</TransitionGroup>
<PeachyTabs.NavigationButton to="next" aria-label="Next slide">
<ChevronRightSvg />
</PeachyTabs.NavigationButton>
</div>
<PeachyTabs.IntervalController>
<template #pause>
<PauseSvg />
Pause carousel
</template>
<template #resume>
<PlaySvg />
Resume carousel
</template>
</PeachyTabs.IntervalController>
</PeachyTabs.Tabs>
</template>
.peachy-tabs__title {
text-align: center;
}
.peachy-tabs[data-carousel="true"] .peachy-tabs__list {
width: 100%;
justify-content: center;
}
.target-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: var(--gap);
}
.peachy-tabs[data-carousel="true"] .peachy-tabs__panel {
width: calc(var(--component-width) + 2rem);
height: 8rem;
}
.peachy-tabs__navigation-button {
cursor: pointer;
border: none;
background: none;
margin-top: 1rem;
display: grid;
place-content: center;
padding: var(--icon-button-padding);
border-radius: 100%;
transition: background 0.25s;
}
.peachy-tabs__navigation-button svg {
width: var(--icon-size);
height: var(--icon-size);
}
.peachy-tabs__navigation-button:hover {
background: var(--button-hover-bg);
}
.peachy-tabs__interval-controller {
color: var(--text);
border: none;
background: none;
font-size: 100%;
display: flex;
align-items: center;
gap: calc(var(--gap) / 2);
margin: 0.5rem 0 0 1.75rem;
}