Guides
shadcn-vue
Use vccs with shadcn-vue's chart components
vccs works with the shadcn-vue chart pattern (ChartContainer, ChartTooltipContent, ChartLegendContent, ChartConfig). The container maps vccs's CSS variables to your shadcn theme once, so charts follow light and dark mode with no attribute overrides.
ChartContainer
components/ui/chart/ChartContainer.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { ChartConfig } from './types'
import { cn } from '@/lib/utils'
import ChartStyle from './ChartStyle.vue'
const props = defineProps<{
id?: string
class?: HTMLAttributes['class']
config: ChartConfig
}>()
const uniqueId = useId()
const chartId = computed(() => `chart-${props.id || uniqueId.replace(/:/g, '')}`)
provide('chart-config', computed(() => props.config))
</script>
<template>
<!-- vccs reads its default colors from --v-charts-* variables; map them to the theme once. -->
<div
data-slot="chart"
:data-chart="chartId"
:class="cn(
'flex aspect-video justify-center text-xs',
'[--v-charts-grid:var(--border)] [--v-charts-axis:var(--border)] [--v-charts-text:var(--muted-foreground)]',
'[--v-charts-cursor:var(--border)] [--v-charts-muted:var(--muted)] [--v-charts-background:var(--background)]',
'[--v-charts-inactive:var(--muted-foreground)] [--v-charts-focus:var(--ring)]',
'[&_.v-charts-surface]:outline-hidden [&_.v-charts-layer]:outline-hidden [&_.v-charts-sector]:outline-hidden',
props.class,
)"
>
<ChartStyle
:id="chartId"
:config="config"
/>
<slot />
</div>
</template>
components/ui/chart/ChartStyle.vue
<script setup lang="ts">
import type { ChartConfig } from './types'
const props = defineProps<{
id: string
config: ChartConfig
}>()
// Same contract as shadcn: `color` for both themes, or `theme: { light, dark }`.
const THEMES = { light: '', dark: '.dark' } as const
// A literal <style> in a template is stripped by the SFC compiler.
const styleTag = 'style'
const css = computed(() => {
const entries = Object.entries(props.config).filter(([, item]) => item.theme || item.color)
if (!entries.length)
return ''
return Object.entries(THEMES).map(([theme, prefix]) => {
const vars = entries
.map(([key, item]) => {
const color = item.theme?.[theme as keyof typeof THEMES] ?? item.color
return color ? ` --color-${key}: ${color};` : null
})
.filter(Boolean)
.join('\n')
return `${prefix} [data-chart=${props.id}] {\n${vars}\n}`
}).join('\n')
})
</script>
<template>
<component
:is="styleTag"
v-if="css"
>
{{ css }}
</component>
</template>
Use it
<script setup lang="ts">
import { Area, AreaChart, CartesianGrid, Tooltip, XAxis } from 'vccs'
import { type ChartConfig, ChartContainer, ChartTooltipContent } from '@/components/ui/chart'
const chartConfig = {
desktop: { label: 'Desktop', color: 'var(--chart-1)' },
mobile: { label: 'Mobile', color: 'var(--chart-2)' },
} satisfies ChartConfig
</script>
<template>
<ChartContainer
:config="chartConfig"
class="aspect-auto h-[250px] w-full"
>
<AreaChart :data="data">
<CartesianGrid :vertical="false" />
<XAxis
data-key="date"
:tick-line="false"
:axis-line="false"
/>
<Tooltip :cursor="false">
<template #content="{ active, payload, label }">
<ChartTooltipContent
:active="active"
:payload="payload"
:label="label"
/>
</template>
</Tooltip>
<Area
data-key="mobile"
fill="var(--color-mobile)"
stroke="var(--color-mobile)"
stack-id="a"
/>
<Area
data-key="desktop"
fill="var(--color-desktop)"
stroke="var(--color-desktop)"
stack-id="a"
/>
</AreaChart>
</ChartContainer>
</template>
The chart fills the container; no ResponsiveContainer is needed. The container's default 16:9 box matches the size charts render at on the server.