Bklit UI
componentbackgroundsource: ok · hit
Chart Background
Pattern fill for the plot area when grid lines are hidden
npx shadcn add "https://ui.bklit.com/r/background.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumanimationmotion
Registry dependencies
@bklit/chart-context
Files3 · source cached 10/9/2026, 4:13:57 PM
src/charts/background.tsx
"use client";
import { motion } from "motion/react";
import { useId, useMemo } from "react";
import { useChartStable } from "./chart-context";
import {
type PatternPresetId,
type PatternPresetOptions,
renderPatternPreset,
} from "./pattern-preset";
export type BackgroundPatternPreset = PatternPresetId;
export interface BackgroundProps extends PatternPresetOptions {
/** Pattern preset. `"none"` renders nothing. */
pattern?: BackgroundPatternPreset;
/** Pattern stroke color. Default: `var(--chart-grid)` */
color?: string;
/** Apply the pattern texture to the plot area. Default: true */
showFill?: boolean;
/** Pattern fill opacity. Default: 1 */
opacity?: number;
/** Fade pattern at the left and right chart edges. Default: true */
fadeHorizontal?: boolean;
/** Fade pattern at the top and bottom chart edges. Default: true */
fadeVertical?: boolean;
/** Horizontal fade zone as % of plot width per edge. Default: 10 */
fadeHorizontalLength?: number;
/** Vertical fade zone as % of plot height per edge. Default: 10 */
fadeVerticalLength?: number;
}
const BACKGROUND_ENTER_FADE_MS = 420;
function clampFadeLength(length: number): number {
return Math.min(45, Math.max(0, length));
}
function fadeMaskStops(lengthPercent: number): Array<{
offset: string;
opacity: number;
}> {
const edge = clampFadeLength(lengthPercent);
return [
{ offset: "0%", opacity: 0 },
{ offset: `${edge}%`, opacity: 1 },
{ offset: `${100 - edge}%`, opacity: 1 },
{ offset: "100%", opacity: 0 },
];
}
/** Plot-area pattern fill for charts without a grid. Renders behind series layers. */
export function Background({
pattern = "diagonal",
color = "var(--chart-grid)",
scale = 1,
strokeWidth,
radius,
complement,
fill,
dotFill,
tileBackground,
showFill = true,
opacity = 1,
fadeHorizontal = true,
fadeVertical = true,
fadeHorizontalLength = 10,
fadeVerticalLength = 10,
}: BackgroundProps) {
const { innerWidth, innerHeight, isLoaded, enterTransition } =
useChartStable();
const uniqueId = useId();
const patternId = `chart-background-${uniqueId.replace(/:/g, "")}`;
const hStops = useMemo(
() => fadeMaskStops(fadeHorizontalLength),
[fadeHorizontalLength]
);
const vStops = useMemo(
() => fadeMaskStops(fadeVerticalLength),
[fadeVerticalLength]
);
if (pattern === "none" || !showFill || innerWidth <= 0 || innerHeight <= 0) {
return null;
}
const patternNode = renderPatternPreset(pattern, patternId, {
color,
scale,
strokeWidth,
radius,
complement,
fill,
dotFill,
tileBackground,
});
if (!patternNode) {
return null;
}
const fadeMask = fadeHorizontal || fadeVertical;
const hMaskId = `chart-background-fade-h-${uniqueId.replace(/:/g, "")}`;
const hGradientId = `${hMaskId}-gradient`;
const vMaskId = `chart-background-fade-v-${uniqueId.replace(/:/g, "")}`;
const vGradientId = `${vMaskId}-gradient`;
const combinedMaskId = `chart-background-fade-${uniqueId.replace(/:/g, "")}`;
let maskRef: string | undefined;
if (fadeHorizontal && fadeVertical) {
maskRef = `url(#${combinedMaskId})`;
} else if (fadeHorizontal) {
maskRef = `url(#${hMaskId})`;
} else if (fadeVertical) {
maskRef = `url(#${vMaskId})`;
}
const targetOpacity = opacity;
const revealOpacity = isLoaded ? targetOpacity : 0;
const enterFade =
enterTransition && typeof enterTransition === "object"
? enterTransition
: { duration: BACKGROUND_ENTER_FADE_MS / 1000, ease: "easeOut" as const };
return (
// biome-ignore lint/a11y/noAriaHiddenOnFocusable: decorative plot-area pattern layer
<g aria-hidden="true" className="chart-background">
{fadeMask ? (
<defs>
{fadeHorizontal ? (
<>
<linearGradient
id={hGradientId}
x1="0%"
x2="100%"
y1="0%"
y2="0%"
>
{hStops.map((stop) => (
<stop
key={stop.offset}
offset={stop.offset}
style={{ stopColor: "white", stopOpacity: stop.opacity }}
/>
))}
</linearGradient>
<mask id={hMaskId}>
<rect
fill={`url(#${hGradientId})`}
height={innerHeight}
width={innerWidth}
x={0}
y={0}
/>
</mask>
</>
) : null}
{fadeVertical ? (
<>
<linearGradient
id={vGradientId}
x1="0%"
x2="0%"
y1="0%"
y2="100%"
>
{vStops.map((stop) => (
<stop
key={stop.offset}
offset={stop.offset}
style={{ stopColor: "white", stopOpacity: stop.opacity }}
/>
))}
</linearGradient>
<mask id={vMaskId}>
<rect
fill={`url(#${vGradientId})`}
height={innerHeight}
width={innerWidth}
x={0}
y={0}
/>
</mask>
</>
) : null}
{fadeHorizontal && fadeVertical ? (
<mask id={combinedMaskId}>
<g mask={`url(#${hMaskId})`}>
<rect
fill={`url(#${vGradientId})`}
height={innerHeight}
width={innerWidth}
x={0}
y={0}
/>
</g>
</mask>
) : null}
</defs>
) : null}
<defs>{patternNode}</defs>
<motion.rect
animate={{ opacity: revealOpacity }}
fill={`url(#${patternId})`}
height={innerHeight}
initial={{ opacity: 0 }}
mask={maskRef}
transition={isLoaded ? enterFade : { duration: 0 }}
width={innerWidth}
x={0}
y={0}
/>
</g>
);
}
Background.displayName = "Background";
export default Background;
src/charts/pattern-preset.tsx
"use client";
import type { ReactNode } from "react";
import { PatternCircles, PatternLines } from "./visx-pattern";
export const PATTERN_PRESET_IDS = [
"none",
"diagonal",
"horizontal",
"vertical",
"cross",
"dots",
"circles",
"accent",
] as const;
export type PatternPresetId = (typeof PATTERN_PRESET_IDS)[number];
export interface PatternPresetOptions {
color?: string;
scale?: number;
strokeWidth?: number;
radius?: number;
complement?: boolean;
fill?: string;
/** Dot grid only — when false, render hollow dots (stroke only). Default: true */
dotFill?: boolean;
tileBackground?: string;
}
/** Presets rendered with @visx/pattern `PatternCircles`. */
export function isCirclePattern(preset: PatternPresetId): boolean {
return preset === "circles" || preset === "dots";
}
/** @deprecated Use `isCirclePattern`. */
export function isCirclesPattern(preset: PatternPresetId): boolean {
return isCirclePattern(preset);
}
export function patternPresetTileSize(
preset: PatternPresetId,
scale = 1
): { width: number; height: number; strokeWidth: number } {
let base = { width: 6, height: 6, strokeWidth: 1 };
if (preset === "dots") {
base = { width: 10, height: 10, strokeWidth: 0 };
} else if (preset === "cross") {
base = { width: 8, height: 8, strokeWidth: 1 };
} else if (preset === "circles") {
base = { width: 6, height: 6, strokeWidth: 1 };
}
return {
width: base.width * scale,
height: base.height * scale,
strokeWidth: base.strokeWidth * scale,
};
}
function renderPatternCircles(
preset: "dots" | "circles",
_id: string,
color: string,
common: {
id: string;
height: number;
width: number;
strokeWidth: number;
background?: string;
},
options: PatternPresetOptions,
scale: number
) {
const isDotGrid = preset === "dots";
const radius =
options.radius ?? (isDotGrid ? Math.max(0.5, 1.5 * scale) : 2 * scale);
const dotFillEnabled = options.dotFill !== false;
if (isDotGrid) {
const dotFill = dotFillEnabled ? options.fill || color : undefined;
return (
<PatternCircles
{...common}
complement={options.complement}
fill={dotFill}
radius={radius}
stroke={dotFillEnabled && options.fill ? undefined : color}
strokeWidth={
dotFillEnabled && !options.fill
? (options.strokeWidth ?? 0)
: (options.strokeWidth ?? 1)
}
/>
);
}
return (
<PatternCircles
{...common}
complement={options.complement}
fill={options.fill || undefined}
radius={radius}
stroke={color}
strokeWidth={options.strokeWidth ?? common.strokeWidth}
/>
);
}
/** Renders a @visx/pattern definition node for the given preset. */
export function renderPatternPreset(
preset: PatternPresetId,
id: string,
options: PatternPresetOptions = {}
): ReactNode {
if (preset === "none") {
return null;
}
const color = options.color ?? "var(--chart-1)";
const scale = options.scale ?? 1;
const tile = patternPresetTileSize(preset, scale);
const common = {
id,
height: tile.height,
width: tile.width,
strokeWidth: tile.strokeWidth,
...(options.tileBackground ? { background: options.tileBackground } : {}),
};
if (preset === "dots" || preset === "circles") {
return renderPatternCircles(preset, id, color, common, options, scale);
}
const strokeWidth = options.strokeWidth ?? tile.strokeWidth;
switch (preset) {
case "diagonal":
return (
<PatternLines
{...common}
orientation={["diagonal"]}
stroke={color}
strokeWidth={strokeWidth}
/>
);
case "horizontal":
return (
<PatternLines
{...common}
orientation={["horizontal"]}
stroke={color}
strokeWidth={strokeWidth}
/>
);
case "vertical":
return (
<PatternLines
{...common}
orientation={["vertical"]}
stroke={color}
strokeWidth={strokeWidth}
/>
);
case "cross":
return (
<PatternLines
{...common}
orientation={["diagonal", "diagonalRightToLeft"]}
stroke={color}
strokeWidth={strokeWidth}
/>
);
case "accent":
return (
<PatternLines
{...common}
orientation={["diagonal"]}
stroke="#e879f9"
strokeWidth={strokeWidth}
/>
);
default:
return null;
}
}
src/charts/visx-pattern.tsx
"use client";
import {
PatternCircles as VisxPatternCircles,
PatternHexagons as VisxPatternHexagons,
PatternLines as VisxPatternLines,
PatternWaves as VisxPatternWaves,
} from "@visx/pattern";
import type { ComponentProps } from "react";
export function PatternLines(props: ComponentProps<typeof VisxPatternLines>) {
return <VisxPatternLines {...props} />;
}
PatternLines.displayName = "PatternLines";
export function PatternCircles(
props: ComponentProps<typeof VisxPatternCircles>
) {
return <VisxPatternCircles {...props} />;
}
PatternCircles.displayName = "PatternCircles";
export function PatternWaves(props: ComponentProps<typeof VisxPatternWaves>) {
return <VisxPatternWaves {...props} />;
}
PatternWaves.displayName = "PatternWaves";
export function PatternHexagons(
props: ComponentProps<typeof VisxPatternHexagons>
) {
return <VisxPatternHexagons {...props} />;
}
PatternHexagons.displayName = "PatternHexagons";