本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
ヒーロー

ヒーローの背景の暗い部屋に光の筋が斜めに差し込み、塵が舞う

デモ

仕様

用途
ヒーロー
動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

暗い部屋に差し込む光を、CSSだけで作った背景です。光源を軸にした細長い帯を扇状に並べ、clip-path で先端が狭まる形に切り抜き、外側をぼかしてから screen の合成で重ねています。ぼかしと切り抜きを同じ要素にかけると輪郭が硬くなるので、二重の要素に分けています。

筋の角度や太さ、明るさは帯ごとに違う値を持たせ、周波数を整数にした揺れと明滅で動かしています。塵は光の扇の中にだけ散らし、小さな弧を描きながら漂わせ、光源に近いほど明るくしています。

背景の飾りなので、aria-hidden を付けて読み上げからは外しています。動きを減らす設定では、光と塵を最初の位置のまま止め、静止した画面として見せます。

角度や本数、色を変えるだけで、朝の光にも舞台の照明にも寄せられます。カフェ・ホテル・ドキュメンタリーなど、落ち着いた暗いヒーローの背景に向いています。

コード

HTML
<section class="rays-hero">
  <!-- 光の筋と塵は飾りなので、JS が組み立てて読み上げから外す -->
  <div class="rays-hero__scene" aria-hidden="true"></div>
  <div class="rays-hero__inner">
    <p class="rays-hero__eyebrow">SINCE 2014</p>
    <h1 class="rays-hero__title">朝の光と、<br>一杯のコーヒー。</h1>
    <p class="rays-hero__lead">ここに説明文が入ります。</p>
    <a class="rays-hero__button" href="/menu/">メニューを見る</a>
  </div>
</section>

CSS
.rays-hero {
  --paper: #eeece6;
  --night: #111111;
  --light: 210, 185, 153; /* 光の色(r, g, b)。グレージュ */
  --dust: 224, 211, 192; /* 塵の色(r, g, b)。グレージュを明るくしたもの */
  --gutter: clamp(24px, 7vw, 96px);
  position: relative;
  display: grid;
  align-items: center;
  min-height: min(90vh, 640px);
  padding: 0 var(--gutter);
  overflow: hidden;
  color: var(--paper);
  background: linear-gradient(165deg, #231e19 0%, #151311 50%, #0b0a09 100%);
}

.rays-hero__scene {
  position: absolute;
  inset: 0;
  isolation: isolate; /* screen の重ね合わせをこの中だけに閉じる */
}

.rays-hero__ray {
  position: absolute;
  transform-origin: 0 50%;
  mix-blend-mode: screen;
}

.rays-hero__ray-fill {
  width: 100%;
  height: 100%;
  /* 先広がりの形に切り抜く */
  clip-path: polygon(0 46%, 100% 0, 100% 100%, 0 54%);
}

.rays-hero__glow {
  position: absolute;
  left: 4%;
  top: -18%;
  width: 70%;
  height: 100%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  mix-blend-mode: screen;
}

.rays-hero__dust {
  position: absolute;
  border-radius: 50%;
  mix-blend-mode: screen;
  transform: translate(-50%, -50%);
}

.rays-hero__inner {
  position: relative;
  max-width: 30em;
  text-shadow: 0 2px 24px rgba(7, 4, 12, 0.85);
}

.rays-hero__eyebrow {
  margin: 0;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0.8;
}

.rays-hero__title {
  margin: 24px 0 0;
  font-size: clamp(36px, 7vw, 60px);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.02em;
}

.rays-hero__lead {
  margin: 24px 0 0;
  font-size: 18px;
  line-height: 1.8;
  word-break: auto-phrase; /* 語の途中で折り返さない */
}

.rays-hero__button {
  display: inline-grid;
  place-items: center;
  height: 48px;
  margin-top: 32px;
  padding: 0 32px;
  border-radius: 999px;
  background: var(--paper);
  color: var(--night);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-decoration: none;
}

.rays-hero__button:focus-visible { outline: 2px solid var(--paper); outline-offset: 4px; }

JavaScript
import { gsap } from "gsap";

const DURATION = 10; // 揺れと明滅がひと巡りする秒数
const SOURCE = [4, -18]; // 光源の位置(コンテナに対する %。左上の外)
const LENGTH = 190; // 筋の長さ(コンテナ幅に対する %)
const DUST = 60;
// angle: 光源から見た向き(度)/ spread: 太さ(高さに対する %)/ alpha: 明るさ / sway: 揺れ角(度)/ f: 周波数(整数)/ blur: ぼかし(高さに対する比)
const RAYS = [
  { angle: 43, spread: 110, alpha: 0.12, sway: 0.4, f: 1, phase: 0.0, blur: 0.08 },
  { angle: 24, spread: 10, alpha: 0.68, sway: 1.2, f: 1, phase: 0.0, blur: 0.012 },
  { angle: 30, spread: 26, alpha: 0.38, sway: 0.9, f: 2, phase: 1.1, blur: 0.02 },
  { angle: 36, spread: 7, alpha: 0.72, sway: 1.4, f: 1, phase: 2.3, blur: 0.01 },
  { angle: 42, spread: 20, alpha: 0.42, sway: 0.8, f: 3, phase: 3.0, blur: 0.018 },
  { angle: 49, spread: 9, alpha: 0.6, sway: 1.2, f: 2, phase: 4.2, blur: 0.012 },
  { angle: 56, spread: 30, alpha: 0.28, sway: 1.0, f: 1, phase: 5.1, blur: 0.024 },
  { angle: 63, spread: 8, alpha: 0.55, sway: 1.1, f: 2, phase: 0.7, blur: 0.012 },
];
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

document.querySelectorAll(".rays-hero").forEach((hero) => {
  const scene = hero.querySelector(".rays-hero__scene");
  // 光と塵の色は CSS 変数から読む
  const style = getComputedStyle(hero);
  const light = style.getPropertyValue("--light").trim();
  const dustColor = style.getPropertyValue("--dust").trim();

  const rays = RAYS.map((ray) => {
    const outer = document.createElement("div");
    outer.className = "rays-hero__ray";
    outer.style.cssText = `left:${SOURCE[0]}%;top:${SOURCE[1] - ray.spread / 2}%;width:${LENGTH}%;height:${ray.spread}%;filter:blur(${Math.round(ray.blur * scene.clientHeight)}px);`;
    const inner = document.createElement("div");
    inner.className = "rays-hero__ray-fill";
    inner.style.background = `linear-gradient(90deg, rgba(${light}, ${ray.alpha}), rgba(${light}, ${ray.alpha * 0.55}) 40%, rgba(${light}, 0))`;
    outer.append(inner);
    scene.append(outer);
    return { node: outer, ray };
  });

  const glow = document.createElement("div");
  glow.className = "rays-hero__glow";
  glow.style.background = `radial-gradient(circle, rgba(${light}, 0.5), rgba(${light}, 0.14) 35%, rgba(${light}, 0) 70%)`;
  scene.append(glow);

  // 位置・大きさ・周波数は組み立てるときに決めて、以後は変えない
  const dust = Array.from({ length: DUST }, () => {
    const angle = ((22 + Math.random() * 43) * Math.PI) / 180;
    const dist = 25 + Math.random() * 95;
    const size = 2 + Math.random() * 3;
    const node = document.createElement("div");
    node.className = "rays-hero__dust";
    node.style.cssText = `left:${SOURCE[0]}%;top:${SOURCE[1]}%;width:${size}px;height:${size}px;background:rgb(${dustColor});box-shadow:0 0 ${size * 2}px rgba(${dustColor}, 0.8);`;
    scene.append(node);
    return {
      node,
      x: SOURCE[0] + Math.cos(angle) * dist,
      y: SOURCE[1] + Math.sin(angle) * dist,
      ax: 0.5 + Math.random() * 2,
      ay: 1 + Math.random() * 3,
      fx: 1 + Math.floor(Math.random() * 2),
      fy: 1 + Math.floor(Math.random() * 2),
      twinkle: 1 + Math.floor(Math.random() * 3),
      phase: Math.random() * Math.PI * 2,
      alpha: (0.35 + Math.random() * 0.65) * (1.1 - dist / 130),
    };
  });

  function shine(t) {
    const a = (t / DURATION) * Math.PI * 2;
    for (const { node, ray } of rays) {
      gsap.set(node, {
        rotation: ray.angle + ray.sway * Math.sin(a * ray.f + ray.phase),
        opacity: 0.65 + 0.35 * Math.sin(a * ray.f + ray.phase * 1.7),
      });
    }
    gsap.set(glow, { opacity: 0.8 + 0.2 * Math.sin(a * 2) });
    for (const d of dust) {
      gsap.set(d.node, {
        left: `${d.x + d.ax * Math.sin(a * d.fx + d.phase)}%`,
        top: `${d.y + d.ay * Math.sin(a * d.fy + d.phase * 1.3)}%`,
        opacity: Math.max(0, d.alpha * (0.55 + 0.45 * Math.sin(a * d.twinkle + d.phase * 2.1))),
      });
    }
  }

  shine(0);
  // 動きを減らす設定では、静止した光の絵のまま
  if (reduceMotion) return;
  gsap.ticker.add((time) => shine(time % DURATION));
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。