- Accordion
- Alert
- Alert Dialog
- Badge
- Breadcrumbs
- Button Group
- Button
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- File Field
- Hover Card
- Kbd
- Menubar
- Navigation Menu
- Number Field
- OTP Field
- Pagination
- Popover
- Progress
- Radio Group
- Resizable
- Search
- Segmented Control
- Select
- Separator
- Sidebar
- Skeleton
- Slider
- Sonner
- Switch
- Table
- Tabs
- Text Field
- Toggle Group
- Toggle Button
- Tooltip
Component bar-chart-interactive not found in registry.
Introducing Charts. A collection of chart components that you can copy and paste into your apps.
Charts are designed to look great out of the box. They work well with the other components and are fully customizable to fit your project.
Component
We use Unovis under the hood.
We designed the chart component with composition in mind. You build your charts using Unovis components and bring in custom components, such as ChartTooltipContent, when and where you need it.
import { VisArea, VisTooltip } from "@unovis/solid"
import {
ChartContainer,
ChartCrosshair,
ChartTooltipContent,
} from "@/components/ui/charts"
const MyChart = () => {
return (
<ChartContainer data={data} type="xy" config={chartConfig}>
<VisArea
...
/>
<ChartCrosshair
...
template={(props) => (
<ChartTooltipContent labelKey="month" indicator="line" {...props} />
)}
/>
<VisTooltip ... />
</ChartContainer>
)
}
export default MyChartInstallation
Install the following dependencies:
Copy and paste the following code into your project:
import {
For,
Match,
Show,
Switch,
createContext,
mergeProps,
splitProps,
useContext,
type JSX,
} from "solid-js"
import { render } from "solid-js/web"
import type { VisCrosshairProps } from "@unovis/solid"
import {
VisCrosshair,
VisSingleContainer,
VisXYContainer,
type VisSingleContainerProps,
type VisXYContainerProps,
} from "@unovis/solid"
import { cx } from "@/registry/lib/cva"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: '[data-kb-theme="dark"]' } as const
export type ChartConfig = Record<
string,
{
label?: JSX.Element
icon?: JSX.Element
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
>
interface ChartContextProps {
config: ChartConfig
}
const ChartContext = createContext<ChartContextProps>()
const useChart = () => {
const context = useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
type SingleContainerProps<T> = VisSingleContainerProps<T> & {
type: "single"
}
type XYContainerProps<T> = VisXYContainerProps<T> & {
type: "xy"
}
export type ChartContainerProps<T> = (
| XYContainerProps<T>
| SingleContainerProps<T>
) &
ChartContextProps
export const ChartContainer = <T,>(props: ChartContainerProps<T>) => {
const [, rest] = splitProps(props, ["config", "children", "type", "class"])
return (
<ChartContext.Provider
value={{
get config() {
return props.config
},
}}
>
<div
data-slot="chart"
class={cx("flex aspect-video justify-center", props.class)}
>
<Switch>
<Match when={props.type === "xy"}>
<VisXYContainer {...(rest as Omit<XYContainerProps<T>, "type">)}>
<ChartStyle type="xy" config={props.config} />
{props.children}
</VisXYContainer>
</Match>
<Match when={props.type === "single"}>
<VisSingleContainer
{...(rest as Omit<SingleContainerProps<T>, "type">)}
>
<ChartStyle type="single" config={props.config} />
{props.children}
</VisSingleContainer>
</Match>
</Switch>
</div>
</ChartContext.Provider>
)
}
export type ChartStyleProps = {
type: "xy" | "single"
} & Omit<ChartContextProps, "data">
export const ChartStyle = (props: ChartStyleProps) => {
const colorConfig = () =>
Object.entries(props.config).filter(
([, config]) => config.theme ?? config.color,
)
return (
<Show when={colorConfig().length}>
<style>
{Object.entries(THEMES)
.map(([theme, prefix]) => {
const colorVars = colorConfig()
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ??
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")
return `${prefix} [data-vis-${props.type}-container] {\n${colorVars}\n}`
})
.join("\n\n")}
</style>
</Show>
)
}
export type ChartCrosshairProps<T> = Omit<VisCrosshairProps<T>, "template"> & {
template?: (
props: {
data: T
x: number | Date
} & ChartContextProps,
) => JSX.Element
}
export const ChartCrosshair = <T,>(props: ChartCrosshairProps<T>) => {
const [, rest] = splitProps(props, ["template"])
const { config } = useChart()
const template = (d: T, x: number | Date) => {
const container = document.createElement("div")
const Component = () =>
!props.template ? null : props.template({ data: d, x, config })
render(() => <Component />, container)
return container.innerHTML
}
return <VisCrosshair template={template} {...rest} />
}
type InferLabelKey<T, C> = C extends ChartConfig
? ChartConfig extends C
? keyof T
: keyof C
: never
const getConfigFromData = <T, C extends ChartConfig = ChartConfig>(
data: T,
config: ChartConfig,
labelKey?: InferLabelKey<T, C>,
nameKey?: C extends undefined ? never : keyof C,
) => {
const valueKeys =
// @ts-expect-error
Object.entries(data)
.filter(
([key, value]) =>
key !== labelKey &&
(typeof value === "number" || typeof value === "object"),
)
.filter(([key]) => !key.includes("_"))
.map(([key]) => key)
const items = valueKeys.map((key) => {
const configItem = config[key]
let color = configItem.color
// @ts-expect-error
if (!color && "fill" in data) {
color = data.fill as string
}
const rawValue = data[key as keyof T]
const value =
typeof rawValue === "object" && rawValue !== null
? Object.values(rawValue).find((v) => typeof v === "number")
: (rawValue as number)
return {
value,
key: nameKey ? config[nameKey].label : configItem.label,
icon: configItem.icon,
color,
}
})
const label = data[labelKey as keyof T] ?? config[labelKey as keyof C].label
return {
label,
items,
}
}
export type ChartTooltipContentProps<T, C extends ChartConfig = ChartConfig> = {
data: T
x: number | Date
labelKey: InferLabelKey<T, C>
class?: string
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: C extends undefined ? never : keyof C
labelFormatter?: (data: number | Date) => JSX.Element
labelAsKey?: boolean
formatter?: (
value: number,
name: JSX.Element,
item: T,
index: number,
) => JSX.Element
} & ChartContextProps
export const ChartTooltipContent = <T, C extends ChartConfig = ChartConfig>(
props: ChartTooltipContentProps<T, C>,
) => {
const merge = mergeProps(
{
hideLabel: false,
hideIndicator: false,
indicator: "dot",
labelAsKey: false,
} satisfies Partial<ChartTooltipContentProps<T, C>>,
props,
)
const value = () =>
getConfigFromData<T, C>(
merge.data,
merge.config,
merge.labelKey,
merge.nameKey,
)
const tooltipLabel = () => {
if (merge.hideLabel || !value().items.length) {
return null
}
return (
<div class="font-medium capitalize">
<Show
when={!merge.labelFormatter}
fallback={merge.labelFormatter!(
typeof merge.x === "number" ? Math.round(merge.x) : merge.x,
)}
>
{value().label as JSX.Element}
</Show>
</div>
)
}
const nestLabel = () =>
value().items.length === 1 && merge.indicator !== "dot"
return (
<div
class={cx("grid min-w-[8rem] items-start gap-1.5 text-xs", merge.class)}
>
<Show when={!nestLabel()}>{tooltipLabel()}</Show>
<div class="grid gap-1.5">
<For each={value().items}>
{(item, index) => (
<div
class={cx(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5",
merge.indicator === "dot" && "items-center",
)}
>
<Show
when={!merge.formatter}
fallback={merge.formatter!(
item.value!,
item.key,
merge.data,
index(),
)}
>
<Show when={item.icon}>{item.icon}</Show>
<Show when={!item.icon && !merge.hideIndicator}>
<div
class={cx(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"size-2.5": merge.indicator === "dot",
"w-1": merge.indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
merge.indicator === "dashed",
"my-0.5": nestLabel() && merge.indicator === "dashed",
},
)}
style={{
"--color-border": item.color,
"--color-bg": item.color,
}}
/>
</Show>
<div
class={cx(
"flex flex-1 justify-between gap-1.5 leading-none",
nestLabel() ? "items-end" : "items-center",
)}
>
<div class="grid gap-1.5">
<Show when={nestLabel()}>{tooltipLabel()}</Show>
<span class="text-muted-foreground capitalize">
<Show
when={!merge.labelAsKey}
fallback={value().label as string}
>
{item.key}
</Show>
</span>
</div>
<span class="text-foreground font-mono font-medium tabular-nums">
{item.value}
</span>
</div>
</Show>
</div>
)}
</For>
</div>
</div>
)
}Add the following colors to your CSS file
@theme inline {
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
}
:root {
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--vis-tooltip-padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 2.5);
--vis-tooltip-box-shadow: var(--shadow-xl);
--vis-tooltip-background-color: var(--color-background);
--vis-tooltip-border-color: var(--color-border);
--vis-tooltip-border-radius: var(--radius-lg);
--vis-tooltip-text-color: var(--color-foreground);
--vis-font-family: var(--font-sans);
--vis-legend-label-color: var(--vis-tooltip-text-color);
}
[data-kb-theme="dark"] {
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
}Chart Config
The chart config is where you define the labels, icons and colors for a chart.
It is intentionally decoupled from chart data.
This allows you to share config and color tokens between charts. It can also works independently for cases where your data or color tokens live remotely or in a different format.
import { Monitor } from "your-icon-library"
import { type ChartConfig } from "@/components/ui/chart"
const chartConfig = {
desktop: {
label: "Desktop",
icon: Monitor,
// A color like 'hsl(220, 98%, 61%)' or 'var(--color-name)'
color: "#2563eb",
// OR a theme object with 'light' and 'dark' keys
theme: {
light: "#2563eb",
dark: "#dc2626",
},
},
} satisfies ChartConfigTheming
Charts has built-in support for theming. You can use css variables (recommended) or color values in any color format, such as hex, hsl or oklch.
CSS Variables
Define your colors in your css file
@theme inline {
...
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
}
:root {
...
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--vis-tooltip-padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 2.5);
--vis-tooltip-box-shadow: var(--shadow-xl);
--vis-tooltip-background-color: var(--color-background);
--vis-tooltip-border-color: var(--color-border);
--vis-tooltip-border-radius: var(--radius-lg);
--vis-tooltip-text-color: var(--color-foreground);
--vis-font-family: var(--font-sans);
--vis-legend-label-color: var(--vis-tooltip-text-color);
}
[data-kb-theme="dark"] {
...
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
}Add the color to your chartConfig
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--chart-1)",
},
mobile: {
label: "Mobile",
color: "var(--chart-2)",
},
} satisfies ChartConfighex, hsl or oklch
You can also define your colors directly in the chart config. Use the color format you prefer.
const chartConfig = {
desktop: {
label: "Desktop",
color: "#2563eb",
},
} satisfies ChartConfigUsing Colors
To use the theme colors in your chart, reference the colors using the format var(--color-KEY).
Components
<VisArea ... color="var(--color-desktop)" />Chart Data
const data = [
{ browser: "chrome", visitors: 275, fill: "var(--color-chrome)" },
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
]Tooltip
A chart tooltip contains a label, name, indicator and value. You can use a combination of these to customize your tooltip.
Component chart-tooltip-demo not found in registry.
You can turn on/off any of these using the hideLabel, hideIndicator props and customize the indicator style using the indicator prop.
Use labelKey and nameKey to use a custom key for the tooltip label and name.
Chart comes with the <ChartCrosshair> and <ChartTooltipContent> components. You can use these two components to add custom tooltips to your chart.
import { ChartCrosshair, ChartTooltipContent } from "@/components/ui/chart"<ChartCrosshair
template={(props) => (
<ChartTooltipContent labelKey="month" indicator="line" {...props} />
)}
/>Or
<VisTooltip
triggers={{
[StackedBar.selectors.bar]: (d: DataRecord, x) => {
const container = document.createElement("div")
const Component = () => (
<ChartTooltipContent
data={d}
x={x}
config={chartConfig}
labelKey="month"
/>
)
render(() => <Component />, container)
return container.innerHTML
},
}}
/>Props
Use the following props to customize the tooltip.
| Prop | Type | Description |
|---|---|---|
labelKey | string | The config or data key to use for the label. |
nameKey | string | The config or data key to use for the name. |
indicator | dot line or dashed | The indicator style for the tooltip. |
hideLabel | boolean | Whether to hide the label. |
hideIndicator | boolean | Whether to hide the indicator. |
Colors
Colors are automatically referenced from the chart config.
Custom
To use a custom key for tooltip label and names, use the labelKey and nameKey props.
const data = [
{ browser: "chrome", visitors: 187, fill: "var(--color-chrome)" },
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
]
const chartConfig = {
visitors: {
label: "Total Visitors",
},
chrome: {
label: "Chrome",
color: "var(--chart-1)",
},
safari: {
label: "Safari",
color: "var(--chart-2)",
},
} satisfies ChartConfig<ChartCrosshair
template={(props) => <ChartTooltipContent labelKey="visitors" {...props} />}
/>This will use Total Visitors for label and Chrome and Safari for the tooltip names.