Web InnoventixFreeCode

Arrow Slide Button

Original · free

Buttons where an arrow slides in and the label shifts on hover.

byWeb InnoventixReact + Tailwind
btnarrowslidebuttons
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/btn-arrow-slide.json
btn-arrow-slide.tsx
"use client";

import { useState, type ReactNode } from "react";

type Size = "sm" | "md" | "lg";

const sizeMap: Record<Size, string> = {
  sm: "h-9 px-4 text-sm gap-2 rounded-lg",
  md: "h-11 px-5 text-[0.9375rem] gap-2.5 rounded-xl",
  lg: "h-14 px-7 text-base gap-3 rounded-2xl",
};

const arrowSizeMap: Record<Size, string> = {
  sm: "h-4 w-4",
  md: "h-[1.125rem] w-[1.125rem]",
  lg: "h-5 w-5",
};

function ArrowGlyph({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2.25}
      strokeLinecap="round"
      strokeLinejoin="round"
      className={className}
      aria-hidden="true"
    >
      <path d="M5 12h14" />
      <path d="m13 6 6 6-6 6" />
    </svg>
  );
}

function Spinner({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      className={`bas-spin ${className ?? ""}`}
      aria-hidden="true"
    >
      <circle
        cx="12"
        cy="12"
        r="9"
        stroke="currentColor"
        strokeWidth={2.5}
        className="opacity-25"
      />
      <path
        d="M21 12a9 9 0 0 0-9-9"
        stroke="currentColor"
        strokeWidth={2.5}
        strokeLinecap="round"
      />
    </svg>
  );
}

const focusRing =
  "outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-950";

/* ── Variant 1: arrow slides in from the right edge, label shifts left ── */
function SlideInRight({ size = "md" }: { size?: Size }) {
  return (
    <button
      type="button"
      className={`bas-group group relative inline-flex items-center justify-center overflow-hidden font-semibold text-white shadow-sm transition-all duration-300 active:scale-[0.98] bg-indigo-600 hover:bg-indigo-500 focus-visible:ring-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400 ${sizeMap[size]} ${focusRing}`}
    >
      <span className="bas-shift inline-block transition-transform duration-300 ease-out group-hover:-translate-x-1.5">
        Get started
      </span>
      <span className="bas-enter-r pointer-events-none absolute right-4 flex translate-x-6 items-center opacity-0 transition-all duration-300 ease-out group-hover:translate-x-0 group-hover:opacity-100">
        <ArrowGlyph className={arrowSizeMap[size]} />
      </span>
    </button>
  );
}

/* ── Variant 2: arrow slides in from the left, label shifts right ── */
function SlideInLeft({ size = "md" }: { size?: Size }) {
  return (
    <button
      type="button"
      className={`bas-group group relative inline-flex items-center justify-center overflow-hidden border font-semibold transition-all duration-300 active:scale-[0.98] border-zinc-300 bg-white text-zinc-900 hover:border-zinc-400 focus-visible:ring-zinc-400 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-50 dark:hover:border-zinc-500 ${sizeMap[size]} ${focusRing}`}
    >
      <span className="bas-enter-l pointer-events-none absolute left-4 flex -translate-x-6 items-center opacity-0 transition-all duration-300 ease-out group-hover:translate-x-0 group-hover:opacity-100">
        <ArrowGlyph className={arrowSizeMap[size]} />
      </span>
      <span className="bas-shift inline-block transition-transform duration-300 ease-out group-hover:translate-x-1.5">
        View pricing
      </span>
    </button>
  );
}

/* ── Variant 3: swap — resting arrow exits right, fresh arrow enters left ── */
function ArrowSwap({ size = "md" }: { size?: Size }) {
  return (
    <button
      type="button"
      className={`bas-group group relative inline-flex items-center justify-center font-semibold text-white shadow-sm transition-all duration-300 active:scale-[0.98] bg-emerald-600 hover:bg-emerald-500 focus-visible:ring-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-400 ${sizeMap[size]} ${focusRing}`}
    >
      <span>Continue</span>
      <span className="relative flex h-full items-center overflow-hidden" style={{ width: "1.25rem" }}>
        <ArrowGlyph
          className={`${arrowSizeMap[size]} absolute transition-all duration-300 ease-out group-hover:translate-x-6 group-hover:opacity-0`}
        />
        <ArrowGlyph
          className={`${arrowSizeMap[size]} absolute -translate-x-6 opacity-0 transition-all duration-300 ease-out group-hover:translate-x-0 group-hover:opacity-100`}
        />
      </span>
    </button>
  );
}

/* ── Variant 4: underline link with a nudging arrow ── */
function LinkArrow({ size = "md" }: { size?: Size }) {
  return (
    <button
      type="button"
      className={`bas-group group inline-flex items-center gap-1.5 rounded-md font-semibold text-violet-700 transition-colors hover:text-violet-600 focus-visible:ring-violet-500 dark:text-violet-300 dark:hover:text-violet-200 ${
        size === "sm" ? "text-sm" : size === "lg" ? "text-base" : "text-[0.9375rem]"
      } px-1 py-0.5 ${focusRing}`}
    >
      <span className="bg-gradient-to-r from-current to-current bg-[length:0%_1.5px] bg-left-bottom bg-no-repeat transition-[background-size] duration-300 ease-out group-hover:bg-[length:100%_1.5px]">
        Read the case study
      </span>
      <ArrowGlyph
        className={`${arrowSizeMap[size]} transition-transform duration-300 ease-out group-hover:translate-x-1`}
      />
    </button>
  );
}

/* ── Variant 5: pill with an arrow reveal in a circular chip ── */
function ChipArrow({ size = "md" }: { size?: Size }) {
  return (
    <button
      type="button"
      className={`bas-group group inline-flex items-center gap-3 rounded-full border border-amber-300 bg-amber-50 pr-1.5 font-semibold text-amber-900 transition-colors hover:border-amber-400 hover:bg-amber-100 focus-visible:ring-amber-500 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-200 dark:hover:bg-amber-500/20 ${
        size === "sm" ? "h-9 pl-4 text-sm" : size === "lg" ? "h-14 pl-7 text-base" : "h-11 pl-5 text-[0.9375rem]"
      } ${focusRing}`}
    >
      <span>Book a demo</span>
      <span
        className={`flex items-center justify-center rounded-full bg-amber-500 text-amber-950 transition-transform duration-300 ease-out group-hover:translate-x-0.5 dark:bg-amber-400 ${
          size === "sm" ? "h-6 w-6" : size === "lg" ? "h-10 w-10" : "h-8 w-8"
        }`}
      >
        <ArrowGlyph className={arrowSizeMap[size]} />
      </span>
    </button>
  );
}

/* ── Variant 6: async submit — idle → loading → done, arrow-driven ── */
function AsyncArrow({ size = "md" }: { size?: Size }) {
  const [status, setStatus] = useState<"idle" | "loading" | "done">("idle");

  function handleClick() {
    if (status !== "idle") return;
    setStatus("loading");
    window.setTimeout(() => setStatus("done"), 1400);
    window.setTimeout(() => setStatus("idle"), 3400);
  }

  return (
    <button
      type="button"
      onClick={handleClick}
      disabled={status === "loading"}
      aria-live="polite"
      className={`bas-group group relative inline-flex min-w-[11rem] items-center justify-center overflow-hidden font-semibold text-white shadow-sm transition-all duration-300 active:scale-[0.98] disabled:cursor-wait bg-rose-600 hover:bg-rose-500 focus-visible:ring-rose-500 dark:bg-rose-500 dark:hover:bg-rose-400 ${sizeMap[size]} ${focusRing}`}
    >
      {status === "idle" && (
        <>
          <span className="bas-shift inline-block transition-transform duration-300 ease-out group-hover:-translate-x-1.5">
            Deploy to production
          </span>
          <span className="pointer-events-none absolute right-4 flex translate-x-6 items-center opacity-0 transition-all duration-300 ease-out group-hover:translate-x-0 group-hover:opacity-100">
            <ArrowGlyph className={arrowSizeMap[size]} />
          </span>
        </>
      )}
      {status === "loading" && (
        <span className="inline-flex items-center gap-2.5">
          <Spinner className={arrowSizeMap[size]} />
          Deploying…
        </span>
      )}
      {status === "done" && (
        <span className="inline-flex items-center gap-2.5">
          <svg
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth={2.5}
            strokeLinecap="round"
            strokeLinejoin="round"
            className={arrowSizeMap[size]}
            aria-hidden="true"
          >
            <path d="M20 6 9 17l-5-5" />
          </svg>
          Deployed
        </span>
      )}
    </button>
  );
}

/* ── Variant 7: toggle follow — aria-pressed, arrow ↔ check ── */
function ToggleArrow({ size = "md" }: { size?: Size }) {
  const [on, setOn] = useState(false);

  return (
    <button
      type="button"
      aria-pressed={on}
      onClick={() => setOn((v) => !v)}
      className={`bas-group group inline-flex items-center gap-2 font-semibold transition-all duration-300 active:scale-[0.98] ${sizeMap[size]} ${focusRing} ${
        on
          ? "border border-sky-500 bg-sky-500 text-white hover:bg-sky-600 focus-visible:ring-sky-500 dark:border-sky-400 dark:bg-sky-500 dark:hover:bg-sky-400"
          : "border border-sky-300 bg-transparent text-sky-700 hover:bg-sky-50 focus-visible:ring-sky-500 dark:border-sky-500/50 dark:text-sky-300 dark:hover:bg-sky-500/10"
      }`}
    >
      {on ? (
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth={2.5}
          strokeLinecap="round"
          strokeLinejoin="round"
          className={arrowSizeMap[size]}
          aria-hidden="true"
        >
          <path d="M20 6 9 17l-5-5" />
        </svg>
      ) : (
        <ArrowGlyph
          className={`${arrowSizeMap[size]} transition-transform duration-300 ease-out group-hover:translate-x-1`}
        />
      )}
      {on ? "Subscribed" : "Subscribe"}
    </button>
  );
}

function Row({ label, children }: { label: string; children: ReactNode }) {
  return (
    <div className="flex flex-col gap-4">
      <span className="text-xs font-medium uppercase tracking-widest text-zinc-400 dark:text-zinc-500">
        {label}
      </span>
      <div className="flex flex-wrap items-center gap-4">{children}</div>
    </div>
  );
}

export default function BtnArrowSlide() {
  return (
    <section className="relative w-full bg-white px-6 py-20 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-50 sm:px-10 sm:py-28">
      <style>{`
        @keyframes bas-spin { to { transform: rotate(360deg); } }
        .bas-spin { animation: bas-spin 0.7s linear infinite; }
        @media (prefers-reduced-motion: reduce) {
          .bas-group, .bas-group * { transition-duration: 0.001ms !important; }
          .bas-shift, .bas-enter-r, .bas-enter-l { transition: none !important; transform: none !important; }
          .bas-spin { animation-duration: 1.6s; }
        }
      `}</style>

      <div className="mx-auto flex max-w-3xl flex-col gap-14">
        <header className="flex flex-col gap-3">
          <h2 className="text-2xl font-bold tracking-tight sm:text-3xl">
            Arrow-slide buttons
          </h2>
          <p className="max-w-prose text-[0.9375rem] leading-relaxed text-zinc-500 dark:text-zinc-400">
            Hover, focus, or press each button. The arrow slides in from an edge
            while the label shifts to make room — every one is a real{" "}
            <code className="rounded bg-zinc-100 px-1 py-0.5 text-[0.8125rem] text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
              &lt;button&gt;
            </code>{" "}
            with a focus-visible ring.
          </p>
        </header>

        <Row label="Slide in — right">
          <SlideInRight size="sm" />
          <SlideInRight size="md" />
          <SlideInRight size="lg" />
        </Row>

        <Row label="Slide in — left · outline">
          <SlideInLeft size="sm" />
          <SlideInLeft size="md" />
          <SlideInLeft size="lg" />
        </Row>

        <Row label="Arrow swap">
          <ArrowSwap size="sm" />
          <ArrowSwap size="md" />
          <ArrowSwap size="lg" />
        </Row>

        <Row label="Inline link">
          <LinkArrow size="sm" />
          <LinkArrow size="md" />
          <LinkArrow size="lg" />
        </Row>

        <Row label="Chip reveal">
          <ChipArrow size="sm" />
          <ChipArrow size="md" />
          <ChipArrow size="lg" />
        </Row>

        <Row label="Async submit — click me">
          <AsyncArrow size="md" />
          <AsyncArrow size="lg" />
        </Row>

        <Row label="Toggle — aria-pressed">
          <ToggleArrow size="sm" />
          <ToggleArrow size="md" />
          <ToggleArrow size="lg" />
        </Row>
      </div>
    </section>
  );
}

Dependencies

None (React + Tailwind only).

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →