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.

Copyright © 2026