primitives
Offer Drawer Button
A springy CTA button where two text drawers — 'expires in…' above and '…8 hours' below — spring out with an overshoot cubic-bezier(0,0,0,2.5) on hover while four concave SVG corner pieces splay outward, framing the drawers with a brightness pulse. Active press collapses the drawers and scales the button down for tactile feedback. Fully accessible: native button semantics, focus-visible ring via --ring, keyboard-operable, decorative SVG corners are aria-hidden.
Ported from dexter-st (MIT)
Install
1. Register the namespace (once per project):
json
// components.json — register the @bottega namespace once
{
"registries": {
"@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
}
}2. Add the component:
bash
npx shadcn add @bottega/offer-drawer-buttonUsage
Usagetsx
<OfferDrawerButton />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| topText | string | expires in... | Top drawer text shown above the button on hover. |
| bottomText | string | ...8 hours | Bottom drawer text shown below the button on hover. |
Source
offer-drawer-button.tsxtsx
"use client";
import * as React from "react";
import styles from "./offer-drawer-button.module.css";
const CORNER_PATH =
"M32,32C14.355,32,0,17.645,0,0h.985c0,17.102,13.913,31.015,31.015,31.015v.985Z";
export interface OfferDrawerButtonProps
extends React.ComponentProps<"button"> {
/** Top drawer text shown above the button on hover. */
topText?: string;
/** Bottom drawer text shown below the button on hover. */
bottomText?: string;
}
/**
* OfferDrawerButton — springy CTA with top/bottom text drawers.
*
* All hover/active interaction is pure CSS via :has() — no JS state needed.
* The four corner SVGs are decorative (aria-hidden); drawer text is always in
* the DOM so screen readers read it regardless of opacity.
*/
export function OfferDrawerButton({
children = "Get Offer",
topText = "expires in...",
bottomText = "...8 hours",
className,
type = "button",
...props
}: OfferDrawerButtonProps) {
return (
<div className={styles.container}>
{/* Drawers: in DOM always — opacity:0 at rest, opacity:1 on hover.
Not aria-hidden so screen readers pick up expiry text. */}
<div className={`${styles.drawer} ${styles.drawerTop}`}>{topText}</div>
<div className={`${styles.drawer} ${styles.drawerBottom}`}>{bottomText}</div>
<button
type={type}
className={`${styles.btn}${className ? ` ${className}` : ""}`}
{...props}
>
<span className={styles.btnText}>{children}</span>
</button>
{/* Concave corner SVGs — decorative blending pieces */}
<svg
aria-hidden="true"
className={`${styles.corner} ${styles.corner1}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="-1 1 32 32"
>
<path d={CORNER_PATH} />
</svg>
<svg
aria-hidden="true"
className={`${styles.corner} ${styles.corner2}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="-1 1 32 32"
>
<path d={CORNER_PATH} />
</svg>
<svg
aria-hidden="true"
className={`${styles.corner} ${styles.corner3}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="-1 1 32 32"
>
<path d={CORNER_PATH} />
</svg>
<svg
aria-hidden="true"
className={`${styles.corner} ${styles.corner4}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="-1 1 32 32"
>
<path d={CORNER_PATH} />
</svg>
</div>
);
}
Dependencies
- @bottega/tokens