Grain
On this page
Grain takes the flatness off a large area of colour — a hero, a coloured band, a card that would otherwise read as a plain rectangle. It wraps content the way container() does, but sets no width of its own: the texture is drawn on ::after, above the children and ignoring the pointer.
It is an extra, so it comes with a stylesheet of its own. Import both from sitelo/ui-extras and put grainStyles() in the head beside styles():
import { styles } from 'sitelo/ui'
import { grain, grainStyles } from 'sitelo/ui-extras'
head(styles(), grainStyles())The tile is a static SVG of fractal noise, painted once. A filter over the live pixels would look much the same and cost a re-raster every time anything underneath it moved.
There are two layers of control. opacity is how hard the texture is pushed once drawn; left alone the theme sets it, and that is the value the two themes are balanced on. type, frequency, octaves, seed and color are the turbulence itself; touching any of them builds a texture for that one element instead of using the shared one in the stylesheet.
Basic grain
Textured.
grain({ style: 'background: var(--su-surface-2); padding: 2rem; border-radius: 0.75rem' },
text({ variant: 'lead', align: 'center' }, 'Textured.'),
)Noise type
fractal sums the noise straight and gives the even speckle of film. turbulence takes its absolute value, which leaves dark veins and clumps — closer to smoke or marble than to grain.
fractal
turbulence
grid({ min: '9rem' },
...['fractal', 'turbulence'].map((type) =>
grain({ type, style: 'background: var(--su-surface-2); padding: 1.5rem 1rem; border-radius: 0.5rem' },
text({ variant: 'small', align: 'center' }, type),
),
),
)Frequency
Cycles per pixel: higher is finer. The noise is drawn at the box’s own size, one unit to the pixel, so this holds whatever the box measures — a small card and a full-width band get the same grain, and nothing repeats.
0.2
0.57
1.2
grid({ min: '9rem' },
...[0.2, 0.57, 1.2].map((frequency) =>
grain({ frequency, style: 'background: var(--su-surface-2); padding: 1.5rem 1rem; border-radius: 0.5rem' },
text({ variant: 'small', align: 'center' }, String(frequency)),
),
),
)Octaves
How many layers of noise are summed, each finer and fainter than the last. One is plain and even; more adds detail, and each one costs the browser another pass when the tile is first drawn.
1
3
6
grid({ min: '9rem' },
...[1, 3, 6].map((octaves) =>
grain({ octaves, style: 'background: var(--su-surface-2); padding: 1.5rem 1rem; border-radius: 0.5rem' },
text({ variant: 'small', align: 'center' }, String(octaves)),
),
),
)Seed
Which noise gets drawn. Any number will do, the same one always gives the same pattern, and nothing else about the texture changes — useful when two grained panels sit side by side and the repeat gives itself away.
0
7
42
grid({ min: '9rem' },
...[0, 7, 42].map((seed) =>
grain({ seed, style: 'background: var(--su-surface-2); padding: 1.5rem 1rem; border-radius: 0.5rem' },
text({ variant: 'small', align: 'center' }, String(seed)),
),
),
)Colour
The noise is grey by default. color tints it — the value is multiplied into the texture inside the filter, so it has to be one that can be resolved when the page is built: #rgb, #rrggbb or rgb(). A named colour, currentColor or a var() cannot be, and leaves the noise grey rather than failing the build. Alpha is how much of the tint: #ff880080 is half of #ff8800, and alpha zero is none.
#0a7a45
#c05621
#2f7fc7
grid({ min: '9rem' },
...['#0a7a45', '#c05621', '#2f7fc7'].map((color) =>
grain({ color, style: 'background: var(--su-surface-2); padding: 1.5rem 1rem; border-radius: 0.5rem' },
text({ variant: 'small', align: 'center' }, color),
),
),
)Around a container
Grain has no width limit of its own, which is what makes this work: the wrapper runs full bleed and the container() inside keeps the text centred and readable.
A textured band
Full width outside, a readable column inside.
grain({ as: 'section', style: 'background: var(--su-primary-soft); padding-block: 2.5rem; border-radius: 0.75rem' },
container({ size: 'sm' },
stack({ gap: 'sm', align: 'center' },
heading({ level: 2, size: 'h4' }, 'A textured band'),
text({ tone: 'muted', align: 'center' }, 'Full width outside, a readable column inside.'),
),
),
)Over a card
The texture inherits the box’s border-radius, so wrapping something rounded does not square its corners off.
Grained
Plain
grid({ min: '12rem' },
grain({ style: 'border-radius: var(--su-radius-lg)' },
card({ variant: 'elevated' },
cardBody(text({ variant: 'small' }, 'Grained')),
),
),
card({ variant: 'elevated' },
cardBody(text({ variant: 'small' }, 'Plain')),
),
)Blending
By default the texture is laid over the content at its own opacity. blend takes any mix-blend-mode — overlay and soft-light push the grain into the colour underneath rather than greying it out.
normal
overlay
soft-light
grid({ min: '9rem' },
...['normal', 'overlay', 'soft-light'].map((blend) =>
grain({ blend, style: 'background: var(--su-primary-soft); padding: 1.5rem 1rem; border-radius: 0.5rem' },
text({ variant: 'small', align: 'center' }, blend),
),
),
)Sandbox
Play with the controls.
grain(…)From script
setGrain() from sitelo/ui-extras/client redraws a grain by element or by id: only what is passed changes, and it returns what the grain is showing now — which getGrain() reads on its own, the theme’s opacity included. The sandbox above is nothing else.
import { setGrain } from 'sitelo/ui-extras/client'
setGrain('hero', { type: 'turbulence', seed: 7 })Props
| Prop | Type | Default | Description |
|---|---|---|---|
opacity | number | — | Opacity of the texture. Left alone, the theme sets it. |
blend | string | 'normal' | A mix-blend-mode for the texture. |
type | 'fractal' | 'turbulence' | 'fractal' | Which turbulence to draw. |
frequency | number | 0.57 | Cycles per pixel — higher is finer. |
octaves | number | 3 | Layers of noise summed together, 1–8. |
seed | number | 0 | Which noise to draw. |
color | string | — | Tints the noise; alpha is how much. #rgb, #rrggbb, #rrggbbaa, rgb() or rgba(). |
as | string | 'div' | Element to render, e.g. section. |