# Photoreal iPhone app mockup

> A pixel-honest iPhone 15 Pro hardware frame — titanium rail, Dynamic Island, physical buttons, iOS status bar and home indicator — with a real ChatGPT-for-iOS chat screen dropped inside. Drop any screen in and it scales crisply from one width prop.

- **Category:** Marketing
- **Source (best-in-class):** [ChatGPT for iOS](https://openai.com/chatgpt/mobile/)
- **Tags:** iphone, mockup, device-frame, mobile, chatgpt, ios, dynamic-island, hero, marketing, svg
- **Dependencies:** react
- **Status:** stable
- **Live demo:** https://eito.me/ui/iphone-mock

## Problem

Screenshots of a mobile app look flat and un-shippable in a hero or a pitch; the credibility comes from the phone around them — the titanium edge, the Dynamic Island, the 9:41 status bar. Maintaining that chrome as a bitmap means re-exporting every time the screen changes. This mock authors the whole device once in a fixed logical iOS-point canvas and scales it as a single CSS transform, so every corner radius, button and status glyph stays pixel-crisp at any size and the app screen is live React you can restyle, theme, or animate.

## When to use

- A landing hero or pitch deck needs a mobile app to look like it's really running on a phone.
- You want the device chrome (frame, Dynamic Island, status bar) crisp at any width from a single number, not a fixed-resolution PNG.
- The screen inside should be live, themeable UI (light/dark, real text, typing indicator) rather than a flat screenshot.

## Code — snippet.tsx

```tsx
import type { CSSProperties, ReactNode } from "react";
import { useId } from "react";

/**
 * IPhoneMock — a photoreal iPhone 15 Pro hardware frame you can drop any screen
 * into, shipped with a pixel-honest ChatGPT-for-iOS chat screen as the sample
 * payload.
 *
 * The whole device is authored once in a fixed logical resolution (a 430 × 920
 * "point" canvas that mirrors an iPhone 15 Pro's proportions) and then scaled as
 * a single unit with a CSS transform. That is the trick that keeps it crisp and
 * simple: corner radii, the titanium rail, the Dynamic Island, the status-bar
 * glyphs and every message bubble are laid out in real iOS points, so nothing
 * has to be re-tuned when you change `width` — you set one number and the entire
 * phone scales with sub-pixel fidelity.
 *
 * Realism lives in the details that read subconsciously:
 *   - a brushed-titanium rail drawn with a horizontal metal gradient plus an
 *     inner hairline and a bright top edge (not a flat grey border),
 *   - a true pill Dynamic Island with the front-camera lens sunk into it,
 *   - the four physical buttons (Action, Volume +/−, Side) as metal nubs,
 *   - the exact iOS status bar (9:41, signal, Wi-Fi, battery) and home indicator,
 *   - ChatGPT's real chat idiom: user turns as right-aligned grey bubbles,
 *     assistant turns as full-width unbubbled text, and a pill composer.
 *
 * Deps: react only. Everything is inline SVG + inline styles, so the snippet is
 * self-contained and theme-swaps (light/dark) from a single token object.
 */

/* ---- device geometry (logical iOS points) --------------------------------- */

const DEVICE_W = 430;
const DEVICE_H = 920;
const DEVICE_RADIUS = 68;
const FRAME = 14; // titanium rail + bezel, uniform
const SCREEN_RADIUS = 55;
const BEZEL_RING = 3; // black glass ring between the titanium frame and the screen
const SCREEN_W = DEVICE_W - (FRAME + BEZEL_RING) * 2; // 396 — the actual visible viewport
const SCREEN_H = DEVICE_H - (FRAME + BEZEL_RING) * 2; // 886

/* ---- app colour tokens ----------------------------------------------------- */

export type Scheme = "light" | "dark";

interface AppTokens {
  canvas: string; // screen background
  ink: string; // primary text
  sub: string; // secondary text / icons
  faint: string; // hairlines
  bubble: string; // user bubble fill
  bubbleInk: string; // user bubble text
  composer: string; // composer pill fill
  composerBorder: string;
  accent: string; // send / active
  statusInk: string; // status-bar glyphs (over canvas)
}

const TOKENS: Record<Scheme, AppTokens> = {
  light: {
    canvas: "#ffffff",
    ink: "#0d0d0d",
    sub: "#8e8ea0",
    faint: "#ececf1",
    bubble: "#f4f4f4",
    bubbleInk: "#0d0d0d",
    composer: "#ffffff",
    composerBorder: "#e5e5e5",
    accent: "#0d0d0d",
    statusInk: "#0d0d0d",
  },
  dark: {
    canvas: "#212121",
    ink: "#ececec",
    sub: "#9a9a9a",
    faint: "#ffffff1a",
    bubble: "#303030",
    bubbleInk: "#ececec",
    composer: "#2f2f2f",
    composerBorder: "#ffffff14",
    accent: "#ffffff",
    statusInk: "#ffffff",
  },
};

/* ---- conversation model ---------------------------------------------------- */

export interface Turn {
  role: "user" | "assistant";
  text: string;
  /** Render the assistant turn as an animated typing indicator. */
  typing?: boolean;
}

/** The sample script shown in the demo — swap freely, the shell renders any. */
export const sampleConversation: Turn[] = [
  { role: "user", text: "Explain quantum entanglement like I'm five 🧠" },
  {
    role: "assistant",
    text: "Imagine two magic coins. You flip them into separate boxes and mail one to the Moon. The instant you peek at yours and see heads, you *know* the other is tails — no matter how far apart they are. The coins were linked from the start. That linked-ness is entanglement.",
  },
  { role: "user", text: "Whoa. So can it send messages faster than light? 🚀" },
  {
    role: "assistant",
    text: "Nope — and that's the beautiful catch. You only get a random result until you compare notes over a normal (slower-than-light) channel. So the universe stays safe, but the correlation is still spooky.",
  },
  { role: "assistant", text: "", typing: true },
];

/* ---- brand + system glyphs (inline SVG, no assets) ------------------------- */

/** OpenAI "blossom" knot — stylised so the snippet ships with no external asset. */
function ChatGPTMark({ size = 22, color = "currentColor" }: { size?: number; color?: string }) {
  return (
    <svg width={size} height={size} viewBox="0 0 256 260" aria-hidden="true">
      <path
        fill={color}
        d="M239.184 106.203a64.716 64.716 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.716 64.716 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.665 64.665 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.767 64.767 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483Zm-97.56 136.338a48.397 48.397 0 0 1-31.105-11.255l1.535-.87 51.67-29.825a8.595 8.595 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601Zm-104.466-44.61a48.345 48.345 0 0 1-5.781-32.589l1.534.921 51.722 29.826a8.339 8.339 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803ZM23.549 85.38a48.499 48.499 0 0 1 25.58-21.333v61.39a8.288 8.288 0 0 0 4.195 7.316l62.874 36.272-21.845 12.636a.819.819 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405v.256Zm179.466 41.695-63.08-36.63L161.73 77.86a.819.819 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.544 8.544 0 0 0-4.4-7.213Zm21.742-32.69-1.535-.922-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.716.716 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391v.205ZM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87-51.67 29.825a8.595 8.595 0 0 0-4.246 7.367l-.051 72.697Zm11.868-25.58 28.138-16.217 28.188 16.218v32.434l-28.086 16.218-28.188-16.218-.052-32.434Z"
      />
    </svg>
  );
}

/* iOS status-bar glyphs, drawn to the real proportions. */

function SignalGlyph({ color }: { color: string }) {
  // Four ascending cellular bars.
  return (
    <svg width="18" height="12" viewBox="0 0 18 12" aria-hidden="true">
      {[0, 1, 2, 3].map((i) => (
        <rect
          key={i}
          x={i * 4.6}
          y={9 - i * 3}
          width="3"
          height={3 + i * 3}
          rx="1"
          fill={color}
        />
      ))}
    </svg>
  );
}

function WifiGlyph({ color }: { color: string }) {
  return (
    <svg width="17" height="12" viewBox="0 0 17 12" aria-hidden="true" fill="none">
      <path d="M8.5 2C5.2 2 2.2 3.3 0 5.4l1.6 1.7C3.4 5.4 5.8 4.4 8.5 4.4s5.1 1 6.9 2.7L17 5.4C14.8 3.3 11.8 2 8.5 2Z" fill={color} />
      <path d="M8.5 6.4c-1.9 0-3.6.7-4.9 2l1.7 1.7c.8-.8 2-1.3 3.2-1.3s2.4.5 3.2 1.3l1.7-1.7c-1.3-1.3-3-2-4.9-2Z" fill={color} />
      <circle cx="8.5" cy="11" r="1.4" fill={color} />
    </svg>
  );
}

function BatteryGlyph({ color }: { color: string }) {
  return (
    <svg width="28" height="13" viewBox="0 0 28 13" aria-hidden="true">
      <rect x="0.5" y="0.5" width="23" height="12" rx="3.5" fill="none" stroke={color} strokeOpacity="0.4" />
      <rect x="2" y="2" width="18" height="9" rx="2" fill={color} />
      <rect x="25" y="4" width="2" height="5" rx="1" fill={color} fillOpacity="0.4" />
    </svg>
  );
}

/* Nav + composer glyphs. */

function SidebarGlyph({ color }: { color: string }) {
  return (
    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <rect x="3" y="4.5" width="18" height="15" rx="3.2" stroke={color} strokeWidth="1.7" />
      <line x1="9" y1="4.5" x2="9" y2="19.5" stroke={color} strokeWidth="1.7" />
    </svg>
  );
}

function ComposeGlyph({ color }: { color: string }) {
  return (
    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path
        d="M4 20h4l10.5-10.5a2.12 2.12 0 0 0-3-3L5 17v3Z"
        stroke={color}
        strokeWidth="1.7"
        strokeLinejoin="round"
      />
      <path d="M13.5 6.5l3 3" stroke={color} strokeWidth="1.7" strokeLinecap="round" />
    </svg>
  );
}

function ChevronDown({ color }: { color: string }) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
      <path d="M4 6l4 4 4-4" stroke={color} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function PlusGlyph({ color }: { color: string }) {
  return (
    <svg width="22" height="22" viewBox="0 0 22 22" fill="none" aria-hidden="true">
      <line x1="11" y1="5.5" x2="11" y2="16.5" stroke={color} strokeWidth="2" strokeLinecap="round" />
      <line x1="5.5" y1="11" x2="16.5" y2="11" stroke={color} strokeWidth="2" strokeLinecap="round" />
    </svg>
  );
}

function MicGlyph({ color }: { color: string }) {
  return (
    <svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <rect x="9" y="3" width="6" height="11" rx="3" fill={color} />
      <path d="M6 11a6 6 0 0 0 12 0M12 17v3" stroke={color} strokeWidth="1.8" strokeLinecap="round" />
    </svg>
  );
}

function WaveformGlyph({ color }: { color: string }) {
  // The "voice mode" audio button — stacked bars inside a filled disc.
  return (
    <svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">
      {[
        [3, 6, 6],
        [6.5, 3, 12],
        [10, 5, 8],
        [13.5, 7, 4],
      ].map(([x, y, h], i) => (
        <rect key={i} x={x} y={y} width="1.8" height={h} rx="0.9" fill={color} />
      ))}
    </svg>
  );
}

/* ---- screen: ChatGPT for iOS ---------------------------------------------- */

export interface ChatGPTScreenProps {
  scheme?: Scheme;
  conversation?: Turn[];
  /** Header model label, e.g. "ChatGPT 5" or "ChatGPT 4o". */
  model?: string;
  /** Status-bar clock. iOS marketing shots always read 9:41. */
  time?: string;
}

/**
 * A faithful ChatGPT-for-iOS screen: iOS status bar, the model-name nav bar,
 * a full-width assistant / right-bubbled user thread, and the pill composer.
 * Sized to the phone's inner screen (402 × 892 pt) so it drops straight into
 * IPhoneMock, but it renders standalone too.
 */
export function ChatGPTScreen({
  scheme = "light",
  conversation = sampleConversation,
  model = "ChatGPT 5",
  time = "9:41",
}: ChatGPTScreenProps) {
  const t = TOKENS[scheme];
  const styleId = useId().replace(/[:]/g, "");

  return (
    <div
      style={{
        width: SCREEN_W,
        height: SCREEN_H,
        background: t.canvas,
        color: t.ink,
        display: "flex",
        flexDirection: "column",
        fontFamily:
          '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
        WebkitFontSmoothing: "antialiased",
        overflow: "hidden",
      }}
    >
      <style>{`
        @keyframes ${styleId}-blink { 0%,60%,100%{opacity:.25} 30%{opacity:1} }
        .${styleId}-dot { animation: ${styleId}-blink 1.3s infinite ease-in-out; }
      `}</style>

      {/* Status bar — 9:41 · signal · Wi-Fi · battery */}
      <div
        style={{
          height: 54,
          flexShrink: 0,
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          padding: "0 30px",
          paddingTop: 8,
        }}
      >
        <span style={{ fontSize: 17, fontWeight: 600, letterSpacing: 0.2, color: t.statusInk }}>
          {time}
        </span>
        <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
          <SignalGlyph color={t.statusInk} />
          <WifiGlyph color={t.statusInk} />
          <BatteryGlyph color={t.statusInk} />
        </div>
      </div>

      {/* Nav bar — sidebar · model name (tappable) · new chat */}
      <div
        style={{
          height: 46,
          flexShrink: 0,
          display: "grid",
          gridTemplateColumns: "44px 1fr 44px",
          alignItems: "center",
          padding: "0 12px",
        }}
      >
        <SidebarGlyph color={t.ink} />
        <button
          type="button"
          style={{
            justifySelf: "center",
            display: "inline-flex",
            alignItems: "center",
            gap: 4,
            border: "none",
            background: "transparent",
            color: t.ink,
            fontSize: 17,
            fontWeight: 600,
            cursor: "pointer",
            padding: 0,
          }}
        >
          {model}
          <span style={{ opacity: 0.4 }}>
            <ChevronDown color={t.ink} />
          </span>
        </button>
        <span style={{ justifySelf: "end" }}>
          <ComposeGlyph color={t.ink} />
        </span>
      </div>

      {/* Thread */}
      <div
        style={{
          flex: 1,
          overflow: "hidden",
          display: "flex",
          flexDirection: "column",
          gap: 18,
          padding: "14px 18px 8px",
        }}
      >
        {conversation.map((turn, i) =>
          turn.role === "user" ? (
            <div key={i} style={{ display: "flex", justifyContent: "flex-end" }}>
              <div
                style={{
                  maxWidth: "78%",
                  background: t.bubble,
                  color: t.bubbleInk,
                  padding: "11px 16px",
                  borderRadius: 22,
                  borderBottomRightRadius: 8,
                  fontSize: 16.5,
                  lineHeight: 1.4,
                }}
              >
                {turn.text}
              </div>
            </div>
          ) : (
            <div key={i} style={{ display: "flex", gap: 10, alignItems: "flex-start" }}>
              <div
                style={{
                  flexShrink: 0,
                  width: 30,
                  height: 30,
                  borderRadius: "50%",
                  border: `1px solid ${t.faint}`,
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  color: t.ink,
                }}
              >
                <ChatGPTMark size={19} color={t.ink} />
              </div>
              {turn.typing ? (
                <div style={{ display: "flex", gap: 5, paddingTop: 12 }}>
                  {[0, 1, 2].map((d) => (
                    <span
                      key={d}
                      className={`${styleId}-dot`}
                      style={{
                        width: 7,
                        height: 7,
                        borderRadius: "50%",
                        background: t.sub,
                        animationDelay: `${d * 0.18}s`,
                      }}
                    />
                  ))}
                </div>
              ) : (
                <div style={{ fontSize: 16.5, lineHeight: 1.5, color: t.ink, paddingTop: 3 }}>
                  {renderRich(turn.text)}
                </div>
              )}
            </div>
          ),
        )}
      </div>

      {/* Composer — plus · placeholder · mic · voice */}
      <div style={{ flexShrink: 0, padding: "6px 14px 8px" }}>
        <div
          style={{
            display: "flex",
            alignItems: "center",
            gap: 10,
            background: t.composer,
            border: `1px solid ${t.composerBorder}`,
            borderRadius: 26,
            padding: "8px 10px 8px 12px",
            boxShadow: scheme === "light" ? "0 1px 2px rgba(0,0,0,0.04)" : "none",
          }}
        >
          <PlusGlyph color={t.sub} />
          <span style={{ flex: 1, fontSize: 16.5, color: t.sub }}>Ask anything</span>
          <MicGlyph color={t.ink} />
          <span
            style={{
              width: 34,
              height: 34,
              borderRadius: "50%",
              background: t.accent,
              display: "inline-flex",
              alignItems: "center",
              justifyContent: "center",
              flexShrink: 0,
            }}
          >
            <WaveformGlyph color={t.canvas} />
          </span>
        </div>
      </div>

      {/* Home indicator */}
      <div style={{ height: 26, flexShrink: 0, display: "flex", justifyContent: "center", alignItems: "center" }}>
        <div style={{ width: 138, height: 5, borderRadius: 3, background: t.ink, opacity: 0.85 }} />
      </div>
    </div>
  );
}

/** Minimal *italic* renderer so the sample copy can carry light emphasis. */
function renderRich(text: string): ReactNode {
  return text.split(/(\*[^*]+\*)/g).map((chunk, i) =>
    chunk.startsWith("*") && chunk.endsWith("*") ? (
      <em key={i}>{chunk.slice(1, -1)}</em>
    ) : (
      <span key={i}>{chunk}</span>
    ),
  );
}

/* ---- hardware: the iPhone 15 Pro frame ------------------------------------ */

/** A physical side button drawn as a titanium nub. */
function SideButton({
  side,
  top,
  height,
}: {
  side: "left" | "right";
  top: number;
  height: number;
}) {
  return (
    <div
      style={{
        position: "absolute",
        top,
        height,
        width: 3,
        [side]: -1,
        borderRadius: 2,
        background:
          side === "left"
            ? "linear-gradient(90deg,#3a3a3d,#6b6b70 60%,#48484b)"
            : "linear-gradient(90deg,#48484b,#6b6b70 40%,#3a3a3d)",
        boxShadow: "inset 0 1px 1px rgba(255,255,255,0.25)",
      }}
    />
  );
}

export interface IPhoneMockProps {
  /** Rendered width in CSS px. The whole device scales from here. Default 300. */
  width?: number;
  /** Screen payload. Defaults to the bundled ChatGPT screen. */
  children?: ReactNode;
  /** Which titanium finish the frame reads as. */
  titanium?: "natural" | "black" | "blue";
  style?: CSSProperties;
}

const RAILS: Record<NonNullable<IPhoneMockProps["titanium"]>, string> = {
  // Horizontal metal gradient: dark edge → bright specular → dark edge.
  natural: "linear-gradient(90deg,#2c2c2e 0%,#8b8b90 12%,#5a5a5f 30%,#3d3d40 50%,#5a5a5f 70%,#9a9a9f 88%,#2c2c2e 100%)",
  black: "linear-gradient(90deg,#141416 0%,#4a4a4d 12%,#2a2a2c 30%,#151517 50%,#2a2a2c 70%,#525256 88%,#131315 100%)",
  blue: "linear-gradient(90deg,#2a3340 0%,#7d8a9c 12%,#495260 30%,#333c48 50%,#495260 70%,#8792a4 88%,#28313d 100%)",
};

/**
 * The device frame. Everything inside is authored in the 430 × 920 logical
 * canvas and scaled together via a single transform, so the phone stays crisp
 * at any `width` and the caller only ever thinks in one number.
 */
export default function IPhoneMock({
  width = 300,
  children,
  titanium = "natural",
  style,
}: IPhoneMockProps) {
  const scale = width / DEVICE_W;

  return (
    <div
      style={{
        width: DEVICE_W * scale,
        height: DEVICE_H * scale,
        ...style,
      }}
    >
      <div
        style={{
          width: DEVICE_W,
          height: DEVICE_H,
          transform: `scale(${scale})`,
          transformOrigin: "top left",
          position: "relative",
        }}
      >
        {/* Physical buttons — behind the metal so they read as extrusions */}
        <SideButton side="left" top={196} height={30} /> {/* Action button */}
        <SideButton side="left" top={250} height={62} /> {/* Volume up */}
        <SideButton side="left" top={330} height={62} /> {/* Volume down */}
        <SideButton side="right" top={286} height={104} /> {/* Side button */}

        {/* Titanium frame */}
        <div
          style={{
            position: "absolute",
            inset: 0,
            borderRadius: DEVICE_RADIUS,
            background: RAILS[titanium],
            padding: FRAME,
            boxShadow: [
              "0 30px 60px -15px rgba(0,0,0,0.55)", // cast shadow
              "0 4px 12px rgba(0,0,0,0.4)",
              "inset 0 0 0 1px rgba(255,255,255,0.18)", // bright rail top-line
              "inset 0 -2px 6px rgba(0,0,0,0.5)",
            ].join(","),
            boxSizing: "border-box",
          }}
        >
          {/* Black bezel ring — the sliver of glass surround before the screen */}
          <div
            style={{
              width: "100%",
              height: "100%",
              borderRadius: SCREEN_RADIUS + 2,
              background: "#000",
              padding: 3,
              boxSizing: "border-box",
              boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.9)",
            }}
          >
            {/* Screen — clips the app content */}
            <div
              style={{
                position: "relative",
                width: "100%",
                height: "100%",
                borderRadius: SCREEN_RADIUS,
                overflow: "hidden",
                background: "#000",
              }}
            >
              {children ?? <ChatGPTScreen />}

              {/* Dynamic Island — sits above the app, with a sunken lens */}
              <div
                style={{
                  position: "absolute",
                  top: 12,
                  left: "50%",
                  transform: "translateX(-50%)",
                  width: 118,
                  height: 34,
                  borderRadius: 20,
                  background: "#000",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "flex-end",
                  paddingRight: 12,
                  boxShadow: "inset 0 0 2px rgba(255,255,255,0.06)",
                  pointerEvents: "none",
                }}
              >
                <div
                  style={{
                    width: 11,
                    height: 11,
                    borderRadius: "50%",
                    background:
                      "radial-gradient(circle at 35% 30%, #2b3a4a 0%, #0a0f16 55%, #000 100%)",
                    boxShadow: "inset 0 0 1px rgba(120,170,220,0.5)",
                  }}
                />
              </div>

              {/* Screen glare — a soft diagonal sheen over the glass */}
              <div
                aria-hidden="true"
                style={{
                  position: "absolute",
                  inset: 0,
                  borderRadius: SCREEN_RADIUS,
                  background:
                    "linear-gradient(125deg, rgba(255,255,255,0.10) 0%, rgba(255,255,255,0) 22%, rgba(255,255,255,0) 100%)",
                  pointerEvents: "none",
                }}
              />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
```

## Related

- Machine-readable index (all patterns, with code): https://eito.me/ui/manifest.json
- Plain-HTML mirror: https://eito.me/ui/all.html
- Interactive gallery: https://eito.me/ui
