shadimage
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 · medium
animationmotion

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";

More from Bklit UI

Browse all components in this library →