klyv-ui

Parallax Portal

Galleries 1

About

A frame that opens into a 3D room behind the page, shifting perspective as you move the pointer, use arrow keys, or tilt your phone, for building depth-filled hero scenes without WebGL.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Parallax Portal to your project and fits it to your theme.

Code

How Parallax Portal is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import ParallaxPortal, {
  ParallaxPortalLayer,
} from "@/components/ui/parallax-portal";

/** A small seeded generator, so every render draws the same shapes. */
function seeded(seed: number) {
  let a = seed;
  return () => {
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function Painting({ seed, className }: { seed: number; className?: string }) {
  const rand = seeded(seed);
  const shapes = Array.from({ length: 7 }, () => ({
    x: rand() * 100,
    y: rand() * 70,
    r: 8 + rand() * 22,
    a: rand(),
  }));
  return (
    <div
      className={`border-[6px] border-[color-mix(in_oklab,var(--color-ink)_78%,var(--color-surface))] bg-surface p-2 shadow-[var(--shadow-float)] ${className ?? ""}`}
    >
      <svg
        viewBox="0 0 100 70"
        className="h-full w-full bg-[color-mix(in_oklab,var(--color-accent)_14%,var(--color-surface))]"
        aria-hidden="true"
      >
        {shapes.map((s, i) => (
          <circle
            key={i}
            cx={s.x}
            cy={s.y}
            r={s.r}
            fill={
              i % 3 === 0
                ? "var(--color-accent)"
                : i % 3 === 1
                  ? "var(--color-ink)"
                  : "var(--color-accent-strong)"
            }
            opacity={0.25 + s.a * 0.6}
          />
        ))}
        <path
          d={`M0 ${50 + rand() * 10} Q50 ${30 + rand() * 20} 100 ${48 + rand() * 12} V70 H0 Z`}
          fill="var(--color-ink)"
          opacity="0.8"
        />
      </svg>
    </div>
  );
}

const GALLERY_WALL =
  "color-mix(in oklab, var(--color-surface) 88%, var(--color-accent) 12%)";

const GALLERY_WALLS = {
  back: (
    <div
      className="absolute inset-0 flex flex-col items-center justify-center gap-3"
      style={{ background: GALLERY_WALL }}
    >
      <Painting seed={11} className="h-[46%] w-[40%]" />
      <div className="rounded-[var(--radius-2)] bg-surface px-2 py-1 text-[9px] font-semibold text-ink-soft shadow-[var(--shadow-tile)]">
        Room 3 · Light studies, 2026
      </div>
    </div>
  ),
  left: (
    <div
      className="absolute inset-0 flex items-center justify-around px-8"
      style={{ background: GALLERY_WALL }}
    >
      <Painting seed={4} className="h-[34%] w-[26%]" />
      <Painting seed={21} className="h-[40%] w-[30%]" />
    </div>
  ),
  right: (
    <div
      className="absolute inset-0 flex items-center justify-around px-8"
      style={{ background: GALLERY_WALL }}
    >
      <Painting seed={32} className="h-[40%] w-[30%]" />
      <Painting seed={8} className="h-[30%] w-[24%]" />
    </div>
  ),
  floor: (
    <div className="absolute inset-0 bg-[repeating-linear-gradient(90deg,color-mix(in_oklab,var(--color-ink)_14%,var(--color-surface))_0_46px,color-mix(in_oklab,var(--color-ink)_22%,var(--color-surface))_46px_48px)]">
      <div className="absolute inset-0 bg-[linear-gradient(to_bottom,transparent,color-mix(in_oklab,white_18%,transparent)_55%,transparent)]" />
    </div>
  ),
  ceiling: (
    <div className="absolute inset-0 bg-surface-muted">
      <div className="absolute inset-x-[18%] top-[20%] h-[18%] rounded-[var(--radius-4)] bg-surface shadow-[0_0_40px_color-mix(in_oklab,white_70%,transparent)]" />
      <div className="absolute inset-x-[18%] top-[58%] h-[18%] rounded-[var(--radius-4)] bg-surface shadow-[0_0_40px_color-mix(in_oklab,white_70%,transparent)]" />
    </div>
  ),
};

function Bench() {
  return (
    <svg viewBox="0 0 200 60" className="w-[200px]" aria-hidden="true">
      <rect
        x="0"
        y="8"
        width="200"
        height="16"
        rx="3"
        fill="color-mix(in oklab, var(--color-ink) 70%, var(--color-accent))"
      />
      <rect x="14" y="24" width="10" height="36" fill="var(--color-ink)" />
      <rect x="176" y="24" width="10" height="36" fill="var(--color-ink)" />
    </svg>
  );
}

function Sculpture() {
  return (
    <svg viewBox="0 0 90 190" className="w-[90px]" aria-hidden="true">
      <path
        d="M45 6 C75 20 70 60 48 78 C30 94 70 104 60 124 L30 124 C20 104 42 94 26 70 C12 48 20 14 45 6 Z"
        fill="var(--color-accent)"
      />
      <rect
        x="10"
        y="124"
        width="70"
        height="66"
        fill="var(--color-surface)"
        stroke="var(--color-line-strong)"
      />
    </svg>
  );
}

function Plant() {
  return (
    <svg viewBox="0 0 160 220" className="w-[150px]" aria-hidden="true">
      {[-50, -25, 0, 25, 50, -70, 70].map((a, i) => (
        <path
          key={i}
          d="M80 150 C70 110 60 70 80 20 C100 70 90 110 80 150 Z"
          transform={`rotate(${a} 80 150)`}
          fill={
            i % 2
              ? "color-mix(in oklab, var(--color-accent-strong) 70%, var(--color-ink))"
              : "var(--color-accent-strong)"
          }
        />
      ))}
      <path
        d="M50 150 H110 L102 220 H58 Z"
        fill="color-mix(in oklab, var(--color-ink) 80%, var(--color-surface))"
      />
    </svg>
  );
}

export default function ParallaxPortalGalleryDemo() {
  return (
    <div className="flex w-full items-center justify-center bg-canvas p-10">
      <ParallaxPortal
        label="A small gallery room behind the page"
        walls={GALLERY_WALLS}
        depth={560}
        className="h-[380px] w-full max-w-xl"
      >
        <ParallaxPortalLayer depth={420} x={0.26}>
          <Sculpture />
        </ParallaxPortalLayer>
        <ParallaxPortalLayer depth={230} x={0.58}>
          <Bench />
        </ParallaxPortalLayer>
        <ParallaxPortalLayer depth={170} x={0.8} y={0} anchor="top">
          <div className="flex flex-col items-center">
            <span className="h-16 w-px bg-ink-faint" />
            <span className="h-5 w-12 rounded-t-full bg-ink" />
            <span className="h-3 w-16 rounded-full bg-[radial-gradient(closest-side,color-mix(in_oklab,white_90%,var(--color-accent)),transparent)]" />
          </div>
        </ParallaxPortalLayer>
        <ParallaxPortalLayer depth={-90} x={0.06} y={1.06}>
          <Plant />
        </ParallaxPortalLayer>
        <ParallaxPortalLayer depth={-60} x={0.88} y={0.2} anchor="center">
          <div className="w-[150px] rounded-[var(--radius-tile)] border border-line bg-surface p-3 shadow-[var(--shadow-window)]">
            <p className="text-[10px] font-bold uppercase tracking-wider text-ink-faint">
              Now showing
            </p>
            <p className="text-sm font-bold text-ink">Light studies</p>
            <p className="text-xs text-ink-soft">Twelve works on paper</p>
          </div>
        </ParallaxPortalLayer>
      </ParallaxPortal>
    </div>
  );
}

More by klyv-ui

View all
  • Gantt Chart

    Gantt Chart

    56
  • Klyv Animated Beam

    Klyv Animated Beam

    47
  • Pareto Chart

    Pareto Chart

    40
  • Shader Canvas

    Shader Canvas

    35
  • Sunburst Chart

    Sunburst Chart

    23
  • Morphing Text

    Morphing Text

    21
  • Gooey Loader

    Gooey Loader

    20
  • Gravity Tags

    Gravity Tags

    17

More in Galleries

View all
  • Thumbnail image viewer

    Thumbnail image viewer

    43
  • Animated Gallery

    Animated Gallery

    1.4K
  • File Upload

    File Upload

    18
  • Editorial Collage Hero

    Editorial Collage Hero

    431
  • 3D Parallax Unfurling Gallery

    3D Parallax Unfurling Gallery

    981
  • Image Gallery

    Image Gallery

    123
  • Elastic Gallery

    Elastic Gallery

    908
  • Tilted Grid Hero

    Tilted Grid Hero

    1.5K