COMPONENT / hazard-button
Hazard Button_
Destructive-action button framed in hazard stripes that march on hover. 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/hazard-button
If shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
Preview
registry/ui/hazard-button.tsx
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
// Destructive-action button framed in hazard stripes that march on hover. The
// 34px background-size matches hazard-march's travel, so the loop is seamless.
export function HazardButton({
className,
type = "button",
children,
...props
}: ComponentProps<"button">) {
return (
<button
type={type}
className={cn(
"group relative inline-flex cursor-pointer rounded-none p-1.5",
"font-mono text-[0.9375rem] font-bold uppercase tracking-button",
"border-0 hazard-stripes bg-size-[34px_34px]",
"enabled:hover:hazard-march enabled:active:translate-(--offset-hard-hover)",
"focus-visible:outline-(length:--focus-width) focus-visible:outline-offset-(--focus-offset) focus-visible:outline-solid focus-visible:outline-signal",
"disabled:cursor-not-allowed disabled:border-(length:--border-base) disabled:border-dashed disabled:border-dim disabled:bg-none disabled:bg-surface",
className,
)}
{...props}
>
<span className="flex min-h-10 items-center gap-2.5 bg-ground-deep px-5.5 text-ink group-disabled:bg-surface group-disabled:text-label">
{children}
</span>
</button>
);
}
Repo source. The shadcn CLI rewrites import aliases to match your project.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| type | "button" | "submit" | "reset" | "button" | Native button type. Defaults to button so it never submits a form by accident. |
| children | ReactNode | — | Label text, shown on the dark inner plate. |
| className | string | — | Extra classes, merged after the defaults with cn(). |
| disabled | boolean | false | Native disabled. Replaces the stripes with the dashed offline style. |
| ...props | ComponentProps<"button"> | — | Every other native <button> attribute, ref and event handler 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 { HazardButton } from "@/components/ui/hazard-button";
export function Action() {
return <HazardButton onClick={() => confirmPurge()}>Purge cache</HazardButton>;
}
States
- Idle: a 6px frame of -45°
--signal/--ground-deephazard stripes around a--ground-deepplate with an--inklabel. - Hover: the stripes march sideways, one stripe period every 0.6s.
- Pressed: the button presses in 3px (
--offset-hard-hover) on both axes, without a transition. - Disabled: stripes are replaced by a
--surfacefill and a 2px dashed--dimborder; the plate turns--surfacewith--labeltext. - Focus-visible: 3px solid
--signaloutline, offset 6px.
Accessibility
- Renders a native
<button>, so keyboard activation, focus and the button role come for free. typedefaults tobutton, so it never submits a surrounding form unless you passtype="submit".- Keyboard focus shows a bold
--signaloutline offset from the button. disableduses the native attribute: it leaves the tab order and is announced as unavailable. Hover and press effects are switched off while disabled.- The stripes signal danger visually only. The label must say what the action does, and destructive actions still need a confirmation step in your own flow.
- The marching stripes cover a small area and swap colour about 1.7 times a second, under the 3-flashes-per-second limit (WCAG 2.3.1). Under
prefers-reduced-motionthey stay still. - Label text is
--inkon--ground-deep(16.7:1). If you override colours withclassName, keep text contrast at 4.5:1 or above.