COMPONENT / glitch-button
Glitch Button_
Outlined button whose label splits into RGB slices on hover, in short bursts. 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-button
If shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
Preview
registry/ui/glitch-button.tsx
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
// Outlined button whose label splits into two clipped RGB slices on hover. The
// slices are aria-hidden spans, not ::before/::after content, so they never leak
// into the accessible name. They only render when motion is allowed.
export function GlitchButton({
className,
type = "button",
children,
...props
}: ComponentProps<"button">) {
const slice =
"pointer-events-none absolute inset-0 hidden motion-safe:group-enabled:group-hover:block";
return (
<button
type={type}
className={cn(
"group relative inline-flex min-h-13 cursor-pointer items-center justify-center gap-2.5 rounded-none px-7",
"font-mono text-[0.9375rem] font-bold uppercase tracking-button",
"border-(length:--border-base) border-solid border-ink bg-transparent text-ink",
"enabled:hover:border-signal 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-dashed disabled:border-dim disabled:bg-surface disabled:text-label",
className,
)}
{...props}
>
<span className="relative inline-block">
{children}
<span aria-hidden="true" className={cn(slice, "text-signal [clip-path:inset(0_0_55%_0)] glitch-slice-a")}>
{children}
</span>
<span aria-hidden="true" className={cn(slice, "text-neon-2 [clip-path:inset(52%_0_0_0)] glitch-slice-b")}>
{children}
</span>
</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. 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(). |
| disabled | boolean | false | Native disabled. Shows the dashed offline style and never glitches. |
| ...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 { GlitchButton } from "@/components/ui/glitch-button";
export function Action() {
return <GlitchButton onClick={() => console.log("connect")}>Jack in</GlitchButton>;
}
States
- Idle: transparent fill,
--inklabel and 2px solid--inkborder. - Hover: border turns
--signal. Two clipped label copies (top in--signal, bottom in--neon-2) jitter onsteps(3)in a ~400ms burst every 3s. - Pressed: the button presses in 3px (
--offset-hard-hover) on both axes, without a transition. - Disabled:
--surfacefill,--labeltext, 2px dashed--dimborder, no glitch. - 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 slice copies are
aria-hiddenspans, not::before/::aftercontent, so the accessible name is the label once. - The glitch runs only on hover and only as a ~400ms burst every 3s, under the 3-flashes-per-second limit (WCAG 2.3.1). It is idle by default, so it never adds a second always-on glitch to a viewport.
- Under
prefers-reduced-motionthe slices never render; hover only changes the border colour. - Label text is
--inkon whatever sits behind the button. Keep that background dark enough for 4.5:1.