COMPONENT / glitch-heading
Glitch Heading_
Display heading that tears into signal and neon slices in a short burst every 3.2s, never continuously. Requires @cybr/tokens and @cybr/effects.
Install
1. Map the
@cybrnamespace, once per project. Add this inside yourcomponents.json, or into its existing"registries"object. Nocomponents.jsonyet? Runnpx shadcn@latest initfirst to create one."registries": { "@cybr": "https://cybr-ui.com/r/{name}.json" }2. Add components:
$ npx shadcn@latest add @cybr/glitch-heading
If shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
Preview
Signal lost
registry/ui/glitch-heading.tsx
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
// Display heading that tears into signal and neon slices in a short burst every
// 3.2s. The slices are aria-hidden spans, not ::before/::after content, so the
// accessible name is the heading text alone. They never render under reduced
// motion. Use at most one glitch element per viewport.
export function GlitchHeading({
as: Tag = "h2",
className,
children,
...props
}: ComponentProps<"h2"> & { as?: HeadingLevel }) {
const slice = "pointer-events-none absolute inset-0 motion-reduce:hidden";
return (
<Tag
className={cn(
"relative inline-block font-display text-display-md uppercase [line-height:0.9] text-ink",
className,
)}
{...props}
>
{children}
<span aria-hidden="true" className={cn(slice, "text-signal glitch-heading-a")}>
{children}
</span>
<span aria-hidden="true" className={cn(slice, "text-neon-2 glitch-heading-b")}>
{children}
</span>
</Tag>
);
}
Repo source. The shadcn CLI rewrites import aliases to match your project.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| as | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "h2" | Heading level. Pick the one that fits your document outline; the look does not change. |
| children | ReactNode | — | Heading text. It is rendered once visibly and twice more as aria-hidden slice copies, so keep it to plain text or inline elements. |
| className | string | — | Extra classes, merged after the defaults with cn(). |
| ...props | ComponentProps<"h2"> | — | Every other native heading attribute and ref is forwarded. |
Usage
Needs both stylesheets from @cybr/tokens and @cybr/effects. After the install, import them in app/globals.css:
@import "tailwindcss";
@import "../styles/cybr-tokens.css";
@import "../styles/cybr-effects.css";
import { GlitchHeading } from "@/components/ui/glitch-heading";
export function Outage() {
return <GlitchHeading as="h1">Signal lost</GlitchHeading>;
}
Behaviour
- Trigger: always on. It is a CSS animation, so it needs no JavaScript and is server-safe.
- Timing: a 3.2s cycle. For about 300ms near the end of each cycle, a
--signalcopy and a--neon-2copy each jump through three clipped slices offset up to 7px sideways. The rest of the cycle shows the plain heading. - Type: display font, uppercase,
text-display-md, line-height 0.9,--ink. Override the size withclassName. - Static: under
prefers-reduced-motionthe slice copies never render.
Accessibility
- The slice copies are
aria-hiddenspans, not::before/::aftercontent, so the heading text is read once. assets the real heading level; the default ish2.- Each burst is about 300ms once every 3.2s, so no second has more than three flashes (WCAG 2.3.1). It is never continuous.
- Use at most one glitch element per viewport. This page shows a single instance.
- Under
prefers-reduced-motionit is a plain static heading. --inkon--bgis 14.63:1. If you override colours withclassName, keep text at 4.5:1 or above.