---
name: bghub
description: Find and install a website background from bgHub, animated or static (CSS, Canvas 2D, Three.js or GLSL), as React or plain HTML source. Use when the user asks for a hero background, an animated backdrop, a gradient/particles/plasma/aurora/starfield/mesh background, a subtle texture or repeating pattern behind a section, or wants a page or section to look less flat or less empty.
---

# bgHub backgrounds

Ready-made backgrounds, animated or static, downloaded as real editable source
files. No API key, no package to install.

Read in order: **Workflow** to fetch and install one, **Choosing well** to pick
the right one, **Two things every install has to get right** for legibility and
theming, **Mounting it** for the JSX and reduced motion. If a background you
installed does not move, jump to the end.

## How the catalog is shaped

Three levels, and only the first is worth searching:

- **Pattern** is a technique, e.g. `plasma-silk`, `grid-lines`, `starfield-warp`.
- **Group** is a pattern at one motion setting, e.g. `plasma-silk-calm`.
- **Background** is a group in one palette. Its slug is `<groupId>-<paletteId>`,
  e.g. `plasma-silk-calm-royal-violet`.

So you never search the whole product and you never invent a slug. You read the
pattern rows, pick a group, pick a palette, and join them with a hyphen.

## Workflow

### 1. Read the index (once per session)

```bash
curl -fsSL https://bghub.space/api/patterns
curl -fsSL https://bghub.space/api/palettes
```

Each pattern row is:

    pattern | tech | weight | groups | palettes | tags | description

Match the user's request against `description`, `tags` and `tech` yourself.
There is no search endpoint on purpose: the index is one response and you read
all of it. Add `?format=json` to either endpoint if you would rather parse than
read.

The index is around 118 KB, which is fine to read once and reuse for the rest
of the session, but do not re-fetch it per background. If you only need part of
it, filter as you fetch:

```bash
# Animated only: drop the static patterns (see "Static patterns" below).
curl -fsSL https://bghub.space/api/patterns | grep -v "does not animate"

# One idea: match the tag or description column.
curl -fsSL https://bghub.space/api/patterns | grep -E "aurora|gradient|mesh"

# One technique, cheapest first.
curl -fsSL https://bghub.space/api/patterns | awk -F' \\| ' '$2=="css" && $3=="light"'
```

### 2. Compose the slug

Take a group id from the `groups` column and a palette id from
`/api/palettes`, then join: `plasma-silk-calm` + `royal-violet` becomes
`plasma-silk-calm-royal-violet`.

`palettes: all` means every palette id works. Anything else is an explicit
list, so use only those.

### 3. Get the manifest

```bash
curl -fsSL https://bghub.space/api/bg/plasma-silk-calm-royal-violet
```

The shape is:

    { slug, title, description, tech, weight, tags, preview,
      frameworks: [ { framework, install?, files: [{ name, lang, bytes, url }], commands } ] }

Pick a `frameworks` entry: `react` for any React, Next, Remix or Vite-React
project, `html` for a plain page or any other framework, where you port the
markup yourself.

The `html` variant's `index.html` is a **demo harness, not a page to ship**: it
is a full document whose only job is to give the background a full-viewport
stage. Take the wrapper element out of its `<body>`, keep the sibling `.css` or
`.js` file as-is, and link them from your own page.

`install` sits **inside the framework entry**, not at the top level. Read it off
the entry you chose, and run it, or the import will not resolve.

A wrong slug returns 404 with a `hint` and a `closest` list of real slugs. Read
those and retry instead of guessing again.

The `url` and `commands` fields are absolute. **If you reached this skill on a
different origin than they print, that origin wins: keep the paths and swap the
host.** The endpoints are identical, and a self-hosted or mirrored instance will
not resolve the public one.

### 4. Download the files

The manifest's `commands` write into the current directory. **Give each
background its own directory, named after the slug**, and change nothing else:

```bash
mkdir -p components/backgrounds/plasma-silk-calm-royal-violet
curl -fsSL https://bghub.space/r/plasma-silk-calm-royal-violet/react/ShaderBackground.tsx \
  -o components/backgrounds/plasma-silk-calm-royal-violet/ShaderBackground.tsx
```

The per-slug directory is not tidiness. File names are generic on purpose
(`Grid.tsx`, `grid.css`, `index.html`), so two backgrounds in one flat folder
collide, and the usual reflex of renaming them to disambiguate is what breaks
them: siblings import each other by exact name.

Download **every** file the entry lists, into that one directory. Keep each
name exactly as the manifest gives it. Names are case-sensitive; a wrong one
returns 404 with an `available` list.

Changing the **extension** is the one safe edit, and only sometimes. A `css`,
`canvas` or `glsl` React download is JSX under a `.tsx` name with
`// @ts-nocheck` at the top, so a plain-JavaScript project can rename it to
`.jsx`. A `three` download carries real type annotations and cannot: keep
TypeScript, or take the `html` variant instead.

**Download the files. Do not retype them.** The sources contain GLSL, nested
template literals and regex. Copying them through your own output corrupts them.
If a fetch tool rewrites or truncates the body, use `curl -o` instead.

Then run that entry's `install` if it has one, e.g.
`npm i three @react-three/fiber`. An absent `install` means
there is genuinely nothing to install, not that the field was forgotten.

## Choosing well

The catalog is 584 patterns and 794 groups across 20 palettes, so
almost any request has a good answer. Four things decide which one.

### Static patterns: read the description before you pick

**224 of the 584 patterns do not move at all.** Every one of them ends its
`description` with the literal sentence `It does not animate.` (the remaining
360 animate). If the user asked for anything animated, skip those rows. If you
install one and then wonder why nothing moves, this is why, and no amount of CSS
debugging will fix it.

Static is the right answer when the user wants texture rather than motion: a
subtle grid, stripes or dots behind a form, a print-like backdrop, or anything
that has to sit under dense body copy.

### tech and weight are two independent axes

Do not infer one from the other. The real distribution:

    canvas  light 25 | medium 41 | heavy 0
    css     light 262 | medium 44 | heavy 38
    glsl    light 12 | medium 42 | heavy 4
    three   light 0 | medium 16 | heavy 100

- `tech` is the technique: `css` needs nothing, `canvas` is 2D and dependency
  free, `glsl` is a raw WebGL fragment shader, `three` pulls in Three.js and
  react-three-fiber.
- `weight` is the runtime cost: `light` is safe anywhere including mobile and
  long scrolling pages, `medium` wants a hero or a section rather than a whole
  page, `heavy` is a deliberate centrepiece on a desktop-first page.

So filter on the column you actually mean. "No new dependencies" is
`tech: css`, `canvas` or `glsl`, not `weight: light`. "Cheap on a phone" is
`weight: light`, which includes plenty of `canvas` and `glsl` rows.

And do not read "go all out" or "make it impressive" as an instruction to reach
for `heavy`. The heavy tier is largely abstract 3D scenes, so for a specific
subject it often has nothing that fits. **A pattern whose description matches
the subject beats a more expensive one that does not.** A `css` or `canvas`
background that reads as the thing the product does will land better than a
Three.js scene of floating shapes.

### Motion is the last segment of the group id

The tokens that actually exist, most common first: `still` (224), `calm` (183), `flow` (90), `busy` (89), `live` (64), `hero` (40), `scatter` (40), `dense` (11).

- Behind headlines and body copy: `still`, `calm`, `soft`, `slow`, `gentle`,
  `veil`, `sparse`, `drift`.
- Decorative sections, not under text: `busy`, `dense`, `live`, `heavy`,
  `steep`, `jump`, `full`, `scatter`.
- `flow` and `hero` sit in between: fine behind a short headline, too much
  behind a paragraph.

When a pattern offers two motions and the background goes behind text, take the
calmer one. It is the more common mistake by a wide margin.

### Colour

- Every palette ships a dark and a light scheme, and a download carries both and
  resolves between them itself. So pick a palette for its **hue**, not its tone,
  and do not wrap one in your own media query. `/api/palettes` returns the exact
  hex of both schemes if you need to match text or a scrim to it.
- `mono-ice` is the neutral choice when you do not know the product's brand colour.
- Palette ids are hyphenated (`royal-violet`), so a slug has more hyphens than
  its group. Never split a slug by hand to recover the halves.

## Two things every install has to get right

### Keep the text readable

The background is decoration; the copy is the product. A drifting glow can pass
under a headline and take it from readable to not, and you cannot see that from
the source. So put a scrim between the background and the content whenever text
sits on top:

```tsx
<div className="absolute inset-0 -z-10"><ShaderBackground /></div>
<div aria-hidden className="absolute inset-0 -z-10 bg-black/45" />
```

A flat `black/40-60` behind light text, or `white/50-70` behind dark text, is
enough. A vertical gradient that is strongest where the text sits looks better.
Mark it `aria-hidden`, and mark the background wrapper `aria-hidden` too: none of
it carries meaning.

If the user gave you brand colours, keep the copy on your own theme tokens rather
than a hardcoded white, so the scrim is the only thing you tune.

### Decide who owns the theme

**This is the seam agents miss.** The download follows the *operating system* via
`prefers-color-scheme`. If the project instead has its own toggle (a `.dark`
class, Tailwind `darkMode: "class"`, next-themes), the two disagree the moment a
visitor's OS and toggle differ: the shell goes dark, the background goes light.

Read the file you downloaded and find that branch first. Then pick one of three:

1. **The project follows the OS.** Nothing to do. They already agree.
2. **The project has its own toggle** (`.dark` on `<html>`, a `data-theme`
   attribute). Re-gate the download on the same signal so the toggle drives the
   background too. This is the one case where editing the shipped scheme
   mechanism is right. For a `css` download the shipped file is dark-first: the
   base rules are the dark scheme and `@media (prefers-color-scheme: light)`
   holds the light ones. So invert it:
   - move the light values into the base rules,
   - replace the media query with the app's own selector holding the dark values,
     e.g. `html.dark .grid-bg { ... }`,
   - drop the `--light` class from the element, since an ancestor now decides.

   For `canvas`, `glsl` and `three`, the scheme comes from a `matchMedia` call
   near the top of the file. Read the class or attribute off `document.documentElement`
   there instead, and observe it with a `MutationObserver` if the toggle can
   change without a reload.
3. **The project is deliberately one theme only.** Pin the background to that
   scheme and delete the other branch:
   - `css`: drop the `--light` class from the element, e.g.
     `className="grid-bg grid-bg--light"` becomes `className="grid-bg"`. The
     light rules then have nothing to match and the dark scheme stands.
   - `canvas`, `glsl`: delete the `"light": { ... }` entry from the `opts`
     object. `three`: return the dark scheme unconditionally from its scheme
     hook. The component then only ever builds from the dark values.
   - To pin it to **light** instead, swap the light values into the base rather
     than deleting them, or take the light hexes from `/api/palettes`.

Say in one line which of the three you did, because it is the choice the user is
most likely to want changed.

## Mounting it

Every download is a self-contained component that positions itself `absolute`
and fills its parent. So the parent must be positioned, and the background must
sit behind the content:

```tsx
<section className="relative isolate overflow-hidden">
  <div className="absolute inset-0 -z-10">
    <ShaderBackground />
  </div>
  <h1 className="relative">Your content</h1>
</section>
```

A hero needs a height of its own: the background has no intrinsic size, so a
section that only contains one collapses to nothing. Give the section
`min-h-[70vh]` or real padding.

**Next.js App Router: add `"use client"` as the first line of any `canvas`,
`glsl` or `three` component.** They use `useEffect`/`useRef` or
react-three-fiber, and without it the build fails. Pure `css` components are
server-safe and need nothing. The downloaded file says so in its header comment,
which is worth opening once for exactly this reason even though you must not
retype it.

The same split decides hydration anywhere else. In Astro, a `canvas`, `glsl` or
`three` background needs `client:load`; a `css` one needs no directive at all
and ships zero JavaScript. Same for any islands architecture: only the techs with
a render loop have to hydrate.

### Mounting an `html` variant inside a component

Only needed if you took the `html` variant for a `canvas`, `glsl` or `three`
background, e.g. because the project is plain JavaScript. Its `.js` file is a
module that grabs a hardcoded canvas id at top level and runs immediately:

```js
const canvas = document.getElementById("scene");
const opts = { ... };
(function (canvas, opts) { /* ... */ })(canvas, opts);
```

Do not rewrite the body, and do not change the IIFE's argument list, which
differs by tech. Change only the two ends: drop the `getElementById` line and
wrap the call in an export that takes the canvas.

```js
export function mountScene(canvas) {
  return (function (canvas, opts) { /* untouched */ })(canvas, opts);
}
```

The IIFE already returns its own teardown, so `return mountScene(ref.current)`
straight out of a `useEffect` cleans up correctly. Render your own
`<canvas ref={ref} />` and delete the downloaded `index.html`.

### Reduced motion

Downloads ship no `prefers-reduced-motion` guard, on purpose: the query belongs
to the page, not to one component. If the project has no global rule, add one
scoped to the wrapper you mounted, and include the pseudo-elements, because CSS
patterns animate on `::before`/`::after` and a bare `*` does not match those:

```css
@media (prefers-reduced-motion: reduce) {
  .bg-wrap, .bg-wrap *, .bg-wrap *::before, .bg-wrap *::after {
    animation: none !important;
    transition: none !important;
  }
}
```

That covers `css` backgrounds. A `canvas`, `glsl` or `three` background drives
its own `requestAnimationFrame` loop and CSS cannot stop it, so if the project
needs to honour the setting, either swap in a static pattern or gate the mount:

```tsx
const still = typeof window !== "undefined" &&
  matchMedia("(prefers-reduced-motion: reduce)").matches;
return still ? <div className="bg-wrap-static" /> : <ShaderBackground />;
```

### When the background does not move

In order of how often it is actually the cause:

1. **The group is static.** Its slug ends in `-still` and its description says
   `It does not animate.` Nothing is broken. Pick an animated group instead.
2. **Nothing is rendering at all.** A background with no size shows as a flat
   fill. Check the parent has height and `position: relative`, and that the
   component is not behind an opaque element.
3. **The project already disables motion.** A global `prefers-reduced-motion`
   rule, or a wrapper with `animation: none`, will freeze a `css` background.
4. **The OS has animations turned off**, which triggers rule 3 but only if such a
   rule exists. Windows: **Settings → Accessibility → Visual effects →
   Animation effects**. macOS: **System Settings → Accessibility → Display →
   Reduce motion**. Verify rather than guess:

```js
matchMedia('(prefers-reduced-motion: reduce)').matches // true => user wants no motion
```

If that is true, respect it. Tell the user which switch to flip and do not strip
the guard to force motion back on.

## Notes

- Preview any background in a browser at https://bghub.space/bg/<slug>.
- The whole gallery is at https://bghub.space/gallery.
- Files are served as `text/plain` and are byte-identical to what the site shows.
- No API key. `/api` and `/r` share a limit of 120 requests per minute per IP,
  which a normal install never approaches. A 429 carries `Retry-After` in
  seconds: wait that long and continue, do not retry immediately.
