Tatiana

Refractive Glass Panel

Backgrounds

About

Glass card that refracts the page background at its bezel — canvas-based, works where backdrop-filter fails, reads the background automatically, zero dependencies.

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

Code

How Refractive Glass Panel is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import * as React from "react";
import { GlassPanel, type Painter } from "@/components/ui/refractive-glass-panel";

/* Демо: живой полосатый фон и стеклянная карточка поверх. Полосы выбраны
   не для красоты — на ровной заливке преломление не видно, а вертикальные
   линии показывают его сразу: у кромки они сжимаются в узкую полосу. */

const BASE = 100; /* опорная ширина шейпа: её и масштабируем через scaleX */

/* тень под белым текстом: на горячем фоне без неё буквы разъедает светом */
const INK = "0 1px 3px rgba(0,0,0,.55), 0 0 1px rgba(0,0,0,.4)";

const GLOW: Array<[number, string]> = [
  [0, "#ff8f4e"],
  [0.2, "#f4573c"],
  [0.36, "#d13340"],
  [0.52, "#8d1a25"],
  [0.68, "#350810"],
  [0.85, "#0d0305"],
  [1, "#050305"],
];

/* Фон собран отдельными слоями, а не фоном контейнера, поэтому автоматика
   его не увидит: растрировать чужую вёрстку она не умеет. Отдаём фон стеклу
   сами — кладём в paintRef функцию, которая рисует ту же картину по той же
   раскладке, и полосы под стеклом продолжают наружные кадр в кадр. */
function StripedBackdrop({
  count = 34,
  paintRef,
}: {
  count?: number;
  paintRef: React.MutableRefObject<Painter | null>;
}) {
  const wrapRef = React.useRef<HTMLDivElement | null>(null);
  const barsRef = React.useRef<Array<HTMLSpanElement | null>>([]);

  React.useEffect(() => {
    const wrap = wrapRef.current;
    if (!wrap) return;

    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    /* сегменты чередуются: полоса — просвет — полоса — ... */
    const seg = Array.from({ length: count * 2 }, () => ({
      base: 0.6 + Math.random(),
      amp: 0.25 + Math.random() * 0.45,
      speed: (2 * Math.PI) / (16 + Math.random() * 18), // период 16–34 с
      phase: Math.random() * Math.PI * 2,
    }));

    let width = wrap.clientWidth;
    const ro = new ResizeObserver(() => {
      width = wrap.clientWidth;
    });
    ro.observe(wrap);

    /* пары «начало — конец» каждой полосы: по ним рисует и стекло */
    const edges: number[] = [];

    paintRef.current = (ctx, sw, sh) => {
      ctx.save();
      ctx.translate(sw * 0.5, sh * 0.1);
      ctx.scale(1, (sh * 0.56) / (sw * 0.4));
      const g = ctx.createRadialGradient(0, 0, 0, 0, 0, sw * 0.4);
      GLOW.forEach(([at, color]) => g.addColorStop(at, color));
      ctx.fillStyle = g;
      ctx.fillRect(-sw, -sh * 2, sw * 2, sh * 4);
      ctx.restore();

      ctx.fillStyle = "rgba(0,0,0,.92)";
      for (let n = 0; n < edges.length; n += 2) {
        if (edges[n] === undefined) continue;
        ctx.fillRect((edges[n] / width) * sw, 0, ((edges[n + 1] - edges[n]) / width) * sw, sh);
      }
    };

    let raf = 0;
    const frame = (now: number) => {
      const t = now / 1000;
      const w: number[] = [];
      let total = 0;

      for (const s of seg) {
        const v = s.base * (1 + s.amp * Math.sin(t * s.speed + s.phase));
        w.push(v);
        total += v;
      }

      /* сумма сегментов каждый кадр нормируется в ширину контейнера:
         соседи поджимаются ровно на столько, на сколько вырос сосед */
      const k = width / total;
      let x = 0;

      w.forEach((v, n) => {
        const px = v * k;
        if ((n & 1) === 0) {
          const bar = barsRef.current[n >> 1];
          if (bar) bar.style.transform = `translateX(${x}px) scaleX(${px / BASE})`;
          edges[n] = x;
          edges[n + 1] = x + px;
        }
        x += px; /* нечётный сегмент — просвет, шейпа под ним нет */
      });

      if (!reduce) raf = requestAnimationFrame(frame);
    };

    raf = requestAnimationFrame(frame);
    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      paintRef.current = null;
    };
  }, [count, paintRef]);

  return (
    <>
      <div
        aria-hidden="true"
        className="absolute inset-0"
        style={{
          background:
            "radial-gradient(40% 56% at 50% 10%, #ff8f4e 0%, #f4573c 20%, #d13340 36%, #8d1a25 52%, #350810 68%, #0d0305 85%, #050305 100%)",
        }}
      />
      <div
        ref={wrapRef}
        aria-hidden="true"
        className="absolute inset-0"
        style={{ mixBlendMode: "multiply", opacity: 0.95, contain: "strict" }}
      >
        {Array.from({ length: count }, (_, i) => (
          <span
            key={i}
            ref={(el) => {
              barsRef.current[i] = el;
            }}
            className="absolute bottom-0 left-0 top-0"
            style={{
              width: BASE,
              background: "rgba(0,0,0,.92)",
              transformOrigin: "0 0",
              transform: "translateX(-9999px)", // до первого кадра шейп за экраном
              backfaceVisibility: "hidden",
            }}
          />
        ))}
      </div>
    </>
  );
}

export default function GlassPanelDemo() {
  const paintRef = React.useRef<Painter | null>(null);
  const sceneRef = React.useRef<HTMLDivElement | null>(null);

  /* стабильная ссылка: иначе эффект стекла пересобирался бы каждый рендер */
  const paint = React.useCallback<Painter>((ctx, w, h) => {
    paintRef.current?.(ctx, w, h);
  }, []);

  return (
    <div
      ref={sceneRef}
      className="relative flex min-h-[560px] w-full items-center justify-center overflow-hidden bg-[#050305] p-8"
    >
      <StripedBackdrop paintRef={paintRef} />

      <GlassPanel
        scene={sceneRef}
        paint={paint}
        /* фиксируем подсветку: автозамер целится в среднюю яркость, а здесь
           под плашкой горячее пятно, и белый текст на нём разъедает светом */
        brightness={1.25}
        className="w-full max-w-[360px] rounded-[28px] p-[26px] text-center shadow-[0_24px_48px_rgba(0,0,0,.34)]"
        style={{ borderRadius: 28 }}
      >
        <p className="mb-2.5 text-[11.5px] uppercase tracking-[0.2em] text-white" style={{ textShadow: INK }}>
          Round-the-clock support
        </p>
        <p
          className="mx-auto mb-4 max-w-[15.5rem] text-[13px] leading-relaxed text-white/90"
          style={{ textShadow: INK }}
        >
          A dedicated manager and a night shift — the account never idles.
        </p>

        <ul
          className="mb-5 flex flex-wrap items-center justify-center gap-x-3.5 gap-y-1 text-[11px] text-white/75"
          style={{ textShadow: INK }}
        >
          <li>Production</li>
          <li>Traffic</li>
          <li>Sales</li>
        </ul>

        <div className="inline-flex items-center rounded-full bg-white/[.13] p-[5px] shadow-[inset_0_0_0_1px_rgba(255,255,255,.2)]">
          <span className="px-3 text-[11px] text-white/90">no commission</span>
          <button
            type="button"
            className="rounded-full bg-[#150c0f] px-[18px] py-2.5 text-[11px] uppercase tracking-[0.12em] text-white transition hover:bg-gradient-to-r hover:from-[#ea364a] hover:to-[#f2894f]"
          >
            Get started
          </button>
        </div>
      </GlassPanel>
    </div>
  );
}

More by Tatiana

View all
  • Gooey CTA Button

    Gooey CTA Button

    1
  • Glass Mark 3D

    Glass Mark 3D

  • Letter Rise Heading

    Letter Rise Heading

  • Count Up Stats

    Count Up Stats

  • Curtain Preloader

    Curtain Preloader

  • Striped Glow Background

    Striped Glow Background

More in Backgrounds

View all
  • Elegant Dark Pattern

    Elegant Dark Pattern

    811
  • Gradient Backgrounds

    Gradient Backgrounds

    25
  • Jade Sky

    Jade Sky

    85
  • LivingOrigami bg

    LivingOrigami bg

    135
  • Tailwind CSS Background Snippet

    Tailwind CSS Background Snippet

    536
  • Background snippets

    Background snippets

    14
  • Light theme Tailwind CSS Background Snippet

    Light theme Tailwind CSS Background Snippet

    221
  • Gradient Blur Bg

    Gradient Blur Bg

    18