Skip to content

Tabs


vue
<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>
css
.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

vue
<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

NameDefaultType
tabstring
followFocusfalseboolean?
interval0number?

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:tabstring

List


Props

NameDefaultType
orientationOrientationstring?
ts
type Orientation = "horizontal" | "vertical";

ListItem


Props

NameDefaultType
idstring

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

NameDefaultType
idstring

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.



Props

NameDefaultType
to"next", "previous"

Styling

CSS Selectors

Follows our CSS classes convention.


State selectors

SelectorDescriptionFor
[data-paused="<boolean>"]Selection based on if it's paused or not.
  • IntervalController
[aria-selected="<boolean>"]For the selected state of a tab.
  • ListItem
[data-to="<Direction>"]For the direction of a navigation button.
  • NavigationButton
ts
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

KeyAction
Space or on ListItemOpens the associated tab.
on a ListItemMoves focus to the next tab item.
on a ListItemMoves focus to the previous tab item.
Home on a ListItemMoves focus to the first tab item.
End on a ListItemMoves 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.

vue
<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>
css
.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;
}