diff --git a/packages/docs/components.d.ts b/packages/docs/components.d.ts
index c53f1629ae3..eb5a98f672b 100644
--- a/packages/docs/components.d.ts
+++ b/packages/docs/components.d.ts
@@ -23,6 +23,8 @@ declare module 'vue' {
ApiSection: typeof import('./src/components/api/Section.vue')['default']
ApiSlotsTable: typeof import('./src/components/api/SlotsTable.vue')['default']
AppBackToTop: typeof import('./src/components/app/BackToTop.vue')['default']
+ AppBanner: typeof import('./src/components/app/Banner.vue')['default']
+ AppBarAuthDialog: typeof import('./src/components/app/bar/AuthDialog.vue')['default']
AppBarBar: typeof import('./src/components/app/bar/Bar.vue')['default']
AppBarEcosystemMenu: typeof import('./src/components/app/bar/EcosystemMenu.vue')['default']
AppBarEnterpriseLink: typeof import('./src/components/app/bar/EnterpriseLink.vue')['default']
@@ -76,6 +78,7 @@ declare module 'vue' {
AppSettingsOptionsQuickbarOption: typeof import('./src/components/app/settings/options/QuickbarOption.vue')['default']
AppSettingsOptionsRailDrawerOption: typeof import('./src/components/app/settings/options/RailDrawerOption.vue')['default']
AppSettingsOptionsSlashSearchOption: typeof import('./src/components/app/settings/options/SlashSearchOption.vue')['default']
+ AppSettingsOptionsSyncOption: typeof import('./src/components/app/settings/options/SyncOption.vue')['default']
AppSettingsOptionsThemeOption: typeof import('./src/components/app/settings/options/ThemeOption.vue')['default']
AppSettingsPerksOptions: typeof import('./src/components/app/settings/PerksOptions.vue')['default']
AppSettingsSettingsHeader: typeof import('./src/components/app/settings/SettingsHeader.vue')['default']
@@ -149,10 +152,16 @@ declare module 'vue' {
SponsorCard: typeof import('./src/components/sponsor/Card.vue')['default']
SponsorLink: typeof import('./src/components/sponsor/Link.vue')['default']
SponsorSponsors: typeof import('./src/components/sponsor/Sponsors.vue')['default']
+ UserAccountConnectedAccounts: typeof import('./src/components/user/account/ConnectedAccounts.vue')['default']
+ UserAccountOneSubscription: typeof import('./src/components/user/account/OneSubscription.vue')['default']
UserBadgesUserAdminBadge: typeof import('./src/components/user/badges/UserAdminBadge.vue')['default']
UserBadgesUserOneBadge: typeof import('./src/components/user/badges/UserOneBadge.vue')['default']
UserBadgesUserSponsorBadge: typeof import('./src/components/user/badges/UserSponsorBadge.vue')['default']
+ UserDiscordLogin: typeof import('./src/components/user/DiscordLogin.vue')['default']
+ UserGithubLogin: typeof import('./src/components/user/GithubLogin.vue')['default']
UserOneSubCard: typeof import('./src/components/user/OneSubCard.vue')['default']
+ UserUserBadges: typeof import('./src/components/user/UserBadges.vue')['default']
+ UserUserProfile: typeof import('./src/components/user/UserProfile.vue')['default']
UserUserTabs: typeof import('./src/components/user/UserTabs.vue')['default']
}
}
diff --git a/packages/docs/src/examples/v-tabs/misc-content.vue b/packages/docs/src/examples/v-tabs/misc-content.vue
index 8b826cbf07c..98cab1775c3 100644
--- a/packages/docs/src/examples/v-tabs/misc-content.vue
+++ b/packages/docs/src/examples/v-tabs/misc-content.vue
@@ -7,13 +7,9 @@
-
- mdi-magnify
-
+
-
- mdi-dots-vertical
-
+
- Item {{ i }}
-
+ >
-
-
+
{{ text }}
-
-
+
+
diff --git a/packages/docs/src/examples/v-tabs/misc-dynamic-height.vue b/packages/docs/src/examples/v-tabs/misc-dynamic-height.vue
index a4dc590e6be..a2e0619037e 100644
--- a/packages/docs/src/examples/v-tabs/misc-dynamic-height.vue
+++ b/packages/docs/src/examples/v-tabs/misc-dynamic-height.vue
@@ -2,7 +2,6 @@
@@ -24,9 +23,8 @@
- Item {{ n }}
-
+ :text="`Item ${n}`"
+ >
diff --git a/packages/docs/src/examples/v-tabs/misc-dynamic.vue b/packages/docs/src/examples/v-tabs/misc-dynamic.vue
index cca6835ff4c..fe290dc018d 100644
--- a/packages/docs/src/examples/v-tabs/misc-dynamic.vue
+++ b/packages/docs/src/examples/v-tabs/misc-dynamic.vue
@@ -7,29 +7,29 @@
- Item {{ n }}
-
+ >
+
- Remove Tab
-
+ >
+
+
- Add Tab
-
+ >
diff --git a/packages/docs/src/examples/v-tabs/misc-mobile.vue b/packages/docs/src/examples/v-tabs/misc-mobile.vue
index 9d36535411f..3d9835c10c7 100644
--- a/packages/docs/src/examples/v-tabs/misc-mobile.vue
+++ b/packages/docs/src/examples/v-tabs/misc-mobile.vue
@@ -1,19 +1,15 @@
-
+
Contacts
-
- mdi-magnify
-
+
-
- mdi-dots-vertical
-
+
- mdi-phone
+
- mdi-heart
+
- mdi-account-box
+
-
-
+
-
-
+
+
diff --git a/packages/docs/src/examples/v-tabs/misc-overflow-to-menu.vue b/packages/docs/src/examples/v-tabs/misc-overflow-to-menu.vue
index c5d95d4a8de..93b537be551 100644
--- a/packages/docs/src/examples/v-tabs/misc-overflow-to-menu.vue
+++ b/packages/docs/src/examples/v-tabs/misc-overflow-to-menu.vue
@@ -9,13 +9,9 @@
-
- mdi-magnify
-
+
-
- mdi-dots-vertical
-
+
- {{ item }}
-
+ >
-
+
more
-
- mdi-menu-down
-
+
+
@@ -52,18 +44,17 @@
- {{ item }}
-
+ >
-
-
+
-
-
+
+
diff --git a/packages/docs/src/examples/v-tabs/misc-pagination.vue b/packages/docs/src/examples/v-tabs/misc-pagination.vue
index c6a0f07403c..54311a3d4c0 100644
--- a/packages/docs/src/examples/v-tabs/misc-pagination.vue
+++ b/packages/docs/src/examples/v-tabs/misc-pagination.vue
@@ -8,10 +8,9 @@
- Item {{ i }}
-
+ >
diff --git a/packages/docs/src/examples/v-tabs/misc-tab-items.vue b/packages/docs/src/examples/v-tabs/misc-tab-items.vue
index b9afa6164dc..3208dbfeefb 100644
--- a/packages/docs/src/examples/v-tabs/misc-tab-items.vue
+++ b/packages/docs/src/examples/v-tabs/misc-tab-items.vue
@@ -7,9 +7,8 @@
- {{ item.tab }}
-
+ :title="item.tab"
+ >
diff --git a/packages/docs/src/examples/v-tabs/prop-align-tabs-center.vue b/packages/docs/src/examples/v-tabs/prop-align-tabs-center.vue
index 7d503eca5f0..f6c7b87386e 100644
--- a/packages/docs/src/examples/v-tabs/prop-align-tabs-center.vue
+++ b/packages/docs/src/examples/v-tabs/prop-align-tabs-center.vue
@@ -9,8 +9,9 @@
City
Abstract
-
-
+
-
-
+
+
diff --git a/packages/docs/src/examples/v-tabs/prop-align-tabs-end.vue b/packages/docs/src/examples/v-tabs/prop-align-tabs-end.vue
index 9d8e075ab1c..cdb081c8430 100644
--- a/packages/docs/src/examples/v-tabs/prop-align-tabs-end.vue
+++ b/packages/docs/src/examples/v-tabs/prop-align-tabs-end.vue
@@ -9,8 +9,9 @@
City
Abstract
-
-
+
-
-
+
+
diff --git a/packages/docs/src/examples/v-tabs/prop-align-tabs-title.vue b/packages/docs/src/examples/v-tabs/prop-align-tabs-title.vue
index 6cd501b2006..80a979eb1e8 100644
--- a/packages/docs/src/examples/v-tabs/prop-align-tabs-title.vue
+++ b/packages/docs/src/examples/v-tabs/prop-align-tabs-title.vue
@@ -7,13 +7,9 @@
-
- mdi-magnify
-
+
-
- mdi-dots-vertical
-
+
- {{ item }}
-
+ >
-
-
+
-
-
+
+
diff --git a/packages/docs/src/examples/v-tabs/prop-direction.vue b/packages/docs/src/examples/v-tabs/prop-direction.vue
index 29112515403..4b4c8dc2ec7 100644
--- a/packages/docs/src/examples/v-tabs/prop-direction.vue
+++ b/packages/docs/src/examples/v-tabs/prop-direction.vue
@@ -1,37 +1,21 @@
-
- User Profile
+
+
-
-
- mdi-account
-
- Option 1
-
-
-
- mdi-lock
-
- Option 2
-
-
-
- mdi-access-point
-
- Option 3
-
+
+
+
-
-
+
+
+
@@ -47,8 +31,9 @@
-
-
+
+
+
@@ -72,8 +57,9 @@
-
-
+
+
+
@@ -85,8 +71,8 @@
-
-
+
+
diff --git a/packages/docs/src/examples/v-tabs/prop-fixed-tabs.vue b/packages/docs/src/examples/v-tabs/prop-fixed-tabs.vue
index 5aeece5c879..40c53637210 100644
--- a/packages/docs/src/examples/v-tabs/prop-fixed-tabs.vue
+++ b/packages/docs/src/examples/v-tabs/prop-fixed-tabs.vue
@@ -3,11 +3,8 @@
bg-color="indigo-darken-2"
fixed-tabs
>
-
- Option
-
-
- Another Option
-
+
+
+
diff --git a/packages/docs/src/examples/v-tabs/prop-grow.vue b/packages/docs/src/examples/v-tabs/prop-grow.vue
index 6e015e5f11b..eec8625e747 100644
--- a/packages/docs/src/examples/v-tabs/prop-grow.vue
+++ b/packages/docs/src/examples/v-tabs/prop-grow.vue
@@ -15,14 +15,13 @@
- {{ item }}
-
+ >
-
-
+
{{ text }}
-
-
+
+
diff --git a/packages/docs/src/examples/v-tabs/prop-icons.vue b/packages/docs/src/examples/v-tabs/prop-icons.vue
index 6219e73017a..3c5d10caaf2 100644
--- a/packages/docs/src/examples/v-tabs/prop-icons.vue
+++ b/packages/docs/src/examples/v-tabs/prop-icons.vue
@@ -9,9 +9,8 @@
- Item {{ i }}
-
+ :text="`Item ${i}`"
+ >
diff --git a/packages/docs/src/examples/v-tabs/prop-stacked.vue b/packages/docs/src/examples/v-tabs/prop-stacked.vue
index 993f5edb995..db0ba0eafe0 100644
--- a/packages/docs/src/examples/v-tabs/prop-stacked.vue
+++ b/packages/docs/src/examples/v-tabs/prop-stacked.vue
@@ -7,23 +7,26 @@
stacked
>
- mdi-phone
+
+
Recents
- mdi-heart
+
+
Favorites
- mdi-account-box
+
+
Nearby
-
-
+
{{ text }}
-
-
+
+
diff --git a/packages/docs/src/examples/v-tabs/slot-tabs.vue b/packages/docs/src/examples/v-tabs/slot-tabs.vue
new file mode 100644
index 00000000000..5888db54568
--- /dev/null
+++ b/packages/docs/src/examples/v-tabs/slot-tabs.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Commodi, ratione debitis quis est labore voluptatibus! Eaque cupiditate minima, at placeat totam, magni doloremque veniam neque porro libero rerum unde voluptatem!
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/examples/v-tabs/usage.vue b/packages/docs/src/examples/v-tabs/usage.vue
index d3e4df2f719..d3e8bf85626 100644
--- a/packages/docs/src/examples/v-tabs/usage.vue
+++ b/packages/docs/src/examples/v-tabs/usage.vue
@@ -10,19 +10,19 @@
-
-
+
+
One
-
+
-
+
Two
-
+
-
+
Three
-
-
+
+
diff --git a/packages/docs/src/pages/en/components/tabs.md b/packages/docs/src/pages/en/components/tabs.md
index f24ae4fcd10..75383cdfba1 100644
--- a/packages/docs/src/pages/en/components/tabs.md
+++ b/packages/docs/src/pages/en/components/tabs.md
@@ -124,3 +124,17 @@ Tabs can be dynamically added and removed. In this example when we add a new tab
You can use a menu to hold additional tabs, swapping them out on the fly.
+
+### Slots
+
+#### Tab and window items
+
+Use the **tab** and **item** slots with the **items** prop to reduce the markup required to build tabs.
+
+::: success
+
+This feature was introduced in [v3.6.0 (Nebula)](/getting-started/release-notes/?version=v3.6.0)
+
+:::
+
+
diff --git a/packages/docs/src/plugins/icons.ts b/packages/docs/src/plugins/icons.ts
index c0cf8a9cd90..18f53826e17 100644
--- a/packages/docs/src/plugins/icons.ts
+++ b/packages/docs/src/plugins/icons.ts
@@ -48,6 +48,7 @@ export {
mdiBookmark,
mdiBookmarkMinus,
mdiBookmarkOutline,
+ mdiBookOpenPageVariant,
mdiBookVariant,
mdiBottleTonicPlus,
mdiBriefcase,
@@ -194,6 +195,7 @@ export {
mdiGithub,
mdiGlassWine,
mdiGoogleNearby,
+ mdiHandshakeOutline,
mdiHeadQuestionOutline,
mdiHeart,
mdiHeartOutline,
@@ -222,6 +224,7 @@ export {
mdiLayersOutline,
mdiLayersTriple,
mdiLeaf,
+ mdiLicense,
mdiLifebuoy,
mdiLightbulbOnOutline,
mdiLink,
diff --git a/packages/vuetify/src/components/VTabs/VTabs.tsx b/packages/vuetify/src/components/VTabs/VTabs.tsx
index bd6b47665ba..df42d23bb31 100644
--- a/packages/vuetify/src/components/VTabs/VTabs.tsx
+++ b/packages/vuetify/src/components/VTabs/VTabs.tsx
@@ -3,6 +3,8 @@ import './VTabs.sass'
// Components
import { VTab } from './VTab'
+import { VTabsWindow } from './VTabsWindow'
+import { VTabsWindowItem } from './VTabsWindowItem'
import { makeVSlideGroupProps, VSlideGroup } from '@/components/VSlideGroup/VSlideGroup'
// Composables
@@ -22,6 +24,20 @@ import { VTabsSymbol } from './shared'
export type TabItem = string | number | Record
+export type VTabsSlot = {
+ item: TabItem
+}
+
+export type VTabsSlots = {
+ default: never
+ tab: VTabsSlot
+ item: VTabsSlot
+ window: never
+} & {
+ [key: `tab.${string}`]: VTabsSlot
+ [key: `item.${string}`]: VTabsSlot
+}
+
function parseItems (items: readonly TabItem[] | undefined) {
if (!items) return []
@@ -53,12 +69,15 @@ export const makeVTabsProps = propsFactory({
hideSlider: Boolean,
sliderColor: String,
- ...makeVSlideGroupProps({ mandatory: 'force' as const }),
+ ...makeVSlideGroupProps({
+ mandatory: 'force' as const,
+ selectedClass: 'v-tab-item--selected',
+ }),
...makeDensityProps(),
...makeTagProps(),
}, 'VTabs')
-export const VTabs = genericComponent()({
+export const VTabs = genericComponent()({
name: 'VTabs',
props: makeVTabsProps(),
@@ -69,7 +88,7 @@ export const VTabs = genericComponent()({
setup (props, { slots }) {
const model = useProxiedModel(props, 'modelValue')
- const parsedItems = computed(() => parseItems(props.items))
+ const items = computed(() => parseItems(props.items))
const { densityClasses } = useDensity(props)
const { backgroundColorClasses, backgroundColorStyles } = useBackgroundColor(toRef(props, 'bgColor'))
@@ -86,36 +105,66 @@ export const VTabs = genericComponent()({
useRender(() => {
const slideGroupProps = VSlideGroup.filterProps(props)
+ const hasWindow = !!(slots.window || props.items.length > 0)
return (
-
- { slots.default ? slots.default() : parsedItems.value.map(item => (
-
- ))}
-
+ <>
+
+ { slots.default?.() ?? items.value.map(item => (
+ slots.tab?.({ item }) ?? (
+ slots[`tab.${item.value}`]?.({ item }),
+ }}
+ />
+ )
+ ))}
+
+
+ { hasWindow && (
+
+ { items.value.map(item => slots.item?.({ item }) ?? (
+ slots[`item.${item.value}`]?.({ item }),
+ }}
+ />
+ ))}
+
+ { slots.window?.() }
+
+ )}
+ >
)
})
diff --git a/packages/vuetify/src/components/VTabs/VTabsWindow.tsx b/packages/vuetify/src/components/VTabs/VTabsWindow.tsx
new file mode 100644
index 00000000000..ea39d0b36a6
--- /dev/null
+++ b/packages/vuetify/src/components/VTabs/VTabsWindow.tsx
@@ -0,0 +1,66 @@
+// Components
+import { makeVWindowProps, VWindow } from '@/components/VWindow/VWindow'
+
+// Composables
+import { useProxiedModel } from '@/composables/proxiedModel'
+
+// Utilities
+import { computed, inject } from 'vue'
+import { genericComponent, omit, propsFactory, useRender } from '@/util'
+
+// Types
+import { VTabsSymbol } from './shared'
+
+export const makeVTabsWindowProps = propsFactory({
+ ...omit(makeVWindowProps(), ['continuous', 'nextIcon', 'prevIcon', 'showArrows', 'touch', 'mandatory']),
+}, 'VTabsWindow')
+
+export const VTabsWindow = genericComponent()({
+ name: 'VTabsWindow',
+
+ props: makeVTabsWindowProps(),
+
+ emits: {
+ 'update:modelValue': (v: unknown) => true,
+ },
+
+ setup (props, { slots }) {
+ const group = inject(VTabsSymbol, null)
+ const _model = useProxiedModel(props, 'modelValue')
+
+ const model = computed({
+ get () {
+ // Always return modelValue if defined
+ // or if not within a VTabs group
+ if (_model.value != null || !group) return _model.value
+
+ // If inside of a VTabs, find the currently selected
+ // item by id. Item value may be assigned by its index
+ return group.items.value.find(item => group.selected.value.includes(item.id))?.value
+ },
+ set (val) {
+ _model.value = val
+ },
+ })
+
+ useRender(() => {
+ const windowProps = VWindow.filterProps(props)
+
+ return (
+
+ )
+ })
+
+ return {}
+ },
+})
+
+export type VTabsWindow = InstanceType
diff --git a/packages/vuetify/src/components/VTabs/VTabsWindowItem.tsx b/packages/vuetify/src/components/VTabs/VTabsWindowItem.tsx
new file mode 100644
index 00000000000..eebd10644a3
--- /dev/null
+++ b/packages/vuetify/src/components/VTabs/VTabsWindowItem.tsx
@@ -0,0 +1,38 @@
+// Components
+import { makeVWindowItemProps, VWindowItem } from '@/components/VWindow/VWindowItem'
+
+// Utilities
+import { genericComponent, propsFactory, useRender } from '@/util'
+
+export const makeVTabsWindowItemProps = propsFactory({
+ ...makeVWindowItemProps(),
+}, 'VTabsWindowItem')
+
+export const VTabsWindowItem = genericComponent()({
+ name: 'VTabsWindowItem',
+
+ props: makeVTabsWindowItemProps(),
+
+ setup (props, { slots }) {
+ useRender(() => {
+ const windowItemProps = VWindowItem.filterProps(props)
+
+ return (
+
+ )
+ })
+
+ return {}
+ },
+})
+
+export type VTabsWindowItem = InstanceType
diff --git a/packages/vuetify/src/components/VTabs/index.ts b/packages/vuetify/src/components/VTabs/index.ts
index e3a24f56450..c137dab2d86 100644
--- a/packages/vuetify/src/components/VTabs/index.ts
+++ b/packages/vuetify/src/components/VTabs/index.ts
@@ -1,2 +1,4 @@
-export { VTabs } from './VTabs'
export { VTab } from './VTab'
+export { VTabs } from './VTabs'
+export { VTabsWindow } from './VTabsWindow'
+export { VTabsWindowItem } from './VTabsWindowItem'