Appearance
Tabs Component Code
Dependencies
This component requires:
- Vue 3 with Composition API
- Optional: i18n library if using translation keys
Full Component Code
vue
<script setup>
import { toRefs, ref, nextTick, onMounted, watch, inject } from "vue";
const convertPixelsToRem = inject('convertPixelsToRem', null);
const props = defineProps({
tabItems: {
type: Array,
required: false,
default: () => [],
},
selected: {
type: [Array, String],
required: false,
default: null,
},
multiple: {
type: Boolean,
required: false,
default: false,
},
isLabeli18String: {
type: Boolean,
required: false,
default: true,
},
singlePacked: {
type: Boolean,
required: false,
default: false,
},
tabClasses: {
type: Array,
required: false,
default: () => [],
},
bottomLineStyle: {
type: Boolean,
required: false,
default: false,
},
});
const { tabItems, selected, multiple, bottomLineStyle } = toRefs(props);
const emit = defineEmits(["item:clicked"]);
// Refs for bottom line animation
const tabsContainer = ref(null);
const bottomLine = ref(null);
function handleTabItemClick(item) {
emit("item:clicked", item);
if (bottomLineStyle.value) {
nextTick(() => {
updateBottomLinePosition();
});
}
}
function updateBottomLinePosition() {
if (!bottomLineStyle.value || !tabsContainer.value || !bottomLine.value) return;
const selectedTab = tabsContainer.value.querySelector('.tab.selected');
if (selectedTab) {
const containerRect = tabsContainer.value.getBoundingClientRect();
const tabRect = selectedTab.getBoundingClientRect();
const offsetLeft = tabRect.left - containerRect.left;
const width = tabRect.width;
bottomLine.value.style.transform = `translateX(${convertPixelsToRem(offsetLeft)}rem)`;
bottomLine.value.style.width = `${convertPixelsToRem(width)}rem`;
}
}
onMounted(() => {
if (bottomLineStyle.value) {
nextTick(() => {
updateBottomLinePosition();
});
}
});
watch(selected, () => {
if (bottomLineStyle.value) {
nextTick(() => {
updateBottomLinePosition();
});
}
});
</script>
<template>
<div class="basic-tabs-wrapper">
<div
ref="tabsContainer"
class="overflow-container"
:class="[{
'single-packed': singlePacked,
'bottom-line-style': bottomLineStyle
}, ...tabClasses]"
>
<div
v-for="item of tabItems"
:key="item.value"
class="tab"
:class="[{
selected: multiple
? selected.includes(item.value)
: selected === item.value,
}]"
@click="handleTabItemClick(item)"
>
<slot name="prepend" :item="item">
<div v-if="item && item.prepend" class="prepend-icon" v-html="item.prepend"></div>
</slot>
<slot name="tab-icon" :item="item">
<template v-if="isLabeli18String && item.name">
{{ $t(item.name) }}
</template>
<template v-else-if="item.name">
{{ item.name }}
</template>
<div class="icon" v-else-if="item.icon" v-html="item.icon"></div>
</slot>
<slot name="append" :item="item">
<div v-if="item && item.append" class="append-icon" v-html="item.append"></div>
</slot>
</div>
<!-- Moving bottom line for bottomLineStyle -->
<div class="bottom-line-container" v-if="bottomLineStyle">
<div
ref="bottomLine"
class="bottom-line"
></div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.basic-tabs-wrapper {
display: flex;
position: relative;
overflow-x: hidden;
width: 100%;
.overflow-container {
display: flex;
align-items: center;
justify-content: start;
overflow: hidden;
overflow-x: auto;
gap: 0.5rem;
&:has(.icon) {
width: max-content;
border-radius: 0.25rem;
}
.tab {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
padding: 0 0.75rem;
border-radius: 0.25rem;
border: 0.0625rem solid black;
background: white;
color: black;
font-size: 0.8125rem;
font-weight: 500;
min-width: 3.375rem;
height: 2rem;
transition: all 0.3s ease;
text-transform: capitalize;
&:hover,
&.selected {
color: white;
border-color: black;
background: black;
}
&:has(.icon) {
padding: 0 0.5rem;
.icon {
display: flex;
justify-content: center;
align-items: center;
width: 1.25rem;
height: 1.25rem;
}
}
}
&.single-packed {
border: 0.0625rem solid black;
border-radius: 0.25rem;
gap: 0rem;
.tab {
border-radius: 0;
border: none;
&:not(:last-child) {
border-right: 0.0625rem solid black;
}
}
}
&.dark-gold-tab {
.tab {
background: #f7dd19;
color: black;
border-color: #f7dd19;
&:hover, &.selected {
background: black;
color: #f7dd19;
}
}
}
&.bottom-line-style {
position: relative;
gap: 0;
.tab {
border: none;
border-radius: 0;
background: transparent;
color: #666;
font-weight: 500;
padding: 0.75rem 1.5rem;
position: relative;
transition: color 0.3s ease;
&:hover {
background: transparent;
color: #333;
border-color: transparent;
}
&.selected {
background: transparent;
color: #007bff;
border-color: transparent;
}
}
.bottom-line-container {
position: absolute;
width: 100%;
height: 2px;
background: #e0e0e0;
z-index: 0;
bottom: 0px;
.bottom-line {
position: absolute;
bottom: 0px;
left: 0;
height: 2px;
background: #007bff;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1;
}
}
&.dark-gold-tab {
.tab {
background: transparent;
color: black;
&:hover, &.selected {
color: #f7dd19;
}
}
.bottom-line-container {
.bottom-line {
background: #f7dd19;
}
}
}
}
}
}
</style>Usage Notes
Tab Items Structure
The tabItems prop expects an array of objects with this structure:
js
[
{
value: 'tab1', // Unique identifier for the tab
name: 'Tab 1', // Display name or i18n key
icon: '<svg>...</svg>', // Optional: SVG icon as string
prepend: '<svg>...</svg>', // Optional: Icon to prepend to the tab
append: '<svg>...</svg>' // Optional: Icon to append to the tab
},
// More tabs...
]Internationalization
When isLabeli18String is set to true (default), the component will attempt to translate tab names using the Vue i18n $t function. Make sure your i18n setup is properly configured.
Multiple Selection
When multiple is set to true, the component allows multiple tabs to be selected simultaneously. In this mode, the selected prop should be an array of tab values.
Bottom Line Style
When bottomLineStyle is set to true, the component will display an animated bottom line indicator that moves to the selected tab. This style is commonly used for navigation tabs.
Single Packed Style
When singlePacked is set to true, the tabs will be displayed in a single container with shared borders between tabs. This style is useful for option groups or segmented controls.
Custom Tab Content
Use the available slots to customize the appearance of each tab:
vue
<OTabs :tabItems="tabs" :selected="selectedTab">
<template #prepend="{ item }">
<div v-if="item.prepend" v-html="item.prepend"></div>
</template>
<template #tab-icon="{ item }">
<div class="custom-tab">
<img :src="item.icon" v-if="item.icon" />
<span>{{ item.name }}</span>
</div>
</template>
<template #append="{ item }">
<div v-if="item.append" v-html="item.append"></div>
</template>
</OTabs>