---
name: floating-shapes
description: Use when you want "Playful, casual, energetic" - Animates decorative shapes around the layout.
---

# Floating Shapes

> **Category:** Background  -  **Personality:** Playful, casual, energetic
>
> **Best use:** Startup pages, education, children brands
>
> **Ratings:** Professional 3/5 - Casual 5/5 - Exotic 3/5 - Visual impact 4/5 - Difficulty 2/5 - Performance cost 2/5

## What it is

A scattering of small, **crisp** decorative shapes — outlined rings, solid dots, triangles, plus-signs, squiggles, zigzags, arcs — that gently bob up and down and slowly rotate around the edges of a layout, with a light pointer-parallax so they drift as the cursor moves. Unlike soft, blurry gradient blobs, every shape has a hard edge and a flat brand colour, which reads as friendly and energetic rather than dreamy. It is the go-to background texture for playful startup, education and kids' brands. Pure CSS for the motion; ~15 lines of vanilla JS for the optional parallax.

## Dependencies / CDN

None - pure CSS animation (the parallax is a tiny vanilla-JS sprinkle). The demo loads two display/body fonts — optional:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@500;600;700&family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
```

## HTML

A non-interactive `.fs-shapes` layer of positioned wrappers sits *behind* your content (`z-index:0` vs `1`). Each shape is **two nested elements**: an outer wrapper that the JS nudges for parallax, and an inner `.fs-art` that runs the bob/rotate keyframes — so the two transforms never collide. Position + per-shape timing live in inline `style` vars; `data-depth` drives the parallax strength.

```html
<div class="fs-stage">
  <div class="fs-shapes" aria-hidden="true">
    <!-- outlined ring -->
    <span class="fs-shape" data-depth=".7" style="top:7%;left:5%;--bob:6.5s;--spin:9s;--delay:-1s">
      <span class="fs-art is-sway fs-ring" style="--s:62px;--c:#FF6B6B"></span>
    </span>
    <!-- filled triangle -->
    <span class="fs-shape" data-depth=".8" style="top:60%;left:4%;--bob:7s;--spin:26s;--delay:-2s">
      <span class="fs-art fs-tri" style="--s:48px;--c:#34D399"></span>
    </span>
    <!-- plus-sign -->
    <span class="fs-shape" data-depth=".75" style="top:11%;right:8%;--bob:5.6s;--spin:22s;--delay:-.3s">
      <span class="fs-art fs-plus" style="--s:34px;--c:#4D96FF"></span>
    </span>
    <!-- squiggle (inline SVG stroke) -->
    <span class="fs-shape" data-depth=".6" style="top:79%;left:18%;--bob:6s;--spin:8s;--delay:-1.4s">
      <svg class="fs-art is-sway fs-svg" width="78" height="28" viewBox="0 0 78 28"
           style="--c:#9B5DE5" fill="none" stroke="currentColor" stroke-width="6" stroke-linecap="round">
        <path d="M3 14 q 9 -13 18 0 t 18 0 t 18 0 t 18 0"/>
      </svg>
    </span>
  </div>

  <div class="fs-content"><!-- your real hero / nav / card sits here, z-index:1 --></div>
</div>
```

## CSS

```css
.fs-stage{position:relative;overflow:hidden;border-radius:28px;min-height:620px;isolation:isolate;
  display:flex;align-items:center;justify-content:center;padding:54px 32px;
  background:#FFFBF3}                 /* a light, airy backdrop so crisp shapes pop */

.fs-shapes{position:absolute;inset:0;z-index:0;pointer-events:none}
.fs-content{position:relative;z-index:1}

/* OUTER wrapper — JS writes --px/--py here for parallax; soft lift via drop-shadow */
.fs-shape{position:absolute;will-change:transform;
  transform:translate3d(var(--px,0px),var(--py,0px),0);
  transition:transform .4s cubic-bezier(.22,.8,.24,1);
  filter:drop-shadow(0 7px 11px rgba(43,44,70,.11))}

/* INNER art — bob + turn run on the *individual* transform properties
   (translate / rotate / scale), so two animations compose without fighting. */
.fs-art{display:block;
  animation:fs-bob var(--bob,6s) ease-in-out var(--delay,0s) infinite,
            fs-spin var(--spin,24s) linear var(--delay,0s) infinite}
.fs-art.is-sway {animation:fs-bob var(--bob,6s) ease-in-out var(--delay,0s) infinite,
                 fs-sway var(--spin,7s) ease-in-out var(--delay,0s) infinite}
.fs-art.is-rev  {animation:fs-bob var(--bob,6s) ease-in-out var(--delay,0s) infinite,
                 fs-spin-rev var(--spin,24s) linear var(--delay,0s) infinite}
.fs-art.is-pulse{animation:fs-bob var(--bob,6s) ease-in-out var(--delay,0s) infinite,
                 fs-pulse var(--spin,5s) ease-in-out var(--delay,0s) infinite}

@keyframes fs-bob {0%,100%{translate:0 0}   50%{translate:0 -18px}}
@keyframes fs-spin{to{rotate:360deg}}
@keyframes fs-spin-rev{to{rotate:-360deg}}
@keyframes fs-sway{0%,100%{rotate:-9deg}    50%{rotate:9deg}}     /* good for non-symmetric shapes */
@keyframes fs-pulse{0%,100%{scale:1}         50%{scale:1.14}}     /* good for rings & dots */

/* the geometry — all crisp, never blurred. --s = size, --c = colour */
.fs-ring{width:var(--s,60px);height:var(--s,60px);border-radius:50%;border:7px solid var(--c)}
.fs-dot {width:var(--s,22px);height:var(--s,22px);border-radius:50%;background:var(--c)}
.fs-tri {width:var(--s,46px);height:var(--s,46px);background:var(--c);
  clip-path:polygon(50% 4%,96% 92%,4% 92%)}
.fs-arc {width:var(--s,56px);height:var(--s,56px);border-radius:50%;border:7px solid var(--c);
  border-right-color:transparent;border-bottom-color:transparent}   /* open "C" */
.fs-plus{position:relative;width:var(--s,34px);height:var(--s,34px)}
.fs-plus::before,.fs-plus::after{content:"";position:absolute;background:var(--c);border-radius:5px}
.fs-plus::before{left:50%;top:0;width:32%;height:100%;transform:translateX(-50%)}
.fs-plus::after {top:50%;left:0;height:32%;width:100%;transform:translateY(-50%)}
.fs-svg{color:var(--c);overflow:visible}   /* SVG squiggle/zigzag/star use stroke/fill:currentColor */

@media (prefers-reduced-motion:reduce){    /* static, hand-placed arrangement */
  .fs-art{animation:none!important}
  .fs-shape{transition:none}
}
```

## JavaScript

Optional. Nudges every shape toward (here, *against*) the cursor by its `data-depth`, on a single rAF. The CSS keeps bobbing/rotating; JS only touches the wrapper's `--px/--py`, so they never conflict. Bailed out entirely under reduced-motion.

```js
(function(){
  var stage=document.querySelector('.fs-stage');
  if(!stage) return;
  if(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return; // static
  var shapes=[].slice.call(stage.querySelectorAll('.fs-shape'));
  var tx=0, ty=0, raf=0;
  function apply(){
    raf=0;
    for(var i=0;i<shapes.length;i++){
      var d=parseFloat(shapes[i].getAttribute('data-depth'))||0.4;
      shapes[i].style.setProperty('--px',(tx*d).toFixed(1)+'px');
      shapes[i].style.setProperty('--py',(ty*d).toFixed(1)+'px');
    }
  }
  stage.addEventListener('pointermove',function(e){
    var r=stage.getBoundingClientRect();
    tx=((e.clientX-r.left)/r.width  - .5) * -44;   // ±22px, scaled per shape by depth
    ty=((e.clientY-r.top )/r.height - .5) * -44;
    if(!raf) raf=requestAnimationFrame(apply);
  });
  stage.addEventListener('pointerleave',function(){ tx=0; ty=0; if(!raf) raf=requestAnimationFrame(apply); });
})();
```

## How it works

- **Two transforms, one shape, no conflict.** The outer `.fs-shape` owns the parallax via `transform: translate3d()`. The inner `.fs-art` animates the *individual* transform properties — `translate` (bob), `rotate` (spin/sway) and `scale` (pulse) — which are separate, independently-animatable properties. That lets bob + rotate run as two simultaneous `@keyframes` without overwriting each other, and lets the wrapper's `transform` stack on top cleanly.
- **Crisp, not blurry.** Shapes are built from borders, `clip-path`, pseudo-element bars and stroked SVG — all hard-edged and flat-coloured. The only blur is a faint neutral `drop-shadow` that lifts each shape off the backdrop. This is the key visual difference from gradient "blobs".
- **Depth via parallax + size.** `data-depth` (≈0.3–0.85) scales how far each shape drifts, so smaller/"farther" shapes move less — a cheap but convincing sense of layered depth.
- **Desynced loops.** Per-shape `--bob`, `--spin` and a negative `--delay` (which starts each animation mid-cycle) keep the field from pulsing in unison.

## Customization

- **Brand it:** swap each `--c`. Stick to 4–6 flat hues for a coherent, playful set; tints of one hue read calmer/more "pro".
- **Calmer or livelier:** raise `--bob`/`--spin` durations and drop the bob distance (`-18px`) for a slow, gentle drift; lower them for bouncy energy.
- **Shape mix:** symmetric shapes (rings, dots) barely read a full spin — give them `is-pulse` or `is-sway`; reserve `fs-spin`/`fs-spin-rev` for triangles, pluses, stars and arcs where rotation is visible.
- **Density:** add/remove `<span class="fs-shape">`s. Keep them mostly around the perimeter so they frame, not obscure, the content.
- **Add a shape:** any small SVG works — set `class="fs-art fs-svg"`, `style="--c:…"`, and `stroke="currentColor"` (or `fill="currentColor"`).

## Accessibility & performance

- The whole layer is `aria-hidden="true"` and `pointer-events:none` — purely decorative, never in the tab order, never blocking clicks on the content above it.
- `@media (prefers-reduced-motion: reduce)` freezes every shape into a tidy static arrangement, **and** the JS returns early so no pointer loop runs.
- Cheap by design: only `transform`/`translate`/`rotate`/`scale`/`opacity` animate (GPU-friendly, no layout/paint). Parallax writes are throttled to one `requestAnimationFrame`. A dozen tiny shapes cost far less than a full-screen shader.
- Keep contrast in mind: route shapes through the page margins and keep the busiest cluster away from body copy so text stays legible.

## Gotchas

- **Don't put bob and spin on the same `transform`.** Two `@keyframes` both animating `transform` — the last one wins and you lose the other. Animate the *individual* properties (`translate`, `rotate`, `scale`) instead, as above. (Browser support: Chrome 104+, Firefox 72+, Safari 14.1+.)
- **Parallax and animation must live on different elements** (wrapper vs `.fs-art`). Set the wrapper's `transform` and the inner one's `translate/rotate` and they compose; put both on one node and they clash.
- **A `.fs-plus`/clip-path shape needs a sized box.** The pseudo-element bars are `%`-of-parent, so the element must be `display:block` (or have explicit width/height) — an inline `<span>` collapses to zero and the plus vanishes.
- **Give the stage `overflow:hidden` + `isolation:isolate`.** Shapes are intentionally placed past the edges so they peek in; the clip keeps them tidy and the isolation keeps the `drop-shadow`/stacking contained.
- **Symmetric shapes + full spin = invisible motion.** A perfect ring or dot looks identical while rotating — use pulse/sway for those.
