ContentToc
Usage
Use the links
prop with the page?.body?.toc?.links
you get when fetching a page.
<script setup lang="ts">
const route = useRoute()
const { data: page } = await useAsyncData(route.path, () => queryCollection('content').path(route.path).first())
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
</script>
<template>
<UContentToc :links="page?.body?.toc?.links" />
</template>
Title
Use the title
prop to change the title of the Table of Contents.
<script setup lang="ts">
const links = ref([
{
id: 'usage',
depth: 2,
text: 'Usage',
children: [
{
id: 'title',
depth: 3,
text: 'Title'
},
{
id: 'color',
depth: 3,
text: 'Color'
},
{
id: 'highlight',
depth: 3,
text: 'Highlight'
}
]
},
{
id: 'api',
depth: 2,
text: 'API',
children: [
{
id: 'props',
depth: 3,
text: 'Props'
},
{
id: 'slots',
depth: 3,
text: 'Slots'
}
]
},
{
id: 'theme',
depth: 2,
text: 'Theme'
}
])
</script>
<template>
<UContentToc title="On this page" :links="links" />
</template>
Color
Use the color
prop to change the color of the links.
<script setup lang="ts">
const links = ref([
{
id: 'usage',
depth: 2,
text: 'Usage',
children: [
{
id: 'title',
depth: 3,
text: 'Title'
},
{
id: 'color',
depth: 3,
text: 'Color'
},
{
id: 'highlight',
depth: 3,
text: 'Highlight'
}
]
}
])
</script>
<template>
<UContentToc color="neutral" :links="links" />
</template>
Highlight
Use the highlight
prop to display a highlighted border for the active item.
Use the highlight-color
prop to change the color of the border. It defaults to the color
prop.
<script setup lang="ts">
const links = ref([
{
id: 'usage',
depth: 2,
text: 'Usage',
children: [
{
id: 'title',
depth: 3,
text: 'Title'
},
{
id: 'color',
depth: 3,
text: 'Color'
},
{
id: 'highlight',
depth: 3,
text: 'Highlight'
}
]
}
])
</script>
<template>
<UContentToc highlight highlight-color="neutral" color="neutral" :links="links" />
</template>
Examples
Within a page
Use the ContentToc component in a page to display the Table of Contents:
<script setup lang="ts">
const route = useRoute()
const { data: page } = await useAsyncData(route.path, () => queryCollection('content').path(route.path).first())
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
</script>
<template>
<UPage v-if="page">
<UPageHeader :title="page.title" />
<UPageBody>
<ContentRenderer v-if="page.body" :value="page" />
<USeparator v-if="surround?.filter(Boolean).length" />
<UContentSurround :surround="(surround as any)" />
</UPageBody>
<template v-if="page?.body?.toc?.links?.length" #right>
<UContentToc :links="page.body.toc.links" />
</template>
</UPage>
</template>
API
Props
Prop | Default | Type |
---|---|---|
as |
|
The element or component this component should render as. |
trailingIcon |
|
The icon displayed to collapse the content. |
title |
|
The title of the table of contents. |
color |
|
|
highlight |
|
Display a line next to the active link. |
highlightColor |
|
|
links |
| |
defaultOpen |
The open state of the collapsible when it is initially rendered. | |
open |
The controlled open state of the collapsible. Can be binded with | |
ui |
|
Slots
Slot | Type |
---|---|
leading |
|
default |
|
trailing |
|
content |
|
link |
|
top |
|
bottom |
|
Emits
Event | Type |
---|---|
update:open |
|
move |
|
Theme
export default defineAppConfig({
uiPro: {
contentToc: {
slots: {
root: 'sticky top-[calc(var(--ui-header-height)+1px)] bg-(--ui-bg)/75 lg:bg-[initial] backdrop-blur -mx-4 px-4 sm:px-6 sm:-mx-6 overflow-y-auto max-h-[calc(100vh-var(--ui-header-height))]',
container: 'pt-4 sm:pt-6 pb-2.5 sm:pb-4.5 lg:py-8 border-b border-dashed border-(--ui-border) lg:border-0 flex flex-col',
top: '',
bottom: 'mt-6 hidden lg:flex lg:flex-col gap-6',
trigger: 'group text-sm font-semibold flex-1 flex items-center gap-1.5 py-1.5 -mt-1.5 focus-visible:outline-(--ui-primary)',
title: 'truncate',
trailing: 'ms-auto inline-flex gap-1.5 items-center',
trailingIcon: 'size-5 transform transition-transform duration-200 shrink-0 group-data-[state=open]:rotate-180 lg:hidden',
content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden focus:outline-none',
list: '',
listWithChildren: 'ms-3',
item: '',
itemWithChildren: '',
link: 'group text-sm block truncate focus-visible:outline-(--ui-primary) py-1',
indicator: 'absolute ms-2.5 transition-[translate,height] duration-200 h-(--indicator-size) translate-y-(--indicator-position) w-px rounded-full'
},
variants: {
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
highlightColor: {
primary: {
indicator: 'bg-(--ui-primary)'
},
secondary: {
indicator: 'bg-(--ui-secondary)'
},
success: {
indicator: 'bg-(--ui-success)'
},
info: {
indicator: 'bg-(--ui-info)'
},
warning: {
indicator: 'bg-(--ui-warning)'
},
error: {
indicator: 'bg-(--ui-error)'
},
neutral: {
indicator: 'bg-(--ui-bg-inverted)'
}
},
active: {
false: {
link: [
'text-(--ui-text-muted) hover:text-(--ui-text)',
'transition-colors'
]
}
},
highlight: {
true: {
list: 'ms-2.5 ps-4 border-s border-(--ui-border)',
item: '-ms-px'
}
}
},
compoundVariants: [
{
color: 'primary',
active: true,
class: {
link: 'text-(--ui-primary)',
linkLeadingIcon: 'text-(--ui-primary)'
}
},
{
color: 'neutral',
active: true,
class: {
link: 'text-(--ui-text-highlighted)',
linkLeadingIcon: 'text-(--ui-text-highlighted)'
}
}
],
defaultVariants: {
color: 'primary',
highlightColor: 'primary'
}
}
}
})
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
uiPro: {
contentToc: {
slots: {
root: 'sticky top-[calc(var(--ui-header-height)+1px)] bg-(--ui-bg)/75 lg:bg-[initial] backdrop-blur -mx-4 px-4 sm:px-6 sm:-mx-6 overflow-y-auto max-h-[calc(100vh-var(--ui-header-height))]',
container: 'pt-4 sm:pt-6 pb-2.5 sm:pb-4.5 lg:py-8 border-b border-dashed border-(--ui-border) lg:border-0 flex flex-col',
top: '',
bottom: 'mt-6 hidden lg:flex lg:flex-col gap-6',
trigger: 'group text-sm font-semibold flex-1 flex items-center gap-1.5 py-1.5 -mt-1.5 focus-visible:outline-(--ui-primary)',
title: 'truncate',
trailing: 'ms-auto inline-flex gap-1.5 items-center',
trailingIcon: 'size-5 transform transition-transform duration-200 shrink-0 group-data-[state=open]:rotate-180 lg:hidden',
content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden focus:outline-none',
list: '',
listWithChildren: 'ms-3',
item: '',
itemWithChildren: '',
link: 'group text-sm block truncate focus-visible:outline-(--ui-primary) py-1',
indicator: 'absolute ms-2.5 transition-[translate,height] duration-200 h-(--indicator-size) translate-y-(--indicator-position) w-px rounded-full'
},
variants: {
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
highlightColor: {
primary: {
indicator: 'bg-(--ui-primary)'
},
secondary: {
indicator: 'bg-(--ui-secondary)'
},
success: {
indicator: 'bg-(--ui-success)'
},
info: {
indicator: 'bg-(--ui-info)'
},
warning: {
indicator: 'bg-(--ui-warning)'
},
error: {
indicator: 'bg-(--ui-error)'
},
neutral: {
indicator: 'bg-(--ui-bg-inverted)'
}
},
active: {
false: {
link: [
'text-(--ui-text-muted) hover:text-(--ui-text)',
'transition-colors'
]
}
},
highlight: {
true: {
list: 'ms-2.5 ps-4 border-s border-(--ui-border)',
item: '-ms-px'
}
}
},
compoundVariants: [
{
color: 'primary',
active: true,
class: {
link: 'text-(--ui-primary)',
linkLeadingIcon: 'text-(--ui-primary)'
}
},
{
color: 'neutral',
active: true,
class: {
link: 'text-(--ui-text-highlighted)',
linkLeadingIcon: 'text-(--ui-text-highlighted)'
}
}
],
defaultVariants: {
color: 'primary',
highlightColor: 'primary'
}
}
}
})
]
})