SurveyStream

SurveyStream · Background pattern

Stream & Telemetry

A seamless 400×400 tile: a node matrix with continuous stream waveforms running through it. Verified pixel-periodic with zero boundary discontinuity — the joins measure quieter than the average interior column. Two inks: one for dark grounds, one for light.

Tile400 × 400 (200 also supplied)
Weight12.6 KB · 1.8 KB gzipped
FiltersNone — that is the point
Seam testBoundary z −0.22 to −0.63
ThemesDark and light, both seam-tested

01 — The pattern

Nine tiles, and you cannot find the joins.

Every specimen below follows the page theme. Switch it with the control in the corner — these are two separately drawn tiles, not one inverted.

Brand palette · full strengthtile-brand-400 · light / dark

A live data feed you can build on.

Subtle preset, with real copy on top. This is the one to use behind text — half the accent opacity, same geometry.

Brand · subtletile-brand-400-subtle · light / dark
Slate paletteAlternate
200 px tileDenser grid

02 — Why it is seamless

Five rules, one of which is the trap everyone hits.

  1. Integer periods across the tile width. This is the whole seam condition — it makes value and slope agree at x=0 and x=W. The four waves run 2, 3, 1 and 4 periods across 400 px.
  2. Paths drawn from −1 period to W+1 period. The overhang is clipped by <pattern>, and because the path is W-periodic the clipped part is exactly what the neighbour draws. No phase break at the join.
  3. No filters. A feGaussianBlur renders its glow outside the path, that output is clipped at the tile boundary, and the clipped halo does not wrap — a bright seam on every edge. The glow here is three layered strokes on the same path: pure geometry, joins correctly.
  4. Grid lines at 0 … W−step, never at W. Drawing both edges doubles the line on every boundary and visibly halves the spacing there.
  5. Edge nodes mirrored. A dot centred at x=0 is clipped to its right half; its mirror at x=W supplies the left half from the neighbouring tile. Corner dots get all four positions.

Two rules about the repeat being invisible

Waves are spaced uniformly at 1/8, 3/8, 5/8, 7/8 of the height, so the gap that wraps across the tile edge is the same size as the internal gaps. My first version spaced them unevenly and left an empty band — the band was seamless, but it was also the thing that made the grid of tiles visible.

Packets stay clear of the edges. A stroke-dasharray along the wave would have been easier, but dash phase is measured from the path start, so it restarts every tile and breaks at the boundary.

What a seam actually looks like in the numbers

Rendered 3×3 at 1:1, then for every pixel column the mean |I(x+1,y) − I(x,y)|. A seam is an outlier column — a bright or dark line where two tiles meet. Interior columns average 0.909 with sd 1.972; the boundary columns measure 0.425, a z-score of −0.25. The joins are quieter than typical content, which is the strongest form of "no seam" you can ask for.

TileAxisInterior mean Δsd Boundary ΔzPeriodicity errorResult
brand 400 darkvertical0.9101.9460.403−0.260PASS
brand 400 darkhorizontal1.5922.5180.000−0.630PASS
brand 400 lightvertical0.9932.8920.345−0.220PASS
brand 400 lighthorizontal1.5243.0730.003−0.500PASS
slate 400 darkvertical0.9091.9720.425−0.250PASS
slate 200 darkvertical2.7362.1911.655−0.490PASS
slate 200 lightvertical2.6773.3911.460−0.360PASS

Periodicity error is the maximum |I(x,y) − I(x+400,y)| across 960,000 samples. Zero means the tiling is pixel-identical, not approximately so.

03 — Two inks, not an inversion

The light tile is drawn, not flipped.

This is the part worth understanding before you reach for filter: invert().

Why inverting fails

On dark the accents are additive — a bright cyan stroke over near-black reads as glow, and the three stacked strokes build a halo. Invert that and you get a dark stroke with a pale halo on white: the halo reads as a dirty smudge rather than light, and the grid becomes a heavy dark cage instead of a whisper.

An earlier version of this page said the pattern was dark-mode only by design. That was true of the artwork, but it made the deliverable half a system. It is fixed rather than justified.

What actually changed

  • Accents move to the deep end. Stream → Current #0A6E86, Pulse → Pulse-deep #12987A. Bright mint on white is invisible; deep teal on white is a line.
  • Glow layers thin to 62%. Dark ink on light shows far more than light ink on dark, so the halo has to be pulled back or it turns to mud.
  • Grid flips to slate at 11% rather than cool grey at 8%.
  • Geometry is identical — same waves, same periods, same node positions. Both tiles pass the same seam test.

pattern.css ships both and swaps them across the three theme states, so .ss-bg is now correct on either ground instead of switching itself off under prefers-color-scheme: light.

04 — The code

Standalone, and the demonstration rect you asked for.

Wave path data abbreviated — the full tile is 12.6 KB, and 1.8 KB gzipped.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800" width="1200" height="800">
  <title>SurveyStream — stream &amp; telemetry pattern</title>
  <defs>
    <pattern id="surveystream-pattern" patternUnits="userSpaceOnUse" width="400" height="400">

      <!-- 1. matrix: lines at 0…350, never at 400, so spacing stays even across tiles -->
      <path d="M0 0V400M50 0V400M100 0V400…M0 0H400M0 50H400…"
            stroke="rgba(126,150,156,0.09)" stroke-width="1" fill="none" shape-rendering="crispEdges"/>

      <!-- 2. telemetry nodes; any node on an edge is mirrored on the opposite edge -->
      <path d="M0 0m-1.15 0a1.15 1.15 0 1 0 2.3 0a1.15 1.15 0 1 0 -2.3 0…" fill="rgba(126,150,156,0.18)"/>

      <!-- 3. streams: 2 periods across 400. Drawn from −200 to 600 so the overhang
              is clipped into exactly what the neighbouring tile draws.
              Three layered strokes fake the glow — a real filter would be clipped
              at the tile edge and leave a bright seam. -->
      <g>
        <path d="M-200 50C-157.55 20.67 -142.45 20.67 -100 50C…"
              stroke="#10BBD1" stroke-width="13"  opacity="0.07" fill="none" stroke-linecap="round"/>
        <path d="M-200 50C…" stroke="#10BBD1" stroke-width="5.85" opacity="0.12" fill="none" stroke-linecap="round"/>
        <path d="M-200 50C…" stroke="#10BBD1" stroke-width="1.5"  opacity="0.3"  fill="none" stroke-linecap="round"/>
      </g>
      <!-- …three more streams at 3, 1 and 4 periods, y = 3/8, 5/8, 7/8 -->

      <!-- 4. packets: short bright segments on the wave, kept clear of the edges -->
      <g><path d="M112.4 33.1L117.3 31.8…" stroke="#10BBD1" stroke-width="3.15"
               opacity="0.85" fill="none" stroke-linecap="round"/></g>
    </pattern>
  </defs>

  <rect width="100%" height="100%" fill="#04171F"/>
  <rect width="100%" height="100%" fill="url(#surveystream-pattern)"/>
</svg>

05 — Usage

Three ways in.

CSS

/* both themes, via the shipped custom properties */
@import "pattern/pattern.css";

.hero { /* .ss-bg swaps ground + tile with the theme */ }

/* or pick one explicitly */
.hero-dark {
  background: #04171F url("pattern/tile-brand-400.svg") repeat;
  background-size: 400px 400px;
}

Reference the file in production. The SVG gzips to 1.8 KB as a file; the same tile percent-encoded inside a stylesheet is 12.3 KB raw and compresses far worse, because URI escaping destroys the repetition gzip feeds on.

Figma

  • Drag in tile-brand-400.svg or the -light counterpart.
  • Select the frame → fill → Image → Tile at 400 px.
  • The tile is a real 400×400 frame with a clip, so Figma's tiling lands on exactly the same grid as the CSS tiling. No half-pixel drift between design and build.

Animating it

Shift background-position by exactly 400 px per cycle — .ss-bg-drift does this over 60 s. Any other distance produces a visible jump when the loop restarts.

One constraint that still holds

Keep body copy off the wave bands. Use the subtle preset behind text in either theme — the full-strength tile is for hero bands, empty states and login screens. The light tile is quieter than the dark one, but a wave crest still runs through a line of 14px text.