Swatch scatter 01
Pattern swatches with mono captions scattered across the section, a pill, a large heading at the bottom start and a line with two buttons at the bottom end.
Last updated
---
/**
* Swatch scatter 01: Pattern swatches with mono captions scattered across the section, a pill, a large heading at the bottom start and a line with two buttons at the bottom end.
*
* @block hero
* @order 55
*/
import { ArrowRight, Sparkles } from '@lucide/astro'
import Section from '@/components/apogee/Section/Section.astro'
import Heading from '@/components/apogee/Heading/Heading.astro'
import Button from '@/components/apogee/Button/Button.astro'
import Link from '@/components/apogee/Link/Link.astro'
import { plain, type HeroContent } from '@/components/apogee/sections/_shared/spine.ts'
/** @composable */
interface Props extends HeroContent {}
const {
announcement = { label: 'New: share a look, remix a link', href: '#' },
heading = 'Backgrounds nobody else has, in seconds',
description = 'Mix gradients, glass and grain into backgrounds and loops of your own.',
primaryAction = { label: 'Open the studio', href: '#' },
secondaryAction = { label: 'Browse looks', href: '#' },
} = Astro.props
// Swatches are CSS patterns on the primary and neutral tokens, so they follow the theme.
const p = 'var(--background-primary)', pf = 'var(--background-primary-faded)', n = 'var(--foreground-neutral)', bg = 'var(--background-page)'
const looks = {
halftone: `radial-gradient(circle, ${p} 1.2px, transparent 1.6px) 0 0 / 6px 6px, linear-gradient(135deg, ${pf}, ${bg})`,
mist: `radial-gradient(circle at 30% 30%, color-mix(in oklab, ${p} 60%, ${bg}), transparent 60%), radial-gradient(circle at 70% 75%, color-mix(in oklab, ${n} 35%, ${bg}), transparent 55%), ${pf}`,
fluted: `repeating-linear-gradient(90deg, color-mix(in oklab, ${p} 90%, ${bg}) 0 8%, color-mix(in oklab, ${p} 35%, ${bg}) 8% 12%, color-mix(in oklab, ${n} 70%, ${bg}) 12% 14%)`,
scanline: `repeating-linear-gradient(0deg, color-mix(in oklab, ${n} 18%, transparent) 0 1px, transparent 1px 3px), radial-gradient(ellipse at 40% 60%, color-mix(in oklab, ${n} 40%, ${bg}), ${bg})`,
fade: `radial-gradient(circle, color-mix(in oklab, ${p} 70%, ${bg}) 1px, transparent 1.5px) 0 0 / 5px 5px, linear-gradient(90deg, ${bg}, ${pf})`,
glow: `radial-gradient(circle at 50% 45%, ${p}, color-mix(in oklab, ${p} 30%, ${bg}) 45%, ${bg} 80%)`,
}
// Position (percent of the section) and width (rem) per swatch; `wide` hides it on phones.
const swatches = [
{ name: 'Pearl Grid', kind: 'Halftone', look: looks.halftone, x: 12, y: 6, w: 11, ratio: '4/3', wide: false },
{ name: 'Low Mist', kind: 'Photo & grain', look: looks.mist, x: 30, y: 12, w: 15, ratio: '3/4', wide: true },
{ name: 'Fluted Glow', kind: 'Fluted glass', look: looks.fluted, x: 50, y: 30, w: 15, ratio: '4/3', wide: false },
{ name: 'Scanline', kind: 'Photo & scanlines', look: looks.scanline, x: 70, y: 4, w: 14, ratio: '1/1', wide: true },
{ name: 'Dot Fade', kind: 'Halftone', look: looks.fade, x: 10, y: 40, w: 12, ratio: '4/3', wide: true },
{ name: 'Soft Bloom', kind: 'Glow & grain', look: looks.glow, x: 70, y: 48, w: 15, ratio: '3/4', wide: true },
{ name: 'Deep Field', kind: 'Aberration', look: looks.mist, x: 90, y: 30, w: 11, ratio: '1/1', wide: true },
]
---
<Section spacing="none" containerWidth="full" class="overflow-hidden">
<div class="relative min-h-[48rem] md:min-h-[56rem]">
{swatches.map((s) => (
<figure class:list={['absolute flex-col gap-3', s.wide ? 'hidden md:flex' : 'flex']} style={`inset-inline-start: ${s.x}%; top: ${s.y}%; width: min(${s.w}rem, 40vw)`} aria-hidden="true">
<div class="w-full rounded-xl" style={`aspect-ratio: ${s.ratio}; background: ${s.look}`}></div>
<figcaption class="flex flex-col font-mono text-xs"><span>{s.name}</span><span class="text-neutral-faded">{s.kind}</span></figcaption>
</figure>
))}
<div class="pointer-events-none absolute inset-0 bg-[linear-gradient(to_top,var(--background-page)_20%,transparent_55%)]" aria-hidden="true"></div>
<div class="absolute inset-x-0 bottom-0 flex flex-col gap-8 px-gutter pb-16 md:flex-row md:items-end md:justify-between">
<div class="flex flex-col items-start gap-6">
{announcement && <Link href={announcement.href} color="inherit" underline="none" class="inline-flex items-center gap-2 rounded-full! border border-neutral-faded bg-page px-3 py-1 text-sm"><Sparkles class="size-4" aria-hidden="true" />{announcement.label}</Link>}
<Heading as="h1" size="display" class="max-w-3xl text-balance">{plain(heading)}</Heading>
</div>
<div class="flex max-w-sm flex-col gap-6">
{description && <p class="text-lg text-neutral-faded">{description}</p>}
{(primaryAction || secondaryAction) && (
<div class="flex flex-wrap gap-3">
{primaryAction && <Button href={primaryAction.href} size="large">{primaryAction.label}<ArrowRight slot="end" class="rtl:-scale-x-100" aria-hidden="true" /></Button>}
{secondaryAction && <Button href={secondaryAction.href} size="large" variant="outline" color="neutral">{secondaryAction.label}</Button>}
</div>
)}
</div>
</div>
</div>
</Section>
Show codeHide code
---
/**
* Swatch scatter 01: Pattern swatches with mono captions scattered across the section, a pill, a large heading at the bottom start and a line with two buttons at the bottom end.
*
* @block hero
* @order 55
*/
import { ArrowRight, Sparkles } from '@lucide/astro'
import Section from '@/components/apogee/Section/Section.astro'
import Heading from '@/components/apogee/Heading/Heading.astro'
import Button from '@/components/apogee/Button/Button.astro'
import Link from '@/components/apogee/Link/Link.astro'
import { plain, type HeroContent } from '@/components/apogee/sections/_shared/spine.ts'
/** @composable */
interface Props extends HeroContent {}
const {
announcement = { label: 'New: share a look, remix a link', href: '#' },
heading = 'Backgrounds nobody else has, in seconds',
description = 'Mix gradients, glass and grain into backgrounds and loops of your own.',
primaryAction = { label: 'Open the studio', href: '#' },
secondaryAction = { label: 'Browse looks', href: '#' },
} = Astro.props
// Swatches are CSS patterns on the primary and neutral tokens, so they follow the theme.
const p = 'var(--background-primary)', pf = 'var(--background-primary-faded)', n = 'var(--foreground-neutral)', bg = 'var(--background-page)'
const looks = {
halftone: `radial-gradient(circle, ${p} 1.2px, transparent 1.6px) 0 0 / 6px 6px, linear-gradient(135deg, ${pf}, ${bg})`,
mist: `radial-gradient(circle at 30% 30%, color-mix(in oklab, ${p} 60%, ${bg}), transparent 60%), radial-gradient(circle at 70% 75%, color-mix(in oklab, ${n} 35%, ${bg}), transparent 55%), ${pf}`,
fluted: `repeating-linear-gradient(90deg, color-mix(in oklab, ${p} 90%, ${bg}) 0 8%, color-mix(in oklab, ${p} 35%, ${bg}) 8% 12%, color-mix(in oklab, ${n} 70%, ${bg}) 12% 14%)`,
scanline: `repeating-linear-gradient(0deg, color-mix(in oklab, ${n} 18%, transparent) 0 1px, transparent 1px 3px), radial-gradient(ellipse at 40% 60%, color-mix(in oklab, ${n} 40%, ${bg}), ${bg})`,
fade: `radial-gradient(circle, color-mix(in oklab, ${p} 70%, ${bg}) 1px, transparent 1.5px) 0 0 / 5px 5px, linear-gradient(90deg, ${bg}, ${pf})`,
glow: `radial-gradient(circle at 50% 45%, ${p}, color-mix(in oklab, ${p} 30%, ${bg}) 45%, ${bg} 80%)`,
}
// Position (percent of the section) and width (rem) per swatch; `wide` hides it on phones.
const swatches = [
{ name: 'Pearl Grid', kind: 'Halftone', look: looks.halftone, x: 12, y: 6, w: 11, ratio: '4/3', wide: false },
{ name: 'Low Mist', kind: 'Photo & grain', look: looks.mist, x: 30, y: 12, w: 15, ratio: '3/4', wide: true },
{ name: 'Fluted Glow', kind: 'Fluted glass', look: looks.fluted, x: 50, y: 30, w: 15, ratio: '4/3', wide: false },
{ name: 'Scanline', kind: 'Photo & scanlines', look: looks.scanline, x: 70, y: 4, w: 14, ratio: '1/1', wide: true },
{ name: 'Dot Fade', kind: 'Halftone', look: looks.fade, x: 10, y: 40, w: 12, ratio: '4/3', wide: true },
{ name: 'Soft Bloom', kind: 'Glow & grain', look: looks.glow, x: 70, y: 48, w: 15, ratio: '3/4', wide: true },
{ name: 'Deep Field', kind: 'Aberration', look: looks.mist, x: 90, y: 30, w: 11, ratio: '1/1', wide: true },
]
---
<Section spacing="none" containerWidth="full" class="overflow-hidden">
<div class="relative min-h-[48rem] md:min-h-[56rem]">
{swatches.map((s) => (
<figure class:list={['absolute flex-col gap-3', s.wide ? 'hidden md:flex' : 'flex']} style={`inset-inline-start: ${s.x}%; top: ${s.y}%; width: min(${s.w}rem, 40vw)`} aria-hidden="true">
<div class="w-full rounded-xl" style={`aspect-ratio: ${s.ratio}; background: ${s.look}`}></div>
<figcaption class="flex flex-col font-mono text-xs"><span>{s.name}</span><span class="text-neutral-faded">{s.kind}</span></figcaption>
</figure>
))}
<div class="pointer-events-none absolute inset-0 bg-[linear-gradient(to_top,var(--background-page)_20%,transparent_55%)]" aria-hidden="true"></div>
<div class="absolute inset-x-0 bottom-0 flex flex-col gap-8 px-gutter pb-16 md:flex-row md:items-end md:justify-between">
<div class="flex flex-col items-start gap-6">
{announcement && <Link href={announcement.href} color="inherit" underline="none" class="inline-flex items-center gap-2 rounded-full! border border-neutral-faded bg-page px-3 py-1 text-sm"><Sparkles class="size-4" aria-hidden="true" />{announcement.label}</Link>}
<Heading as="h1" size="display" class="max-w-3xl text-balance">{plain(heading)}</Heading>
</div>
<div class="flex max-w-sm flex-col gap-6">
{description && <p class="text-lg text-neutral-faded">{description}</p>}
{(primaryAction || secondaryAction) && (
<div class="flex flex-wrap gap-3">
{primaryAction && <Button href={primaryAction.href} size="large">{primaryAction.label}<ArrowRight slot="end" class="rtl:-scale-x-100" aria-hidden="true" /></Button>}
{secondaryAction && <Button href={secondaryAction.href} size="large" variant="outline" color="neutral">{secondaryAction.label}</Button>}
</div>
)}
</div>
</div>
</div>
</Section>Add
npx @ava-ui/apogee add hero/swatch-scatter-01
Every piece of copy is a prop, with the sample copy as its default. Pass yours, or null to hide a part; see copy is props.