Subframe
Animation & motionGlitch text effect

Chromatic signal

Cobalt and vermilion type briefly split into clipped, displaced channels before returning to a clean headline.

Press Disrupt signal.

Subframe Original · MIT

Add to your project

Component source

Standalone React component. Styles are included.

React only. No additional packages.

Render the default export inside a container with a defined height. Its optional active prop stops movement when false. This example works with React 19; Next.js projects can use it as a client component.

View source code
/**
 * MIT License
 *
 * Copyright (c) 2026 Subframe
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

"use client"

import { useEffect, useRef, useState } from "react"

export default function ChromaticSignal({ active = true }: { active?: boolean }) {
  const [pulse, setPulse] = useState(0)
  const [busy, setBusy] = useState(false)
  const [reduced, setReduced] = useState(true)
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
  useEffect(() => {
    const q = matchMedia("(prefers-reduced-motion: reduce)")
    const update = () => setReduced(q.matches)
    update()
    q.addEventListener("change", update)
    return () => {
      q.removeEventListener("change", update)
      if (timer.current) clearTimeout(timer.current)
    }
  }, [])
  useEffect(() => {
    if (!active || reduced) {
      setBusy(false)
      if (timer.current) clearTimeout(timer.current)
    }
  }, [active, reduced])
  function disrupt() {
    if (timer.current) clearTimeout(timer.current)
    setPulse((p) => p + 1)
    setBusy(true)
    timer.current = setTimeout(() => setBusy(false), 480)
  }
  return (
    <section className="sf-chromatic-signal" data-pulse={busy && active && !reduced}>
      <div key={pulse} className="sf-cs-type" aria-label="Frequency Shift">
        <div className="sf-cs-small" data-text="FREQUENCY" aria-hidden="true">
          FREQUENCY
        </div>
        <div className="sf-cs-large" data-text="SHIFT" aria-hidden="true">
          SHIFT
        </div>
      </div>
      <div className="sf-cs-bottom">
        <button type="button" onClick={disrupt} data-busy={busy}>
          Disrupt signal <span>↗</span>
        </button>
      </div>
      <style>{`.sf-chromatic-signal{width:100%;height:100%;min-height:280px;container-type:inline-size;position:relative;overflow:hidden;box-sizing:border-box;background:#fff;color:#1829cb;font-family:Arial,sans-serif;padding:22px 25px;display:flex;flex-direction:column;justify-content:space-between}.sf-chromatic-signal *{box-sizing:border-box}.sf-cs-type{position:relative;line-height:.79;letter-spacing:-.075em;align-self:flex-start;width:100%;font-family:Arial,Helvetica,sans-serif;font-weight:900;flex:1;display:flex;flex-direction:column;justify-content:center}.sf-cs-type>div{position:relative;width:max-content;max-width:100%;isolation:isolate}.sf-cs-small{font-size:12.5cqw;letter-spacing:-.08em}.sf-cs-large{font-size:28.5cqw;color:#ed3126;margin-top:.025em;letter-spacing:-.075em;transform:translateX(-.04em)}.sf-cs-type>div:before,.sf-cs-type>div:after{content:attr(data-text);position:absolute;inset:0;opacity:0;pointer-events:none}.sf-cs-type>div:before{color:#1829cb;z-index:1}.sf-cs-type>div:after{color:#ed3126;z-index:2}.sf-chromatic-signal[data-pulse=true] .sf-cs-type>div:before{opacity:1;animation:sf-cs-break-a 480ms steps(1,end) both}.sf-chromatic-signal[data-pulse=true] .sf-cs-type>div:after{opacity:1;animation:sf-cs-break-b 480ms steps(1,end) both}.sf-cs-bottom{display:flex;align-items:center;justify-content:flex-end;color:#161616}.sf-cs-bottom button{min-height:44px;border:0;border-bottom:1px solid #161616;padding:0;background:none;color:inherit;display:flex;align-items:center;gap:26px;font:11px monospace;cursor:pointer}.sf-cs-bottom button:focus-visible{outline:2px solid #1829cb;outline-offset:4px}.sf-cs-bottom button:active,.sf-cs-bottom button[data-busy=true]{color:#ed3126}@keyframes sf-cs-break-a{0%{clip-path:inset(19% 0 53%);transform:translateX(-8px)}20%{clip-path:inset(61% 0 9%);transform:translateX(11px)}42%{clip-path:inset(37% 0 44%);transform:translateX(-5px)}70%{clip-path:inset(75% 0 8%);transform:translateX(4px)}100%{clip-path:inset(0);transform:translateX(0);opacity:0}}@keyframes sf-cs-break-b{0%{clip-path:inset(51% 0 17%);transform:translateX(7px)}25%{clip-path:inset(9% 0 74%);transform:translateX(-10px)}46%{clip-path:inset(70% 0 13%);transform:translateX(6px)}72%{clip-path:inset(28% 0 56%);transform:translateX(-3px)}100%{clip-path:inset(0);transform:translateX(0);opacity:0}}@media(prefers-reduced-motion:reduce){.sf-cs-type>div:before,.sf-cs-type>div:after{animation:none!important;opacity:0!important}}@container(max-width:360px){.sf-cs-small{font-size:12cqw}.sf-cs-large{font-size:28cqw}.sf-cs-type{transform:scaleY(1.24);transform-origin:center}.sf-cs-bottom button{font-size:10px;gap:20px}}`}</style>
    </section>
  )
}

Where it works well

A finite signal interruption for display typography.

Reduced motion

The control briefly changes color; the headline stays intact.

Things to know

An authored 480ms text effect; not a live audio signal.

More examples