Backgrounds
Five SVG patterns, and the wrapper that puts one behind your content.
<script setup lang="ts">
import SaBackground from '@shared/components/backgrounds/SaBackground.vue'
import { Button } from '@shared/components/ui/button'
</script>
<template>
<SaBackground pattern="grid" class="overflow-hidden rounded-xl border bg-card">
<div class="space-y-3 p-8">
<h3 class="text-xl font-semibold">
Your workspace is ready
</h3>
<p class="max-w-sm text-sm text-muted-foreground">
Invite the rest of the team, or start by importing the customers you
already have.
</p>
<div class="flex flex-wrap gap-2">
<Button size="sm">
Invite people
</Button>
<Button size="sm" variant="outline">
Import customers
</Button>
</div>
</div>
</SaBackground>
</template>Usage
SaBackground wraps your content and paints a pattern behind it. Give it a pattern and put whatever you like in the default slot.
The pattern layer is clipped, takes no pointer events, and is aria-hidden. The content layer is positioned and later in the DOM, so it always paints above it without a z-index anywhere.
Every pattern is drawn in currentColor, so it takes the text color it inherits. That is what lets one set of patterns work on any surface and follow a re-theme, and color overrides it per instance.
Examples
The five patterns
pattern is dots, grid, waves, geometric, mesh or none. none renders no layer at all, which is how a page turns the pattern off without a v-if around the wrapper.
<script setup lang="ts">
import SaBackground from '@shared/components/backgrounds/SaBackground.vue'
const PATTERNS = ['dots', 'grid', 'waves', 'geometric', 'mesh', 'none'] as const
</script>
<template>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<SaBackground
v-for="pattern in PATTERNS"
:key="pattern"
:pattern="pattern"
:opacity="0.18"
class="overflow-hidden rounded-xl border bg-card"
>
<div class="flex h-28 items-end p-3">
<code class="text-xs font-medium">{{ pattern }}</code>
</div>
</SaBackground>
</div>
</template>Position
position is full, top-right or bottom-left. The two corners cover two thirds of the box, and they use logical sides, so they mirror in a right-to-left layout.
<script setup lang="ts">
import SaBackground from '@shared/components/backgrounds/SaBackground.vue'
const POSITIONS = ['full', 'top-right', 'bottom-left'] as const
</script>
<template>
<div class="grid gap-4 sm:grid-cols-3">
<SaBackground
v-for="position in POSITIONS"
:key="position"
pattern="dots"
:position="position"
:opacity="0.25"
class="overflow-hidden rounded-xl border bg-card"
>
<div class="flex h-28 items-end p-3">
<code class="text-xs font-medium">{{ position }}</code>
</div>
</SaBackground>
</div>
</template>Color and opacity
opacity defaults to 0.05, which is a texture rather than a decoration. Turn it up and the pattern becomes the point; the examples on this page use higher values so the shapes are visible.
<script setup lang="ts">
import SaBackground from '@shared/components/backgrounds/SaBackground.vue'
import SaColorInput from '@shared/components/common/SaColorInput.vue'
import { Slider } from '@shared/components/ui/slider'
import { ref } from 'vue'
const color = ref('#1DB9C3')
const opacity = ref([22])
</script>
<template>
<div class="space-y-4">
<div class="flex flex-wrap items-center gap-6">
<SaColorInput v-model="color" aria-label="Pattern color" />
<div class="w-48 space-y-1">
<p class="text-xs text-muted-foreground">
Opacity {{ (opacity[0] / 100).toFixed(2) }}
</p>
<Slider v-model="opacity" :min="0" :max="60" :step="1" aria-label="Pattern opacity" />
</div>
</div>
<SaBackground
pattern="waves"
:color="color"
:opacity="opacity[0] / 100"
class="overflow-hidden rounded-xl border bg-card"
>
<div class="flex h-36 items-center justify-center">
<p class="text-sm text-muted-foreground">
The pattern tints to the color you give it
</p>
</div>
</SaBackground>
</div>
</template>A pattern on its own
Each pattern is also a component. Use one directly where you want to own the positioning, the clipping and the opacity yourself.
<script setup lang="ts">
import PatternGeometric from '@shared/components/backgrounds/PatternGeometric.vue'
</script>
<template>
<div class="relative overflow-hidden rounded-xl border bg-card">
<div class="pointer-events-none absolute inset-0 text-primary opacity-20">
<PatternGeometric />
</div>
<div class="relative p-8">
<h3 class="text-lg font-semibold">
A pattern on its own
</h3>
<p class="mt-1 text-sm text-muted-foreground">
You position it, clip it and set its opacity yourself.
</p>
</div>
</div>
</template>API
Source: src/shared/components/backgrounds/
SaBackground props
| Prop | Type | Default | Description |
|---|---|---|---|
pattern | 'dots' | 'grid' | 'waves' | 'geometric' | 'mesh' | 'none' | required | Which pattern, or none. |
opacity | number | 0.05 | The pattern layer's opacity. |
position | 'full' | 'top-right' | 'bottom-left' | 'full' | Where the layer sits. The corners are two thirds of the box. |
color | string | — | Overrides the inherited currentColor the pattern tints to. |
SaBackground emits
None.
SaBackground slots
| Slot | Description |
|---|---|
default | The content, which paints above the pattern. |
The five patterns
Each takes no props, emits nothing and has no slots. They fill their parent at 100% by 100%.
| Component | The tile | Size |
|---|---|---|
PatternDots | One filled dot per tile. | 24 × 24 |
PatternGrid | A single-pixel rule along two sides. | 32 × 32 |
PatternWaves | Two stroked waves. | 80 × 40 |
PatternGeometric | Two stroked triangles, the second at half opacity. | 40 × 35 |
PatternGradientMesh | Three overlapping radial gradients, filled rather than stroked. | Fills the box |
PatternGradientMesh is the one that fills rather than strokes: each gradient fades currentColor to transparent, so the mesh tints to whatever color it inherits. It is a pattern the template ships and is documented as it ships.
Each instance generates its own id for its SVG <pattern> or gradients, so several patterns on one page cannot collide.
Accessibility
- Every pattern SVG is
aria-hidden, and so is the layerSaBackgroundputs it in. They are decoration and say nothing. - The layer takes no pointer events, so a pattern never eats a click meant for the content above it.
- At the default opacity the pattern is a texture rather than a background, so text over it keeps its contrast. If you turn it up, check the text over it again.