Paper-white shapes lit with a two-color crescent shadow, in fourteen geometries from circle and pill to egg, arch and shield. Pinned to a daylight look; shading deepens on dark grounds.
import type { ComponentProps } from "react"
import { cn } from "cn"
// Cloud Dancer shapes: the crescent-lit paper recipe on any geometry.
// Pinned to daylight paper — a lit object does not change with the site theme.
export type CloudPalette = { main: string; accent: string }
const DEFAULT_PALETTE: CloudPalette = {
main: "oklch(0.638 0.107 20)",
accent: "oklch(0.616 0.057 196)",
}
const CLOUD_SHAPES = {
circle: { w: 76, h: 76, radius: "50%" },
pill: { w: 120, h: 48, radius: "9999px" },
vpill: { w: 48, h: 100, radius: "9999px" },
square: { w: 68, h: 68, radius: "6px" },
squircle: { w: 76, h: 76, radius: "18px" },
chip: { w: 110, h: 38, radius: "8px" },
rect: { w: 100, h: 64, radius: "10px" },
blob: { w: 80, h: 72, radius: "60% 40% 50% 50% / 50% 60% 40% 50%" },
egg: { w: 56, h: 74, radius: "50% 50% 50% 50% / 62% 62% 38% 38%" },
pebble: { w: 64, h: 80, radius: "44% 56% 52% 48% / 36% 48% 52% 64%" },
semicircle: { w: 80, h: 40, radius: "80px 80px 6px 6px" },
arch: { w: 56, h: 84, radius: "56px 56px 6px 6px" },
lozenge: { w: 100, h: 56, radius: "50%" },
shield: { w: 60, h: 76, radius: "50% 50% 12% 12% / 40% 40% 12% 12%" },
} as const
// Knob: shading strength. On a dark ground the same tint reads as flat white.
const DEPTH = "[--cloud-depth:1] dark:[--cloud-depth:2.5]"
export type CloudShapeName = keyof typeof CLOUD_SHAPES
const PAPER =
"radial-gradient(circle at 35% 35%, oklch(1 0 0) 0%, oklch(0.995 0.002 90) 40%, oklch(0.915 0.006 85) 100%)"
const crescentShadow = (palette: CloudPalette) =>
[
`inset -6px -8px 16px color-mix(in oklch, ${palette.main} calc(25% * var(--cloud-depth)), transparent)`,
`inset -3px -4px 8px color-mix(in oklch, ${palette.main} calc(15% * var(--cloud-depth)), transparent)`,
"inset 4px 5px 10px oklch(1 0 0 / 0.8)",
"inset 2px 3px 5px oklch(1 0 0 / 0.6)",
`inset -8px -10px 20px color-mix(in oklch, ${palette.accent} calc(10% * var(--cloud-depth)), transparent)`,
`0 4px 12px color-mix(in oklch, ${palette.main} calc(15% * var(--cloud-depth)), transparent)`,
"0 2px 4px oklch(0 0 0 / 0.05)",
].join(", ")
export type CloudShapeProps = Omit<ComponentProps<"div">, "children"> & {
/**
* One of 14 outlines, such as `circle`, `pill`, `squircle`, `egg` or
* `arch`. Each has its own designed size in px.
*/
shape?: CloudShapeName
/**
* Shading tints: `main` for the crescent and drop shadow, `accent` for a
* fainter second tint.
*/
palette?: CloudPalette
/** Multiplies the shape's designed size; 1 is the size it was designed at. */
scale?: number
/** Width in px. Replaces the shape's width times `scale`. */
width?: number
/** Height in px. Replaces the shape's height times `scale`. */
height?: number
}
/**
* A paper-white shape lit from the upper left, with a tinted crescent of
* shade, used as decoration or as a stand-in avatar. The paper stays light in
* every theme; in dark mode the shading is stronger so it does not read as
* flat white.
*
* Hidden from assistive tech. Shading strength is the `--cloud-depth`
* variable (1 in light, 2.5 in dark), which you can override through `style`.
*/
export function CloudShape({
shape = "circle",
palette = DEFAULT_PALETTE,
scale = 1,
width,
height,
className,
style,
...props
}: CloudShapeProps) {
const geo = CLOUD_SHAPES[shape]
const w = width ?? geo.w * scale
const h = height ?? geo.h * scale
return (
<div
aria-hidden="true"
data-slot="cloud-shape"
data-shape={shape}
className={cn("shrink-0", DEPTH, className)}
style={{
width: w,
height: h,
borderRadius: geo.radius,
background: PAPER,
boxShadow: crescentShadow(palette),
...style,
}}
{...props}
/>
)
}
pnpm dlx shadcn add @01orbital/cloud-shapesnpx shadcn add @01orbital/cloud-shapesyarn shadcn add @01orbital/cloud-shapesbunx --bun shadcn add @01orbital/cloud-shapesFree under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.
It lands at components/graphics/cloud-shapes.tsx, as source you own and edit.
import {
CloudShape,
type CloudShapeName,
} from "@/components/graphics/cloud-shapes"
const SHAPES: CloudShapeName[] = [
"circle",
"pill",
"vpill",
"square",
"squircle",
"chip",
"rect",
"blob",
"egg",
"pebble",
"semicircle",
"arch",
"lozenge",
"shield",
]
const TONES = ["rose", "coral", "teal", "purple", "gold"] as const
export default function CloudShapesDemo() {
return (
<div className="flex w-full flex-wrap place-items-center justify-center gap-x-10 gap-y-8 p-6">
{SHAPES.map((shape, i) => (
<CloudShape
key={shape}
shape={shape}
palette={PALETTES[TONES[i % TONES.length]]}
/>
))}
</div>
)
}
const PALETTES = {
rose: {
main: "oklch(0.638 0.107 20.5)",
accent: "oklch(0.616 0.057 195.8)",
},
coral: {
main: "oklch(0.582 0.122 31.3)",
accent: "oklch(0.638 0.107 20.5)",
},
teal: {
main: "oklch(0.616 0.057 195.8)",
accent: "oklch(0.582 0.122 31.3)",
},
purple: {
main: "oklch(0.581 0.057 319.9)",
accent: "oklch(0.638 0.107 20.5)",
},
gold: {
main: "oklch(0.69 0.097 88.5)",
accent: "oklch(0.616 0.057 195.8)",
},
} as constProp
Type
It also accepts the HTML attributes of the element it renders.
npm: cn. The CLI installs them for you.
No other registry pieces.
Compare
Every Graphics component side by side: whether it follows the theme or stays dark, how many parts and custom props it has, and whether it is free. Open one for its preview, API and install.
Color Wash
Background panel of two blurred color fields over a ground with overlay grain, in nine palettes such as delft, gold, vesper and gesso. Blur, field height, grain and ground lift are adjustable.