a Mac's desktop picture or an iPhone's Lock Screen, pure CSS, filling any box
npx shadcn add @ag/wallpaperAlready installed? Preview an update
npx shadcn add @ag/wallpaper --dry-runYour installed source stays unchanged until you update it. How updates work
Latest changes · 2026-10-18
- New: a device's picture, pure CSS.
<Wallpaper of="macos" />is the Mac's desktop picture thatmacosdraws,<Wallpaper of="ios" />an iPhone's Lock Screen, both originals. Each covers any box at any size and shape, its lights placed and blurred in shares of the box, so a card, a Mac's screen and a phone show the same picture.dimdarkens it for a lock screen,childrenstand over it.
the demo above, verbatim
"use client"
import { Sample } from "@/app/samples"
import { IphoneFrame, MacbookFrame } from "@/registry/base-nova/ui/device-frame"
import { Wallpaper, type WallpaperOf } from "@/registry/base-nova/ui/wallpaper"
// #region lock
/** A Lock Screen's words over the picture: the date and the time, large and light. */
function Lock() {
return (
<div className="flex flex-col items-center pt-[24cqw] text-white">
<span className="font-semibold text-[4.5cqw] text-white/85">
Tuesday, September 30
</span>
<span className="font-semibold text-[24cqw] leading-none tracking-tight">
9:41
</span>
</div>
)
}
// #endregion
// #region boxes
/** Boxes of every shape: wide, a Mac's, square, a phone's. */
const BOXES = [
"aspect-[21/9]",
"aspect-[16/10]",
"aspect-square",
"aspect-[9/19.5]",
]
function Boxes({ of }: { of: WallpaperOf }) {
return (
<div className="flex w-full flex-wrap items-end gap-4 p-6">
{BOXES.map((box) => (
<Wallpaper
key={box}
of={of}
className={`${box} h-40 overflow-hidden rounded-xl`}
/>
))}
</div>
)
}
// #endregion
export default function WallpaperDemo() {
return (
<div className="space-y-16">
<Sample
name="devices"
glance="large"
title="a mac and an iphone"
description="each device names its picture; the phone's dimmed under its Lock Screen"
with="lock"
>
<div className="grid w-full items-center gap-10 p-6 sm:grid-cols-[2fr_1fr]">
<MacbookFrame>
<Wallpaper of="macos" className="absolute inset-0" />
</MacbookFrame>
<div className="mx-auto w-full max-w-44">
<IphoneFrame>
<Wallpaper
of="ios"
dim={0.15}
className="absolute inset-0 [container-type:inline-size]"
>
<Lock />
</Wallpaper>
</IphoneFrame>
</div>
</div>
</Sample>
<Sample
name="mac-any-box"
title="the mac's, any box"
description="covered, never stretched: a wide box crops the foot, a narrow one the sides"
with="boxes"
>
<Boxes of="macos" />
</Sample>
<Sample
name="iphone-any-box"
title="the iphone's, any box"
description="held at its middle: a wide box crops the top and foot"
with="boxes"
>
<Boxes of="ios" />
</Sample>
<Sample
name="dim"
title="dim"
description="0 · 0.28 · 0.5, for words over it"
with="lock"
>
<div className="grid w-full grid-cols-3 gap-4 p-6">
{[0, 0.28, 0.5].map((dim) => (
<Wallpaper
key={dim}
of="ios"
dim={dim}
className="aspect-[9/19.5] overflow-hidden rounded-2xl [container-type:inline-size]"
>
<Lock />
</Wallpaper>
))}
</div>
</Sample>
</div>
)
}the source, every file an install writes
// A device's picture: originals, not Apple's (theirs are theirs), in the spirit of
// the ones macOS 26 and iOS 26 ship. `macos`, a desktop picture: deep blue night with
// a luminous sweep of violet and teal across it. `ios`, a Lock Screen: indigo night
// over a violet bloom with a ribbon of teal and rose rising through it. Pure CSS, so
// they cost no request and are sharp at any size. Each fills any box, a Mac's screen,
// a phone, a card, and reads as the same picture in each.
//
// A picture is composed once on its device's screen (its `frame`, in design pixels:
// a MacBook at half scale, an iPhone in points) and shown the way a screen shows a
// picture: covering the box, held where the picture says (`align`). The box's size is
// the one fact this needs, read by CSS as container units, so the server's HTML is
// already the picture at its true size: one design pixel is
// `max(100cqw / width, 100cqh / height)`, and every light's place, size and blur is a
// count of them. A box wider than the frame crops the picture's top and foot, a
// narrower one its sides.
import type * as React from "react"
import { cn } from "@/lib/utils"
/** Which device's picture: a Mac's desktop or an iPhone's Lock Screen. */
export type WallpaperOf = "macos" | "ios"
export interface WallpaperProps {
/** Which device's picture. Said by the caller: a Mac and a phone never share one. */
of: WallpaperOf
/** How far the picture is darkened, 0..1: a lock screen's is about 0.28. */
dim?: number
className?: string
/** Drawn over the picture, in the box's flow. */
children?: React.ReactNode
}
/** One light of a picture, in design pixels: its box on the frame, its blur, and
* what it is made of (its colours, its turn, its shape). */
interface Light {
name: string
x: number
y: number
width: number
height: number
blur: number
className: string
}
interface Picture {
/** The screen it is composed on, in design pixels. */
frame: { width: number; height: number }
/** Where the picture is held when the box crops it, 0..1 of each axis: the part
* that stays when the box is wider or narrower than the frame. */
align: { x: number; y: number }
/** The night under the lights. */
ground: string
lights: Light[]
}
const PICTURES: Record<WallpaperOf, Picture> = {
macos: {
frame: { width: 960, height: 600 },
// Held at the top, as a Mac's screen is: a taller display shows more of its foot.
align: { x: 0.5, y: 0 },
ground: "bg-[linear-gradient(165deg,#0b1230_0%,#070a1c_45%,#04050d_100%)]",
lights: [
{
name: "sweep",
x: -180,
y: 260,
width: 900,
height: 420,
blur: 70,
className:
"rotate-[-18deg] rounded-[50%] bg-[conic-gradient(from_200deg_at_55%_50%,#2f5dff_0deg,#7b4dff_80deg,#1fb6c9_170deg,#2f5dff_260deg,#1a2a80_360deg)] opacity-75",
},
{
name: "streak",
x: 120,
y: 330,
width: 760,
height: 140,
blur: 40,
className:
"rotate-[-14deg] rounded-[50%] bg-[linear-gradient(90deg,transparent,rgb(180_200_255/0.55),rgb(120_230_240/0.35),transparent)]",
},
{
name: "glow",
x: 560,
y: -160,
width: 520,
height: 420,
blur: 90,
className:
"rounded-full bg-[radial-gradient(closest-side,#1d6fd1,transparent)] opacity-55",
},
],
},
ios: {
frame: { width: 390, height: 844 },
// Held at the middle: the bloom is the picture, wherever the box crops it.
align: { x: 0.5, y: 0.5 },
ground:
"bg-[linear-gradient(180deg,#070b22_0%,#0d0a26_40%,#120821_70%,#040309_100%)]",
lights: [
{
name: "glow",
x: 150,
y: -150,
width: 360,
height: 340,
blur: 70,
className:
"rounded-full bg-[radial-gradient(closest-side,#1d6fd1,transparent)] opacity-45",
},
{
name: "bloom",
x: -150,
y: 400,
width: 600,
height: 600,
blur: 60,
className:
"rounded-full bg-[radial-gradient(closest-side,#7b4dff,#3b1aa8_55%,transparent)] opacity-80",
},
{
name: "ribbon",
x: 70,
y: 220,
width: 250,
height: 640,
blur: 46,
className:
"rotate-[24deg] rounded-[50%] bg-[conic-gradient(from_160deg_at_50%_55%,#1fb6c9_0deg,#2f5dff_90deg,#7b4dff_170deg,#e0559a_250deg,#1fb6c9_360deg)] opacity-70",
},
{
name: "streak",
x: 20,
y: 500,
width: 380,
height: 80,
blur: 26,
className:
"rotate-[-34deg] rounded-[50%] bg-[linear-gradient(90deg,transparent,rgb(200_210_255/0.5),rgb(140_235_240/0.35),transparent)]",
},
],
},
}
const LIGHT =
"absolute top-(--ag-wallpaper-top) left-(--ag-wallpaper-left) h-(--ag-wallpaper-height) w-(--ag-wallpaper-width) blur-(--ag-wallpaper-blur)"
/** A count of design pixels as a length of the box. */
const px = (n: number) => `calc(${n} * var(--ag-wallpaper-unit))`
/** One design pixel of the box, the larger of its two shares so the picture covers. */
const unitOf = ({ frame }: Picture) =>
`max(calc(100cqw / ${frame.width}), calc(100cqh / ${frame.height}))`
/** A light's place on the box: on each axis, the box's held point plus the light's
* offset from the frame's held point. */
function placed({ frame, align }: Picture, l: Light): React.CSSProperties {
return {
"--ag-wallpaper-left": `calc(${align.x * 100}cqw + ${px(l.x - align.x * frame.width)})`,
"--ag-wallpaper-top": `calc(${align.y * 100}cqh + ${px(l.y - align.y * frame.height)})`,
"--ag-wallpaper-width": px(l.width),
"--ag-wallpaper-height": px(l.height),
"--ag-wallpaper-blur": px(l.blur),
} as React.CSSProperties
}
/** A device's picture, filling its box; `children` stand over it. Size the box with
* `className` (`absolute inset-0` behind a screen, an aspect for a card). */
export function Wallpaper({
of,
dim = 0,
className,
children,
}: WallpaperProps) {
const picture = PICTURES[of]
if (!picture) throw new Error(`wallpaper: no picture of "${of}"`)
return (
<div
data-slot="wallpaper"
data-of={of}
className={cn("relative isolate", className)}
>
<div
aria-hidden="true"
data-slot="wallpaper-picture"
className={cn(
"absolute inset-0 -z-10 overflow-hidden [container-type:size]",
picture.ground,
)}
style={
{ "--ag-wallpaper-unit": unitOf(picture) } as React.CSSProperties
}
>
{picture.lights.map((l) => (
<div
key={l.name}
data-slot={`wallpaper-${l.name}`}
className={cn(LIGHT, l.className)}
style={placed(picture, l)}
/>
))}
{dim > 0 && (
<div
data-slot="wallpaper-dim"
className="absolute inset-0 bg-black opacity-(--ag-wallpaper-dim)"
style={{ "--ag-wallpaper-dim": dim } as React.CSSProperties}
/>
)}
</div>
{children}
</div>
)
}