Hand Drawn Squiggle Arrows Pointing at a Call to Action

A bright call to action section where the button is annotated like a page someone has scribbled on. Three squiggly arrows in different shapes (wavy, bouncy and smooth) draw themselves stroke by stroke toward the button, each beside a short note in italic serif type, then keep a slight wobble as if drawn by hand. They redraw every few seconds. On desktop two arrows come in from the sides and one from below. On phones the side arrows are swapped for one pointing down from above the button, so nothing overflows.

Code

// Adapted from Cult UI (https://www.cult-ui.com/docs/components/squiggle-arrow)
// Copyright (c) 2023 Jordan-Gilliam. MIT License: https://we.inc/components/licenses
import { useEffect, useState } from "react";
import { motion, useReducedMotion } from "framer-motion";

/** Three hand-drawn shapes. Each has a body and a two-stroke head, drawn pointing right in a 200 by 100 box. */
const shapes = {
  wavy: {
    body: "M 15 50 Q 35 35, 55 48 T 95 52 Q 115 48, 135 50 Q 145 52, 155 48",
    head: "M 155 48 Q 147 44, 143 42 M 155 48 Q 148 53, 144 56",
  },
  bouncy: {
    body: "M 15 50 Q 45 32, 65 50 Q 85 68, 105 50 Q 125 32, 145 50 Q 152 54, 158 50",
    head: "M 158 50 Q 149 45, 145 43 M 158 50 Q 150 56, 146 59",
  },
  smooth: {
    body: "M 15 50 Q 60 38, 100 48 Q 135 56, 158 50",
    head: "M 158 50 Q 149 45, 145 43 M 158 50 Q 150 56, 146 59",
  },
};

const turns = { right: "rotate(0)", left: "rotate(180 100 50)", down: "rotate(90 100 50)", up: "rotate(-90 100 50)" };

const REDRAW_MS = 7000; // the arrows draw themselves again this often

function SquiggleArrow({ shape, direction, delay = 0, className = "", width = 150 }: { shape: keyof typeof shapes; direction: keyof typeof turns; delay?: number; className?: string; width?: number }) {
  const reduce = useReducedMotion();
  const s = shapes[shape];
  const upright = direction === "up" || direction === "down";
  const draw = (d: number, length: number) =>
    reduce ? { initial: false as const } : { initial: { pathLength: 0, opacity: 0 }, animate: { pathLength: 1, opacity: 1 }, transition: { pathLength: { delay: d, duration: length, ease: [0.65, 0, 0.35, 1] as const }, opacity: { delay: d, duration: 0.01 } } };

  return (
    <svg
      aria-hidden="true"
      viewBox={upright ? "68 -45 64 166" : "5 26 165 48"}
      width={upright ? width * 0.4 : width}
      height={upright ? width * 1.04 : width * 0.29}
      fill="none"
      className={`squiggle-arrows-wobble shrink-0 ${className}`}
      style={{ animationDelay: `${delay}s` }}
    >
      <g transform={turns[direction]} stroke="currentColor" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round">
        <motion.path d={s.body} {...draw(delay, 0.7)} />
        <motion.path d={s.head} {...draw(delay + 0.65, 0.25)} />
      </g>
    </svg>
  );
}

function Note({ children, tilt, className = "" }: { children: string; tilt: number; className?: string }) {
  return (
    <p className={`font-serif text-lg italic leading-tight text-stone-800 ${className}`} style={{ transform: `rotate(${tilt}deg)` }}>
      {children}
    </p>
  );
}

export default function HandDrawnSquiggleArrowsPointingAtACallToAction() {
  const reduce = useReducedMotion();
  const [round, setRound] = useState(0);

  useEffect(() => {
    if (reduce) return;
    const t = window.setInterval(() => setRound((r) => r + 1), REDRAW_MS);
    return () => window.clearInterval(t);
  }, [reduce]);

  return (
    <section className="relative overflow-hidden bg-[#FFE45C] px-4 py-16 sm:px-6 sm:py-20">
      <style>{`
        @keyframes squiggle-arrows-wobble-kf { 0%, 100% { transform: rotate(-2deg) translateY(0); } 50% { transform: rotate(2.5deg) translateY(-2px); } }
        .squiggle-arrows-wobble { animation: squiggle-arrows-wobble-kf 2.6s ease-in-out infinite; transform-origin: center; }
        @media (prefers-reduced-motion: reduce) { .squiggle-arrows-wobble { animation: none; } }
      `}</style>

      <div className="mx-auto max-w-4xl text-center">
        <p className="text-sm font-semibold uppercase tracking-[0.18em] text-stone-800">Plot &amp; Pantry veg boxes</p>
        <h2 className="mx-auto mt-4 max-w-2xl text-4xl font-black tracking-tight text-stone-950 sm:text-6xl">Dinner, dug up this morning.</h2>
        <p className="mx-auto mt-4 max-w-lg text-lg text-stone-800">Eight kinds of seasonal veg from farms within forty miles, on your doorstep every Thursday.</p>

        <div key={round} className="mt-10 flex flex-col items-center">
          {/* phones: one note above the button */}
          <div className="mb-2 flex flex-col items-center gap-1 md:hidden">
            <Note tilt={-3}>first box is half price</Note>
            <SquiggleArrow shape="bouncy" direction="down" width={110} className="text-stone-950" />
          </div>

          <div className="flex items-center justify-center gap-3">
            <div className="hidden items-center gap-2 md:flex">
              <Note tilt={-5} className="w-36 text-right">
                first box is half price
              </Note>
              <SquiggleArrow shape="wavy" direction="right" className="text-stone-950" />
            </div>

            <a
              href="#"
              className="inline-flex h-16 items-center rounded-full bg-stone-950 px-9 text-lg font-bold text-[#FFE45C] shadow-[0_6px_0_0_rgba(28,25,23,0.25)] outline-none transition-transform hover:-translate-y-0.5 focus-visible:ring-4 focus-visible:ring-stone-950/40 focus-visible:ring-offset-4 focus-visible:ring-offset-[#FFE45C] motion-reduce:transition-none"
            >
              Build my box
            </a>

            <div className="hidden items-center gap-2 md:flex">
              <SquiggleArrow shape="bouncy" direction="left" delay={0.35} className="text-rose-600" />
              <Note tilt={4} className="w-36 text-left">
                skip a week whenever you like
              </Note>
            </div>
          </div>

          <div className="mt-3 flex flex-col items-center gap-1">
            <SquiggleArrow shape="smooth" direction="up" delay={0.7} width={110} className="text-emerald-700" />
            <Note tilt={2}>takes about two minutes</Note>
          </div>
        </div>

        <p className="mt-8 text-sm font-medium text-stone-800">From $24 a week. No contract.</p>
      </div>
    </section>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/hand-drawn-squiggle-arrows-pointing-at-a-call-to-action.json

When to use it

How to customize it

Accessibility

Questions

How do the arrows draw themselves?

Each stroke is a `motion.path` whose `pathLength` animates from 0 to 1. Framer Motion converts that into the dash offset needed to reveal the line from start to end. The head starts just as the body finishes.

How is one path used for four directions?

The path is always drawn pointing right. A `transform` on the SVG group rotates it around the centre of the drawing by 90, 180 or 270 degrees, and the `viewBox` is cropped to suit a tall or a wide arrow.

Can I use the arrows on their own?

Yes. Copy `SquiggleArrow` with the `shapes` and `turns` objects. It has no dependency on the rest of the section.

How do I use a real handwriting font for the notes?

Load the font in your site and replace `font-serif italic` in `Note` with its class. The demo uses italic serif so it works without loading anything.

Why does it redraw?

A repeat gives the eye a second chance to notice the button. Set `REDRAW_MS` very high, or delete the effect, to draw once only.

Adapted from Squiggle Arrow in Cult UI, used under its open-source licence. Licences and credits

Related: Call-to-Action Banner on a Brand Colour, Editorial Hero with Sliding Letters and Drawn Underline, Marker Highlight Text That Sweeps Across Phrases.

More call-to-action sections or all free UI components. Start for free to have a full site built around it.

More in Cta Sections

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap