COMPONENT / hard-offset-button
Hard-Offset Button_
Signal-filled button with a solid offset shadow that steps in on hover and presses flush on click. Requires @cybr/tokens.
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/hard-offset-button
If shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
Preview
registry/ui/hard-offset-button.tsx
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
// Solid offset shadow that steps in on hover and presses flush on click.
// Classes avoid pairs that a stock tailwind-merge would collapse, because
// installed copies use the consumer's own cn().
export function HardOffsetButton({
className,
type = "button",
...props
}: ComponentProps<"button">) {
return (
<button
type={type}
className={cn(
"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-heavy) border-solid border-ink bg-signal text-ground-deep shadow-hard",
"transition-[translate,box-shadow] duration-(--duration-snap) ease-step-2 motion-reduce:transition-none",
"enabled:hover:translate-(--offset-hard-hover) enabled:hover:shadow-hard-hover",
"enabled:active:translate-(--offset-hard) enabled:active:shadow-none",
"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-surface disabled:text-label disabled:shadow-none",
className,
)}
{...props}
/>
);
}
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. |
| className | string | — | Extra classes, merged after the defaults with cn(). |
| disabled | boolean | false | Native disabled. Drops the shadow and shows the dashed offline style. |
| ...props | ComponentProps<"button"> | — | Every other native <button> attribute, ref and event handler is forwarded. |
Usage
import { HardOffsetButton } from "@/components/ui/hard-offset-button";
export function Cta() {
return <HardOffsetButton onClick={() => console.log("deploy")}>Deploy now</HardOffsetButton>;
}
States
- Idle:
--signalfill,--ground-deeplabel, 3px solid--inkborder and a 6px solid--inkoffset shadow. - Hover: steps 3px toward the shadow, which shrinks to 3px (
steps(2), 90ms). - Pressed: steps the full 6px so it sits flush, with no shadow.
- Disabled:
--surfacefill,--labeltext, 2px dashed--dimborder, no shadow and no movement. - 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 shadow. disableduses the native attribute: it leaves the tab order and is announced as unavailable.- Under
prefers-reduced-motionthe hover and press offset still apply, but without a transition. - Label text is
--ground-deepon--signal. If you override colours withclassName, keep text contrast at 4.5:1 or above.