Graphics

Cloud Shapes

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.

Free, MIT licenseOpen in Browse
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}
    />
  )
}

Installation

pnpm dlx shadcn add @01orbital/cloud-shapes
npx shadcn add @01orbital/cloud-shapes
yarn shadcn add @01orbital/cloud-shapes
bunx --bun shadcn add @01orbital/cloud-shapes

Free 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.

Usage

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 const

API reference

Prop

Type

It also accepts the HTML attributes of the element it renders.

Dependencies

npm: cn. The CLI installs them for you.

No other registry pieces.