React

Blob shapes in React

Drop the exported TSX component into a project, drive size and color with props, clip an image with the shape, and keep SVG ids unique.

Updated September 18, 2026

The React export is a single file with no dependencies beyond React itself. It holds the path data as a constant and renders one svg element, so it works the same in a Vite app, a Next.js route or a component library.

Add the component

Copy the export into your components folder, for example src/components/Blob.tsx, and import it where you need the shape.

import type { SVGProps } from "react";

const PATH =
  "M413,84.5Q476,169,447,249.5Q418,330,349,383Q280,436,204,412Q128,388,96,314Q64,240,105,169.5Q146,99,220,74.5Q294,50,353.5,67.25Q413,84.5,413,84.5Z";

type BlobProps = SVGProps<SVGSVGElement> & {
  size?: number | string;
  color?: string;
};

export default function Blob({ size = 500, color = "#1e8fe5", ...rest }: BlobProps) {
  return (
    <svg
      viewBox="0 0 500 500"
      width={size}
      height={size}
      aria-hidden="true"
      focusable="false"
      {...rest}
    >
      <path d={PATH} fill={color} />
    </svg>
  );
}

The JavaScript version is identical without the type annotations. Nothing here reaches for state, effects or the DOM, so in the Next.js App Router this component renders on the server as it stands. The variants further down call useId, which is a hook, so those belong in a file marked as a client component.

Pass size and color

Both props have defaults, so the bare component renders a 500 by 500 shape. Override either one per instance, and keep the rest of the layout in CSS.

<Blob size={240} color="#0668b6" />
<Blob size="100%" color="currentColor" className="hero-shape" />

currentColor is worth remembering: it lets the shape inherit the text color of its container, which makes theme switching a one-line change. Because the extra props are spread onto the root element, className, style and event handlers all pass through.

For a gradient, replace the fill with a reference to a linearGradient in the same file and give that gradient a generated id, as shown in the next two sections.

One component can serve a whole page this way. Sizing in CSS rather than through the prop is often easier: pass size="100%" and let a wrapper element decide the box, which keeps the shape responsive without a resize listener.

Clip an image with the shape

The same path can crop a photo. Put it in a clipPath, then clip an image element with it. preserveAspectRatio="xMidYMid slice" fills the box the way object-fit: cover does.

import { useId } from "react";

export function BlobImage({ src, size = 320 }: { src: string; size?: number }) {
  const clipId = `blob-${useId().replace(/[^a-zA-Z0-9]/g, "")}`;

  return (
    <svg viewBox="0 0 500 500" width={size} height={size} aria-hidden="true">
      <clipPath id={clipId}>
        <path d={PATH} />
      </clipPath>
      <image
        href={src}
        width="500"
        height="500"
        preserveAspectRatio="xMidYMid slice"
        clipPath={`url(#${clipId})`}
      />
    </svg>
  );
}

Cross-origin images render, but they taint a canvas, so add crossOrigin="anonymous" if you later draw the result into one.

Keep ids unique

Gradient, pattern and clipPath ids live in one global document namespace. Render the same component twice with a hard-coded id and the second blob silently borrows the first one’s fill, because every reference resolves to the first matching element in the DOM.

useId gives each instance its own value. It returns characters that are awkward in CSS selectors, so strip anything outside letters and digits and add a readable prefix, exactly as above. Build the whole id once per render and reuse it in both places.

const gradientId = `blob-grad-${useId().replace(/[^a-zA-Z0-9]/g, "")}`;

return (
  <svg viewBox="0 0 500 500" width={size} height={size} aria-hidden="true">
    <defs>
      <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
        <stop offset="0" stopColor="#c2e59c" />
        <stop offset="1" stopColor="#64b3f4" />
      </linearGradient>
    </defs>
    <path d={PATH} fill={`url(#${gradientId})`} />
  </svg>
);

On the server the id is generated during rendering and reused during hydration, so the markup matches and React logs no mismatch warning.

Common questions

Do I need one component per shape? No. Keep one component and pass the path as a prop when you use several shapes, or export a small map of named paths. Each exported blob is just a string, so a handful of them costs a few hundred bytes.

How do I animate the blob? Animate transform, opacity or the fill with CSS and leave the path alone. Morphing between two different paths needs both to have the same number of nodes, which means exporting them at the same Complexity value.

Can I render the shape as a background instead? Yes. Export the SVG file, put it in your public folder and reference it from CSS. Use the component when the shape has to react to props or state, and a plain file when it never changes.