COMPONENT / warning-tape
Warning Tape_
Tilted hazard-stripe tape with scrolling label chips, or a static stripe band without items. Pauses on hover, focus and button; static under reduced motion. 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/warning-tape
If shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
Preview
- CAUTION // UNDER CONSTRUCTION
- RESTRICTED SECTOR
- CAUTION // UNDER CONSTRUCTION
- RESTRICTED SECTOR
registry/ui/warning-tape.tsx
"use client";
import { useState, type ComponentProps, type CSSProperties, type ReactNode } from "react";
import { cn } from "@/lib/utils";
const tapeVariants = {
signal: "h-10 hazard-stripes",
ink: "h-5.5 opacity-85 bg-[repeating-linear-gradient(-45deg,var(--ink)_0_10px,var(--ground-deep)_10px_20px)]",
};
// Tilted hazard tape that overhangs its container. With `items`, label chips
// scroll along it (in reverse by default), pausing on hover, while focus is
// inside, and from the PAUSE button (WCAG 2.2.2); the second copy is hidden from
// assistive tech. Without items it is a static, decorative stripe band. Under
// reduced motion nothing moves and the button is not shown.
export function WarningTape({
items = [],
variant = "signal",
tilt = -2.5,
duration = 20,
reverse = true,
pauseControl = true,
className,
style,
...props
}: Omit<ComponentProps<"div">, "children"> & {
items?: ReactNode[];
variant?: "signal" | "ink";
tilt?: number;
duration?: number;
reverse?: boolean;
pauseControl?: boolean;
}) {
const [paused, setPaused] = useState(false);
const hasItems = items.length > 0;
const list = (hidden?: boolean) => (
<ul aria-hidden={hidden || undefined} className="m-0 flex shrink-0 list-none p-0">
{items.map((item, i) => (
<li key={i} className="mr-35 bg-ground-deep px-3.5 py-1.5 whitespace-nowrap">
{item}
</li>
))}
</ul>
);
return (
<div
className={cn("group relative overflow-hidden py-6", className)}
style={
{
"--marquee-duration": `${duration}s`,
"--marquee-direction": reverse ? "reverse" : "normal",
...style,
} as CSSProperties
}
{...props}
>
<div
aria-hidden={hasItems ? undefined : "true"}
className={cn("-mx-10 flex items-center overflow-hidden", tapeVariants[variant])}
style={{ transform: `rotate(${tilt}deg)` }}
>
{hasItems ? (
<div
className={cn(
"marquee flex w-max font-mono text-[0.8125rem] font-bold tracking-[0.2em] text-ink group-focus-within:[animation-play-state:paused] group-hover:[animation-play-state:paused]",
paused && "[animation-play-state:paused]",
)}
>
{list()}
{list(true)}
</div>
) : null}
</div>
{hasItems && pauseControl ? (
<button
type="button"
onClick={() => setPaused((p) => !p)}
className="absolute top-2 right-2 z-10 cursor-pointer bg-ground-deep px-3 py-1 font-mono text-caption font-bold tracking-label text-ink focus-visible:outline-(length:--focus-width) focus-visible:outline-offset-2 focus-visible:outline-ink motion-reduce:hidden"
>
{paused ? "PLAY" : "PAUSE"}
<span className="sr-only"> warning tape</span>
</button>
) : null}
</div>
);
}
Repo source. The shadcn CLI rewrites import aliases to match your project.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | ReactNode[] | [] | Label chips that scroll along the tape. Without items, the tape is a static, decorative band. |
| variant | "signal" | "ink" | "signal" | Signal is the 40px hazard tape. Ink is the thinner ink-striped tape. |
| tilt | number | -2.5 | Rotation in degrees. The tape overhangs each side by 40px so the ends stay hidden. |
| duration | number | 20 | Seconds for one full loop. |
| reverse | boolean | true | Scroll left to right, as on the artboard. Set false for right to left. |
| pauseControl | boolean | true | Show the PAUSE/PLAY button when there are items. |
| ...props | Omit<ComponentProps<"div">, "children"> | — | Every other native <div> attribute is forwarded, such as aria-label; className is merged last with cn(). Set the background behind the tape here. |
Usage
Needs the 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 { WarningTape } from "@/components/ui/warning-tape";
export function Construction() {
return <WarningTape className="bg-ground-deep" items={["CAUTION // UNDER CONSTRUCTION", "RESTRICTED SECTOR"]} />;
}
States
- Running: the chips scroll in reverse (left to right) along the tilted tape.
- Hover or focus inside: the scroll pauses.
- Paused by the button: stays paused until PLAY is pressed.
- Reduced motion: the tape is static and the button is not rendered.
- No items: a static, decorative stripe band, hidden from assistive tech, with no button. The demo stacks one under the signal tape, as on the artboard.
- The stripes reuse the shared
hazard-stripesutility, so they are 12px rather than the artboard's 14px. A palette swap changes them in one place.
Accessibility
- With items, the chips render once as a list and the second copy is
aria-hidden. Without items, the whole tape isaria-hidden. - The PAUSE/PLAY button meets WCAG 2.2.2 for keyboard and touch users. Its name is "PAUSE warning tape" / "PLAY warning tape". It sits unrotated in the top-right corner and has an
--inkfocus outline. - The scroll is a smooth slide with no flashing.
- Chip text is
--inkon--ground-deepchips, at least the 14.6:1 measured on--bg. If you override colours, keep text contrast at 4.5:1 or above.