Clipboard Rails Components

Copy text, URLs, code snippets, and more to the user's clipboard with a single click. Perfect for sharing links, copying API keys, and code examples.

Installation

1. Stimulus Controller Setup

Start by adding the following controller to your project:

import { Controller } from "@hotwired/stimulus";
// import ClipboardJS from "clipboard"; Removed ClipboardJS import
import { computePosition, offset, flip, shift, arrow, autoUpdate } from "@floating-ui/dom";

export default class extends Controller {
  static values = {
    successMessage: { type: String, default: "Copied!" }, // Success message to show when text is copied
    errorMessage: { type: String, default: "Failed to copy!" }, // Error message to show when text is not copied
    showTooltip: { type: Boolean, default: true }, // Whether to show the tooltip
    tooltipPlacement: { type: String, default: "top" }, // Placement(s) of the tooltip, e.g., "top", "top-start", "top-end", "bottom", "bottom-start", "bottom-end", "left", "left-start", "left-end", "right", "right-start", "right-end"
    tooltipOffset: { type: Number, default: 8 }, // Offset of the tooltip
    tooltipDuration: { type: Number, default: 2000 }, // Duration of the tooltip
  };

  static targets = ["copyContent", "copiedContent"];

  connect() {
    // this.clipboard = new ClipboardJS(this.element);
    // this.clipboard.on("success", (e) => this.handleSuccess(e));
    // this.clipboard.on("error", (e) => this.handleError(e));
    this.boundCopyTextToClipboard = this._copyTextToClipboard.bind(this);
    this.element.addEventListener("click", this.boundCopyTextToClipboard);

    this.cleanupAutoUpdate = null;
    this.tooltipElement = null;
    this.arrowElement = null;
    this.hideTooltipTimeout = null;
    this.removeTooltipDOMTimeout = null;
    this.intersectionObserver = null;
  }

  disconnect() {
    // if (this.clipboard) {
    //   this.clipboard.destroy();
    // }
    if (this.boundCopyTextToClipboard) {
      this.element.removeEventListener("click", this.boundCopyTextToClipboard);
    }
    clearTimeout(this.hideTooltipTimeout);
    clearTimeout(this.removeTooltipDOMTimeout);
    if (this.intersectionObserver) {
      this.intersectionObserver.disconnect();
      this.intersectionObserver = null;
    }
    this._removeTooltipDOM(); // This also handles cleanupAutoUpdate
  }

  handleSuccess(e) {
    if (this.hasCopyContentTarget && this.hasCopiedContentTarget) {
      this.showCopiedState();
    }
    if (this.showTooltipValue) {
      this._showFloatingTooltip(this.successMessageValue);
    }
    // if (e && typeof e.clearSelection === "function") {  Removed e.clearSelection()
    //   e.clearSelection();
    // }
  }

  handleError(e) {
    if (this.showTooltipValue) {
      this._showFloatingTooltip(this.errorMessageValue);
    }
  }

  showCopiedState() {
    if (this.hasCopyContentTarget && this.hasCopiedContentTarget) {
      this.copyContentTarget.classList.add("hidden");
      this.copiedContentTarget.classList.remove("hidden");
      setTimeout(() => {
        if (this.hasCopyContentTarget && this.hasCopiedContentTarget) {
          // Check targets still exist
          this.copyContentTarget.classList.remove("hidden");
          this.copiedContentTarget.classList.add("hidden");
        }
      }, this.tooltipDurationValue);
    }
  }

  async _copyTextToClipboard() {
    const textToCopy = this.element.dataset.clipboardText || this.element.getAttribute("data-clipboard-text");
    if (textToCopy === null || typeof textToCopy === "undefined") {
      console.warn("No text to copy. Missing data-clipboard-text attribute on element:", this.element);
      this.handleError({ message: "No text to copy specified." }); // Pass a mock error object or just the message
      return;
    }

    try {
      await navigator.clipboard.writeText(textToCopy);
      this.handleSuccess(); // Removed 'e' argument as it's not provided by navigator.clipboard
    } catch (err) {
      console.error("Failed to copy text: ", err);
      this.handleError(err); // Pass the actual error object
    }
  }

  _createOrUpdateTooltipDOMSafer(message) {
    if (!this.tooltipElement) {
      this.tooltipElement = document.createElement("div");
      this.tooltipElement.className =
        "tooltip-content pointer-events-none shadow-sm border rounded-lg border-white/10 absolute bg-[#333333] text-white text-sm py-1 px-2 z-[1000] opacity-0 transition-opacity duration-150";
      // Example: this.tooltipElement.style.maxWidth = `${this.maxWidthValue}px`; // If you add a maxWidthValue

      const messageSpan = document.createElement("span");
      messageSpan.setAttribute("data-tooltip-message-span", "");
      this.tooltipElement.appendChild(messageSpan);

      // Create arrow container with padding to prevent clipping at viewport edges
      this.arrowContainer = document.createElement("div");
      this.arrowContainer.className = "absolute z-[1000]";

      this.arrowElement = document.createElement("div");
      this.arrowElement.className = "tooltip-arrow-element bg-[#333333] w-2 h-2 border-white/10";
      this.arrowElement.style.transform = "rotate(45deg)";

      this.arrowContainer.appendChild(this.arrowElement);
      this.tooltipElement.appendChild(this.arrowContainer);

      const appendTarget = this.element.closest("dialog[open]") || document.body;
      appendTarget.appendChild(this.tooltipElement);
    }

    const messageSpan = this.tooltipElement.querySelector("[data-tooltip-message-span]");
    if (messageSpan) {
      messageSpan.textContent = message;
    }
  }

  _removeTooltipDOM() {
    if (this.cleanupAutoUpdate) {
      this.cleanupAutoUpdate();
      this.cleanupAutoUpdate = null;
    }
    if (this.intersectionObserver) {
      this.intersectionObserver.disconnect();
      this.intersectionObserver = null;
    }
    if (this.tooltipElement && this.tooltipElement.parentElement) {
      this.tooltipElement.remove();
    }
    this.tooltipElement = null;
    this.arrowContainer = null;
    this.arrowElement = null;
  }

  async _showFloatingTooltip(message) {
    if (!this.showTooltipValue) return;

    clearTimeout(this.hideTooltipTimeout);
    clearTimeout(this.removeTooltipDOMTimeout);

    // Ensure any old instance is fully gone before creating a new one
    // This handles cases where the tooltip is shown again quickly.
    if (this.tooltipElement) {
      this._removeTooltipDOM();
    }
    this._createOrUpdateTooltipDOMSafer(message);

    if (!this.tooltipElement) return; // Should not happen if _createOrUpdateTooltipDOMSafer worked

    this.tooltipElement.style.visibility = "visible";

    // Ensure autoUpdate is cleaned up if it was somehow active without a tooltipElement (defensive)
    if (this.cleanupAutoUpdate) this.cleanupAutoUpdate();

    const referenceElement = this.element;
    this.cleanupAutoUpdate = autoUpdate(
      referenceElement,
      this.tooltipElement,
      async () => {
        if (!this.tooltipElement || !this.arrowContainer) {
          if (this.cleanupAutoUpdate) {
            this.cleanupAutoUpdate();
            this.cleanupAutoUpdate = null;
          }
          return;
        }

        // Parse placement value to support multiple placements
        const placements = this.tooltipPlacementValue.split(/[\s,]+/).filter(Boolean);
        const primaryPlacement = placements[0] || "top";
        const fallbackPlacements = placements.slice(1);

        const middleware = [
          offset(this.tooltipOffsetValue),
          flip({
            fallbackPlacements: fallbackPlacements.length > 0 ? fallbackPlacements : undefined,
          }),
          shift({ padding: 5 }),
        ];
        middleware.push(arrow({ element: this.arrowContainer, padding: 2 }));

        const { x, y, placement, middlewareData } = await computePosition(referenceElement, this.tooltipElement, {
          placement: primaryPlacement,
          middleware: middleware,
        });

        Object.assign(this.tooltipElement.style, {
          left: `${x}px`,
          top: `${y}px`,
        });

        if (this.arrowContainer && this.arrowElement && middlewareData.arrow) {
          const { x: arrowX, y: arrowY } = middlewareData.arrow;
          const basePlacement = placement.split("-")[0];
          const staticSide = { top: "bottom", right: "left", bottom: "top", left: "right" }[basePlacement];

          // Apply appropriate padding based on placement direction
          this.arrowContainer.classList.remove("px-1", "py-1");
          if (basePlacement === "top" || basePlacement === "bottom") {
            this.arrowContainer.classList.add("px-1"); // Horizontal padding for top/bottom
          } else {
            this.arrowContainer.classList.add("py-1"); // Vertical padding for left/right
          }

          // Position the arrow container
          Object.assign(this.arrowContainer.style, {
            left: arrowX != null ? `${arrowX}px` : "",
            top: arrowY != null ? `${arrowY}px` : "",
            right: "",
            bottom: "",
            [staticSide]: "-0.28rem", // Matches tooltip_controller.js arrow positioning
          });

          // Style the arrow element within the container
          this.arrowElement.classList.remove("border-t", "border-r", "border-b", "border-l");
          if (staticSide === "bottom") this.arrowElement.classList.add("border-b", "border-r");
          else if (staticSide === "top") this.arrowElement.classList.add("border-t", "border-l");
          else if (staticSide === "left") this.arrowElement.classList.add("border-b", "border-l");
          else if (staticSide === "right") this.arrowElement.classList.add("border-t", "border-r");
        }
      },
      { animationFrame: true }
    );

    // Setup intersection observer to hide tooltip when trigger element goes out of view
    if (this.intersectionObserver) {
      this.intersectionObserver.disconnect();
    }
    this.intersectionObserver = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (!entry.isIntersecting) {
            this._hideFloatingTooltip();
          }
        });
      },
      { threshold: 0 } // Hide as soon as any part goes out of view
    );
    this.intersectionObserver.observe(this.element);

    requestAnimationFrame(() => {
      if (this.tooltipElement) {
        this.tooltipElement.classList.remove("opacity-0");
        this.tooltipElement.classList.add("opacity-100");
      }
    });

    this.hideTooltipTimeout = setTimeout(() => {
      this._hideFloatingTooltip();
    }, this.tooltipDurationValue);
  }

  _hideFloatingTooltip() {
    if (!this.tooltipElement || !this.tooltipElement.classList.contains("opacity-100")) {
      if (!this.tooltipElement) {
        // If element is already gone, ensure related properties are nulled.
        this._removeTooltipDOM();
      }
      return;
    }

    this.tooltipElement.classList.remove("opacity-100");
    this.tooltipElement.classList.add("opacity-0");

    if (this.cleanupAutoUpdate) {
      this.cleanupAutoUpdate();
      this.cleanupAutoUpdate = null;
    }

    if (this.intersectionObserver) {
      this.intersectionObserver.disconnect();
      this.intersectionObserver = null;
    }

    this.removeTooltipDOMTimeout = setTimeout(() => {
      this._removeTooltipDOM();
    }, 150); // Transition duration (duration-150)
  }
}

2. Floating UI Installation

The clipboard component relies on Floating UI for intelligent tooltip positioning. Choose your preferred installation method:

pin "@floating-ui/dom", to: "https://cdn.jsdelivr.net/npm/@floating-ui/[email protected]/+esm"
Terminal
npm install @floating-ui/dom
Terminal
yarn add @floating-ui/dom

Examples

Basic clipboard button

A simple clipboard button that copies text to the user's clipboard.

<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
        data-controller="clipboard"
        data-clipboard-text="Hello from Rails Blocks!">
  Copy Text
</button>

Clipboard with icon states

A clipboard button that shows different icons for copy and copied states with visual feedback.

<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
        data-controller="clipboard"
        data-clipboard-text="https://railsblocks.com"
        data-clipboard-success-message-value="URL Copied!">
  <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
    <span>Copy URL</span>
    <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 sm:size-4" width="18" height="18" viewBox="0 0 18 18">
      <g fill="currentColor">
        <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
      </g>
    </svg>
  </div>
  <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5 text-green-400 dark:text-green-500">
    <span>Copied!</span>
    <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 sm:size-4" width="18" height="18" viewBox="0 0 18 18">
      <g fill="currentColor">
        <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
      </g>
    </svg>
  </div>
</button>

Clipboard with input field

An input field with an attached copy button for copying predefined text or URLs.

<div class="flex max-w-md">
  <input
    type="text"
    id="url-input"
    value="https://railsblocks.com"
    readonly
    class="flex-1 rounded-l-lg border border-r-0 border-neutral-300 bg-neutral-50 px-3 py-2 text-sm text-neutral-600 dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 dark:focus-visible:outline-neutral-200">
  <button
    class="rounded-r-lg border border-l border-neutral-300 bg-white px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 dark:focus-visible:outline-neutral-200"
    data-controller="clipboard"
    data-clipboard-text="https://railsblocks.com"
    data-clipboard-success-message-value="Link copied to clipboard!">
    <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
      <svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18">
        <g fill="currentColor">
          <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
        </g>
      </svg>
    </div>
    <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
      <svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18">
        <g fill="currentColor">
          <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
        </g>
      </svg>
    </div>
  </button>
</div>

Clipboard for code blocks

A code block with a positioned copy button for copying code snippets and commands.

Your code...
<div class="relative max-w-lg w-full">
  <pre class="bg-neutral-900 text-neutral-100 p-4 rounded-lg overflow-x-auto text-sm"><code>Your code...</code></pre>
  <button
    class="absolute top-2 right-4 flex items-center gap-1.5 rounded-lg px-3.5 py-2 text-sm font-medium shadow-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 bg-neutral-800/50 text-neutral-50 hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200 border border-neutral-700"
    data-controller="clipboard"
    data-clipboard-text="Your code..."
    data-clipboard-success-message-value="Code copied!"
    data-clipboard-show-tooltip-value="true">
    <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
      <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
        <g fill="currentColor">
          <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
        </g>
      </svg>
      <span>Copy</span>
    </div>
    <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
      <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
        <g fill="currentColor">
          <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
        </g>
      </svg>
      <span>Copied!</span>
    </div>
  </button>
</div>

Icon-only clipboard button

A minimal icon-only clipboard button with tooltip support.

<button
  class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 p-2.5 text-xs font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
  data-controller="clipboard"
  data-clipboard-text="Copy this secret text!"
  data-clipboard-success-message-value="Secret copied!"
  data-clipboard-show-tooltip-value="true">
  <div data-clipboard-target="copyContent">
    <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 sm:size-4" width="18" height="18" viewBox="0 0 18 18">
      <g fill="currentColor">
        <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
      </g>
    </svg>
  </div>
  <div data-clipboard-target="copiedContent" class="hidden">
    <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 sm:size-4 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
      <g fill="currentColor">
        <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
      </g>
    </svg>
  </div>
</button>

Clipboard without tooltip

A clipboard button with tooltips disabled, showing only the visual state changes.

<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
        data-controller="clipboard"
        data-clipboard-text="Rails Blocks - Premium UI components for Rails"
        data-clipboard-show-tooltip-value="false"
        data-clipboard-success-message-value="Description copied!">
  <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
    <span>Copy Description</span>
    <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 sm:size-4" width="18" height="18" viewBox="0 0 18 18">
      <g fill="currentColor">
        <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
      </g>
    </svg>
  </div>
  <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
    <span>Copied!</span>
    <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 sm:size-4 text-green-500" width="18" height="18" viewBox="0 0 18 18">
      <g fill="currentColor">
        <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
      </g>
    </svg>
  </div>
</button>

Clipboard with different tooltip positions

Demonstrate the four available tooltip positions: top (default), bottom, left, and right using the "tooltipPlacement" configuration value.

<div class="space-y-6">
  <div class="flex flex-col sm:grid grid-cols-3 gap-4 place-items-center py-8">
    <!-- Top row - top positions -->
    <div class="flex flex-col items-center space-y-2 w-full">
      <button
        class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
        data-controller="clipboard"
        data-clipboard-text="Top start tooltip!"
        data-clipboard-tooltip-placement-value="top-start"
        data-clipboard-success-message-value="Top start shown!">
        <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
            </g>
          </svg>
          <span>top-start</span>
        </div>
        <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
            </g>
          </svg>
          <span>Copied!</span>
        </div>
      </button>
    </div>

    <div class="flex flex-col items-center space-y-2 w-full">
      <button
        class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
        data-controller="clipboard"
        data-clipboard-text="Top tooltip!"
        data-clipboard-tooltip-placement-value="top"
        data-clipboard-success-message-value="Top tooltip shown!">
        <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
            </g>
          </svg>
          <span>top</span>
        </div>
        <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
            </g>
          </svg>
          <span>Copied!</span>
        </div>
      </button>
    </div>

    <div class="flex flex-col items-center space-y-2 w-full">
      <button
        class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
        data-controller="clipboard"
        data-clipboard-text="Top end tooltip!"
        data-clipboard-tooltip-placement-value="top-end"
        data-clipboard-success-message-value="Top end shown!">
        <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
            </g>
          </svg>
          <span>top-end</span>
        </div>
        <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
            </g>
          </svg>
          <span>Copied!</span>
        </div>
      </button>
    </div>

    <%# Middle row - left positions %>
    <div class="w-full col-start-1 flex gap-4">
      <button
        class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
        data-controller="clipboard"
        data-clipboard-text="Left start tooltip!"
        data-clipboard-tooltip-placement-value="left-start"
        data-clipboard-success-message-value="Left start shown!">
        <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
            </g>
          </svg>
          <span>left-start</span>
        </div>
        <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
            </g>
          </svg>
          <span>Copied!</span>
        </div>
      </button>
    </div>

    <%# Center - demonstrates the reference point %>
    <div class="relative hidden sm:block">

    </div>

    <%# Middle row - right positions %>
    <div class="w-full col-start-3 flex gap-4">
      <button
        class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
        data-controller="clipboard"
        data-clipboard-text="Right start tooltip!"
        data-clipboard-tooltip-placement-value="right-start"
        data-clipboard-success-message-value="Right start shown!">
        <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
            </g>
          </svg>
          <span>right-start</span>
        </div>
        <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
            </g>
          </svg>
          <span>Copied!</span>
        </div>
      </button>
    </div>

    <%# Second middle row %>
    <div class="w-full col-start-1 flex gap-4">
      <button
        class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
        data-controller="clipboard"
        data-clipboard-text="Left tooltip!"
        data-clipboard-tooltip-placement-value="left"
        data-clipboard-success-message-value="Left tooltip shown!">
        <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
            </g>
          </svg>
          <span>left</span>
        </div>
        <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
            </g>
          </svg>
          <span>Copied!</span>
        </div>
      </button>
    </div>

    <div class="hidden sm:block"></div>

    <div class="w-full col-start-3 flex gap-4">
      <button
        class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
        data-controller="clipboard"
        data-clipboard-text="Right tooltip!"
        data-clipboard-tooltip-placement-value="right"
        data-clipboard-success-message-value="Right tooltip shown!">
        <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
            </g>
          </svg>
          <span>right</span>
        </div>
        <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
            </g>
          </svg>
          <span>Copied!</span>
        </div>
      </button>
    </div>

    <%# Third middle row %>
    <div class="w-full col-start-1 flex gap-4">
      <button
        class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
        data-controller="clipboard"
        data-clipboard-text="Left end tooltip!"
        data-clipboard-tooltip-placement-value="left-end"
        data-clipboard-success-message-value="Left end shown!">
        <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
            </g>
          </svg>
          <span>left-end</span>
        </div>
        <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
            </g>
          </svg>
          <span>Copied!</span>
        </div>
      </button>
    </div>

    <div class="hidden sm:block"></div>

    <div class="w-full col-start-3 flex gap-4">
      <button
        class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
        data-controller="clipboard"
        data-clipboard-text="Right end tooltip!"
        data-clipboard-tooltip-placement-value="right-end"
        data-clipboard-success-message-value="Right end shown!">
        <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
            </g>
          </svg>
          <span>right-end</span>
        </div>
        <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
            <g fill="currentColor">
              <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
            </g>
          </svg>
          <span>Copied!</span>
        </div>
      </button>
    </div>

    <%# Bottom row - bottom positions %>
    <button
      class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
      data-controller="clipboard"
      data-clipboard-text="Bottom start tooltip!"
      data-clipboard-tooltip-placement-value="bottom-start"
      data-clipboard-success-message-value="Bottom start shown!">
      <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
        <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
          <g fill="currentColor">
            <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
          </g>
        </svg>
        <span>bottom-start</span>
      </div>
      <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
        <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
          <g fill="currentColor">
            <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
          </g>
        </svg>
        <span>Copied!</span>
      </div>
    </button>

    <button
      class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
      data-controller="clipboard"
      data-clipboard-text="Bottom tooltip!"
      data-clipboard-tooltip-placement-value="bottom"
      data-clipboard-success-message-value="Bottom tooltip shown!">
      <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
        <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
          <g fill="currentColor">
            <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
          </g>
        </svg>
        <span>bottom</span>
      </div>
      <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
        <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
          <g fill="currentColor">
            <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
          </g>
        </svg>
        <span>Copied!</span>
      </div>
    </button>

    <button
      class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200"
      data-controller="clipboard"
      data-clipboard-text="Bottom end tooltip!"
      data-clipboard-tooltip-placement-value="bottom-end"
      data-clipboard-success-message-value="Bottom end shown!">
      <div data-clipboard-target="copyContent" class="flex items-center gap-1.5">
        <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18">
          <g fill="currentColor">
            <path d="M12.75,2h-.275c-.123-.846-.845-1.5-1.725-1.5h-3.5c-.879,0-1.602,.654-1.725,1.5h-.275c-1.517,0-2.75,1.233-2.75,2.75V14.25c0,1.517,1.233,2.75,2.75,2.75h7.5c1.517,0,2.75-1.233,2.75-2.75V4.75c0-1.517-1.233-2.75-2.75-2.75Zm-5.75,.25c0-.138,.112-.25,.25-.25h3.5c.138,0,.25,.112,.25,.25v1c0,.138-.112,.25-.25,.25h-3.5c-.138,0-.25-.112-.25-.25v-1Zm4.75,10.25H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Zm0-3H6.25c-.414,0-.75-.336-.75-.75s.336-.75,.75-.75h5.5c.414,0,.75,.336,.75,.75s-.336,.75-.75,.75Z"></path>
          </g>
        </svg>
        <span>bottom-end</span>
      </div>
      <div data-clipboard-target="copiedContent" class="hidden flex items-center gap-1.5">
        <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 text-green-600 dark:text-green-400" width="18" height="18" viewBox="0 0 18 18">
          <g fill="currentColor">
            <path d="M6.75,15h-.002c-.227,0-.442-.104-.583-.281L2.165,9.719c-.259-.324-.207-.795,.117-1.054,.325-.259,.796-.206,1.054,.117l3.418,4.272L14.667,3.278c.261-.322,.732-.373,1.055-.111,.322,.261,.372,.733,.111,1.055L7.333,14.722c-.143,.176-.357,.278-.583,.278Z"></path>
          </g>
        </svg>
        <span>Copied!</span>
      </div>
    </button>
  </div>
</div>

Configuration

The clipboard component uses the native Navigator Clipboard API and provides customizable tooltips, success messages, and visual feedback through a Stimulus controller.

Controller Setup

Basic clipboard structure with required data attributes:

<button data-controller="clipboard" data-clipboard-text="Text to copy">
  <div data-clipboard-target="copyContent">Copy</div>
  <div data-clipboard-target="copiedContent" class="hidden">Copied!</div>
</button>

Configuration Values

Prop Description Type Default
successMessage
Message displayed in tooltip when copy succeeds String "Copied!"
errorMessage
Message displayed in tooltip when copy fails String "Failed to copy!"
showTooltip
Controls whether tooltips are displayed Boolean true
tooltipPlacement
Position of the tooltip (top, bottom, left, right) String "top"
tooltipOffset
Distance between tooltip and button in pixels Number 8
tooltipDuration
How long tooltip and copied state persist (milliseconds) Number 2000

Targets

Target Description Required
copyContent
The element shown when ready to copy (usually contains copy icon/text) Optional
copiedContent
The element shown when copy is successful (usually contains checkmark/success text) Optional

Data Attributes

Attribute Description Required
data-clipboard-text
The text content that will be copied to the user's clipboard when the button is clicked Required

Browser Support

  • Modern Browsers: Uses Navigator Clipboard API for secure, async copying
  • HTTPS Required: Clipboard API requires secure context (HTTPS or localhost)
  • Error Handling: Graceful fallback with error messages for unsupported browsers

Accessibility Features

  • Keyboard Accessible: Buttons are focusable and activate with Enter/Space keys
  • Screen Reader Friendly: Clear visual state changes and meaningful button text
  • Focus Management: Proper focus indicators and focus-visible styles

Table of contents