bottega

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)

expires in...
...8 hours

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-button

Usage

Usagetsx
<OfferDrawerButton />

Props

PropTypeDefaultDescription
topTextstringexpires in...Top drawer text shown above the button on hover.
bottomTextstring...8 hoursBottom 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