import { Component } from '@angular/core';
import { WrThreadLines } from 'ngwr/animations/thread-lines';
@Component({ imports: [WrThreadLines] })
export class MyComponent {}Thread Lines
A wide ribbon of fine threads behind a section's content, like silk: dozens of hair-thin strands flowing across the region at a slight angle, close together near one end, almost a single cord, and parting as they go toward the other, each taking a path of its own. The bundle rides slow waves that travel along it, so it shimmers, and the pointer parts the threads around itself, like fingers drawn through silk.
Installation
Import the class, and @use the entry point in your global styles. Nothing warns you when the @use is missing.
Runs on the GPU
Basic
Put <wr-thread-lines /> first inside the region it should fill. The drawing is one WebGL pass: every pixel measures its distance to every strand, so a strand is a crisp anti-aliased hairline with a faint glow, never a stroke. The context is made when the host is first on screen and sized, and the loop runs while it stays on screen and the document is visible: thirty frames a second while only the waves move, the screen's rate while the parting is live. The parent takes the pointer, passively; a finger is ignored, so a scroll over the section is never fought. Under RTL the ribbon is mirrored and gathers at the inline start.
<section class="opt-in">
<wr-thread-lines />
<h2>Opt in to what you need.</h2>
</section>The parent
The host is absolute, inset: 0, at z-index: -1, under no pointer. The parent provides two things: it is positioned, so the host fills it, and isolated, so the host's negative z-index lands at the floor of the parent's own stacking context, under the content and over the page background.
.opt-in {
position: relative;
isolation: isolate;
}Calmer
count is how many strands the bundle has, amplitude how high the wave lifts it, in px, speed how fast the wave travels and fray how far the strands part by the far end; none of it keeps them a cord the whole way. Fewer strands, a lower and slower wave and a narrower fray is a ribbon that barely stirs.
<wr-thread-lines count="48" amplitude="8" speed="0.6" fray="0.6" />Dense and frayed
More strands, a wider fray, a steeper angle (degrees; negative rises toward the far end) and a finer thickness (device px) is a frayed sheet of silk rather than a cord. The count is capped at 128: the shader walks every strand for every pixel, and the canvas renders under a pixel budget of its own, so a wide section is never a wide texture.
<wr-thread-lines count="110" fray="1.6" angle="-10" thickness="0.8" />Colours
The middle of the bundle is --wr-thread-lines-color, the body's ink by default, and the outer strands blend toward --wr-thread-lines-accent, the primary by default; set either on the host, where the defaults are declared. Both are read through resolveCssColorChannels and re-read when the scheme changes. The surface is read too: on a light one the strands are laid as ink, darker than the surface, with next to no glow; on a dark one they are lit, with a soft glow round each.
<wr-thread-lines style="--wr-thread-lines-color: var(--wr-color-info); --wr-thread-lines-accent: var(--wr-color-warning)" />Parting
parting is how strongly the pointer parts the strands: the gap is a lens drawn out along the ribbon, whole at the pointer and gone about 110px across from it, and it opens and closes on a slightly under-damped spring, so the threads swing a little past and back when the pointer leaves. None turns it off, and the loop then holds at thirty frames a second throughout.
<wr-thread-lines parting="0" />API
| Name | Description | Type | Default |
|---|---|---|---|
count | How many strands the bundle has, 128 at most. | number | 72 |
thickness | How thick a strand is, in device px. | number | 1 |
amplitude | How high the travelling wave lifts the bundle, in CSS px. | number | 16 |
speed | How fast the wave travels, a multiplier: one is a wave every nine seconds. | number | 1 |
fray | How far the strands part by the far end, a multiplier: none keeps them a cord the whole way. | number | 1 |
angle | The ribbon's tilt, in degrees: positive falls toward the far end, negative rises. | number | -6 |
parting | How strongly the pointer parts the strands, a multiplier: none turns the parting off. | number | 1 |
Accessibility
| Part | Behaviour |
|---|---|
| Screen readers | Decorative: the host is aria-hidden, holds no text, takes no pointer and is never focusable. The listeners on the parent are passive and stop nothing, so every control over the ribbon behaves as before. |
| Touch | A finger is ignored: it parts nothing, nothing is prevented and touch-action is left alone, so a scroll that starts over the region scrolls. |
| Reduced motion | Under prefers-reduced-motion: reduce one still frame of the closed ribbon is drawn and nothing moves; a preference that flips mid-flight stops the loop at once. Under forced-colors: active nothing is drawn. |
| Contrast | A strand is a hairline laid in the body's ink at well under full coverage, brightest in the middle of the bundle and faded at its edges and ends, so the text over it reads as it does on the plain surface. |
| No WebGL | Without WebGL nothing is drawn; a lost context is let go and rebuilt when the browser restores it. |
| Server | The empty canvas is rendered, absolutely positioned, so the browser's first draw shifts no layout. |
CSS variables
Set these on the element itself, with a rule more specific than its Declared on selector. In a global stylesheet, put a class of yours on an ancestor it really has: <body> for an overlay panel, which renders outside your markup. In a component stylesheet the selector alone works, but only for an element in that component's own template. A value on a wrapper or :root is shadowed, because ngwr/animations/thread-lines declares each default on its own element.
| Variable | Default | Declared on |
|---|---|---|
--wr-thread-lines-accent | var(--wr-color-primary) | .wr-thread-lines |
--wr-thread-lines-color | var(--wr-color-on-surface) | .wr-thread-lines |
See also
Animation Flow LinesA dense field of thin parallel lines behind a section's content that flows like silk: a slow, smooth noise field bends every line in long gentle waves, neighbours moving together, so the field reads like a surface or a contour map in a breeze.Animation AuroraA curtain of light hanging from one edge of a section and reaching part of the way in, like an aurora: its free edge a slow broad wave, fine rays shimmering inside it, brightest just short of the free edge and fading toward the one it hangs from, its colour flowing across the width through three stops from the theme.Guide MotionEasing and duration tokens for your transitions and animations.Component AlertAn inline status message.