bottega

primitives

Save Bounce Button

A card-like button presenting a layered file icon. On press, the card plays a brief scale-down animation and the top icon layer bounces off with a rotation and upward translation, fading to transparent — evoking a satisfying 'save' gesture. Built from pure CSS :active keyframes with no JS animation library. Accessible: native button semantics, visible label as the accessible name, focus-visible ring from --ring, icon layers are aria-hidden.

Ported from Codecite (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/save-bounce-button

Usage

Usagetsx
<SaveBounceButton />

Props

Standard props — see source.


Source

save-bounce-button.tsxtsx
"use client";

import type React from "react";
import styles from "./save-bounce-button.module.css";

export type SaveBounceButtonProps = React.ComponentProps<"button"> & {
  /** Text label shown below the icon. Provides the button's accessible name. */
  label?: string;
};

/** Reusable layered file icon — rendered twice (back + bouncing front). */
function FileIcon({ className }: { className?: string }) {
  return (
    <svg
      width="50"
      viewBox="0 0 19 21"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      aria-hidden="true"
      className={className}
    >
      {/* Back shadow layer — lightest green (#C4FFE4) */}
      <path
        className={styles.pathLight}
        d="M3.4,4 L11.5,4 L11.5,4 L16,8.25 L16,17.6 C16,19.4777681 14.4777681,21 12.6,21 L3.4,21 C1.52223185,21 6.74049485e-16,19.4777681 0,17.6 L0,7.4 C2.14128934e-16,5.52223185 1.52223185,4 3.4,4 Z"
      />
      {/* Main file body — medium green (#85EBBC) */}
      <path
        className={styles.pathMid}
        d="M6.4,0 L12,0 L12,0 L19,6.5 L19,14.6 C19,16.4777681 17.4777681,18 15.6,18 L6.4,18 C4.52223185,18 3,16.4777681 3,14.6 L3,3.4 C3,1.52223185 4.52223185,7.89029623e-16 6.4,0 Z"
      />
      {/* Corner fold — darkest green (#64B18D) */}
      <path
        className={styles.pathDark}
        d="M12,0 L12,5.5 C12,6.05228475 12.4477153,6.5 13,6.5 L19,6.5 L19,6.5 L12,0 Z"
      />
    </svg>
  );
}

/**
 * A card-like button with a layered file icon. On press the card scales down
 * and the top icon layer bounces off — pure CSS :active keyframes, no JS
 * animation library.
 */
export function SaveBounceButton({
  label = "Save",
  className,
  ...props
}: SaveBounceButtonProps) {
  return (
    <button
      type="button"
      className={[styles.container, className].filter(Boolean).join(" ")}
      {...props}
    >
      {/* Icon group: back icon sets the size; front icon overlays and bounces */}
      <span className={styles.iconGroup} aria-hidden="true">
        <FileIcon className={styles.iconBack} />
        <FileIcon className={styles.iconFront} />
      </span>
      {/* Visible label — also the button's accessible name */}
      <span className={styles.label}>{label}</span>
    </button>
  );
}

Dependencies

  • @bottega/tokens