Open demo

CSS code

HTML
<div class="ca"><span class="lbl">Hotspots on a product photo</span>
<figure class="ca-photo">
  <img src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=1200&h=800&q=70" width="1200" height="800" alt="Black over-ear headphones lying on a bright yellow background">
  <div class="ca-layer" aria-hidden="true">
    <span class="ca-dot" style="--x:66%;--y:17%">1</span>
    <span class="ca-dot" style="--x:53%;--y:60%">2</span>
    <span class="ca-dot" style="--x:71%;--y:73%">3</span>
  </div>
  <ol class="ca-layer notes" aria-label="Product details">
    <li class="ca-pin" style="--x:66%;--y:17%"><p class="ca-tip ca-l"><span class="ca-num" aria-hidden="true">1</span>Padded headband. 248 g in total.</p></li>
    <li class="ca-pin" style="--x:53%;--y:60%"><p class="ca-tip ca-l"><span class="ca-num" aria-hidden="true">2</span>Memory-foam cushions that swap out in seconds.</p></li>
    <li class="ca-pin" style="--x:71%;--y:73%"><p class="ca-tip ca-b"><span class="ca-num" aria-hidden="true">3</span>Power, pairing and USB‑C on the right cup.</p></li>
  </ol>
  <figcaption>Tessel Studio 2 wireless headphones, 32 hours per charge.</figcaption>
</figure></div>

<div class="ca"><span class="lbl">Callout pointing at a UI element</span>
<div class="ca-app">
  <div class="ca-bar" role="group" aria-label="Board tools">
    <button class="ca-btn" type="button" aria-label="Bold">B</button>
    <button class="ca-btn" type="button" aria-label="Italic"><i>I</i></button>
    <button class="ca-btn" type="button" aria-label="Add link"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1"/><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1"/></svg></button>
    <span class="ca-sep" aria-hidden="true"></span>
    <span class="ca-anchor">
      <button class="ca-btn export" type="button" aria-describedby="ca-new"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15V4M7.5 8.5 12 4l4.5 4.5M5 14v5h14v-5"/></svg>Export</button>
      <div class="ca-tip ca-pop" id="ca-new" role="note">
        <strong>New: export to PDF</strong>
        <p>Send the whole board as a print-ready PDF, with or without comments.</p>
        <a href="#">See how it works</a>
      </div>
    </span>
  </div>
  <div class="ca-doc" aria-hidden="true"><i></i><i></i><i></i></div>
</div></div>

<div class="ca"><span class="lbl">Annotations on a chart</span>
<figure class="ca-chart">
  <figcaption>Orders per month, 2025</figcaption>
  <div class="ca-plot" role="img" aria-label="Bar chart of orders per month in 2025. Lowest: February, 410 orders, during a two-day site outage. Highest: November, 1,920 orders, in Black Friday week.">
    <span class="ca-col" style="--h:34%"></span>
    <span class="ca-col lo" style="--h:21%"><span class="ca-pin"><span class="ca-tip ca-t start">Site outage, 2 days: 410</span></span></span>
    <span class="ca-col" style="--h:40%"></span>
    <span class="ca-col" style="--h:44%"></span>
    <span class="ca-col" style="--h:49%"></span>
    <span class="ca-col" style="--h:46%"></span>
    <span class="ca-col" style="--h:42%"></span>
    <span class="ca-col" style="--h:47%"></span>
    <span class="ca-col" style="--h:55%"></span>
    <span class="ca-col" style="--h:63%"></span>
    <span class="ca-col hi" style="--h:100%"><span class="ca-pin"><span class="ca-tip ca-l">Black Friday week: 1,920</span></span></span>
    <span class="ca-col" style="--h:72%"></span>
  </div>
  <ol class="ca-months" aria-hidden="true"><li>Jan</li><li>Feb</li><li>Mar</li><li>Apr</li><li>May</li><li>Jun</li><li>Jul</li><li>Aug</li><li>Sep</li><li>Oct</li><li>Nov</li><li>Dec</li></ol>
</figure></div>
CSS
/* Callouts and annotation bubbles. A .ca-pin is a zero-size point placed with --x and --y; the bubble is positioned
   from that point and its border-triangle tail always lands on it. .ca-l / .ca-r / .ca-t / .ca-b say where the bubble sits. */
.ca{--ca-bg:#18213a;--ca-ink:#fff;--ca-accent:#e11d48;--ca-t:8px;--ca-gap:25px;--ca-muted:#565d70}
.ca .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.ca-tip{position:absolute;z-index:1;width:max-content;max-width:210px;margin:0;padding:9px 13px;border-radius:10px;background:var(--ca-bg);color:var(--ca-ink);
  font-size:.84rem;line-height:1.4;box-shadow:0 10px 24px -10px rgba(24,33,58,.55)}
.ca-tip::after{content:"";position:absolute;border:var(--ca-t) solid transparent}
.ca-l{right:var(--ca-gap);top:0;translate:0 -50%}
.ca-l::after{left:100%;top:50%;margin-top:calc(var(--ca-t) * -1);border-right-width:0;border-left-color:var(--ca-bg)}
.ca-r{left:var(--ca-gap);top:0;translate:0 -50%}
.ca-r::after{right:100%;top:50%;margin-top:calc(var(--ca-t) * -1);border-left-width:0;border-right-color:var(--ca-bg)}
.ca-b{top:var(--ca-gap);left:0;translate:-50% 0}
.ca-b::after{bottom:100%;left:50%;margin-left:calc(var(--ca-t) * -1);border-top-width:0;border-bottom-color:var(--ca-bg)}
.ca-t{bottom:var(--ca-gap);left:0;translate:-50% 0}
.ca-t::after{top:100%;left:50%;margin-left:calc(var(--ca-t) * -1);border-bottom-width:0;border-top-color:var(--ca-bg)}
.ca-num{display:inline-grid;place-items:center;width:18px;height:18px;margin-right:6px;border-radius:50%;background:var(--ca-accent);color:#fff;font-size:.7rem;font-weight:700;vertical-align:1px}

/* 1. Photo hotspots: numbered dots over the image, notes in a list laid over the same box */
.ca-photo{position:relative;margin:0}
.ca-photo img{display:block;width:100%;height:auto;aspect-ratio:3/2;object-fit:cover;border-radius:16px}
.ca-layer{position:absolute;top:0;left:0;width:100%;aspect-ratio:3/2;margin:0;padding:0;list-style:none}
.ca-pin{position:absolute;left:var(--x);top:var(--y);width:0;height:0}
.ca-dot{position:absolute;left:var(--x);top:var(--y);display:grid;place-items:center;width:26px;height:26px;translate:-50% -50%;border-radius:50%;
  background:var(--ca-accent);color:#fff;font-size:.76rem;font-weight:700;box-shadow:0 0 0 4px rgba(255,255,255,.75),0 4px 10px rgba(24,33,58,.4)}
.ca-photo figcaption{margin-top:12px;font-size:.82rem;color:var(--ca-muted)}
/* Phones: the notes leave the photo and become a numbered list under it */
@media (max-width:640px){
  .ca-photo .ca-layer.notes{position:static;aspect-ratio:auto;display:grid;gap:8px;margin-top:14px}
  .ca-photo .ca-pin{position:static;width:auto;height:auto}
  .ca-photo .ca-tip{position:static;translate:none;width:auto;max-width:none}
  .ca-photo .ca-tip::after{display:none}
}

/* 2. Callout on a UI element: the bubble hangs under the button it introduces */
.ca-app{border:1px solid #dfe2ec;border-radius:16px;background:#fff;box-shadow:0 14px 34px -24px rgba(24,33,58,.45)}
.ca-bar{display:flex;align-items:center;gap:4px;padding:10px 12px;border-bottom:1px solid #e7e9f1}
.ca-btn{display:inline-grid;place-items:center;min-width:36px;height:36px;padding:0 8px;border:0;border-radius:8px;background:none;color:#2d3550;font:inherit;font-size:.9rem;font-weight:700;cursor:pointer}
.ca-btn:hover{background:#f0f2f8}
.ca-sep{width:1px;height:22px;margin:0 6px;background:#dfe2ec}
.ca-anchor{position:relative;margin-left:auto}
.ca-btn.export{display:inline-flex;gap:6px;padding:0 14px;background:#ede9fe;color:#4c1d95}
.ca-btn.export:hover{background:#ddd6fe}
.ca-pop{--ca-bg:#4c1d95;top:calc(100% + 14px);right:0;width:250px;max-width:calc(100vw - 64px);padding:14px 16px;font-size:.86rem}
.ca-pop::after{bottom:100%;right:38px;border-top-width:0;border-bottom-color:var(--ca-bg)}
.ca-pop strong{display:block;margin-bottom:3px;font-size:.92rem}
.ca-pop p{margin:0 0 8px}
.ca-pop a{color:#fff;font-weight:600;text-underline-offset:3px}
.ca-doc{display:grid;gap:12px;padding:150px 24px 28px}
.ca-doc i{display:block;height:10px;border-radius:5px;background:#eceef5}
.ca-doc i:nth-child(2){width:86%}.ca-doc i:nth-child(3){width:64%}
.ca-btn:focus-visible{outline:2px solid #4c1d95;outline-offset:2px}
.ca-pop a:focus-visible{outline:2px solid #fff;outline-offset:2px;border-radius:2px}

/* 3. Chart annotation: bubbles pinned to the top of a bar */
.ca-chart{margin:0;padding:22px 22px 16px;border:1px solid #dfe2ec;border-radius:16px;background:#fff}
.ca-chart figcaption{margin-bottom:18px;font-size:.9rem;font-weight:700}
.ca-plot{display:flex;align-items:flex-end;gap:6px;height:220px;padding-top:70px;border-bottom:1px solid #cfd3df}
.ca-col{position:relative;flex:1;height:var(--h);border-radius:6px 6px 0 0;background:#c7cbe0}
.ca-col.hi{background:var(--ca-accent)}
.ca-col.lo{background:#7c86a8}
.ca-col .ca-pin{left:50%;top:0}
.ca-months{display:flex;gap:6px;margin:6px 0 0;padding:0;list-style:none}
.ca-months li{flex:1;text-align:center;font-size:.7rem;color:var(--ca-muted)}
.ca-chart .ca-tip{max-width:170px;font-size:.78rem}
.ca-t.start{translate:-20px 0}
.ca-t.start::after{left:20px}
@media (max-width:640px){.ca-plot,.ca-months{gap:3px}.ca-months li{font-size:0}.ca-months li::first-letter{font-size:.7rem}.ca-chart{padding:18px 14px 14px}.ca-chart .ca-tip{max-width:150px}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="ca"><span class="lbl">Hotspots on a product photo</span>
<figure class="ca-photo figure d-block position-relative mb-0">
  <img class="figure-img img-fluid rounded-4 mb-0" src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=1200&h=800&q=70" width="1200" height="800" alt="Black over-ear headphones lying on a bright yellow background">
  <div class="ca-layer" aria-hidden="true">
    <span class="ca-dot" style="--x:66%;--y:17%">1</span>
    <span class="ca-dot" style="--x:53%;--y:60%">2</span>
    <span class="ca-dot" style="--x:71%;--y:73%">3</span>
  </div>
  <ol class="ca-layer notes list-unstyled" aria-label="Product details">
    <li class="ca-pin" style="--x:66%;--y:17%"><p class="ca-tip ca-l"><span class="ca-num" aria-hidden="true">1</span>Padded headband. 248 g in total.</p></li>
    <li class="ca-pin" style="--x:53%;--y:60%"><p class="ca-tip ca-l"><span class="ca-num" aria-hidden="true">2</span>Memory-foam cushions that swap out in seconds.</p></li>
    <li class="ca-pin" style="--x:71%;--y:73%"><p class="ca-tip ca-b"><span class="ca-num" aria-hidden="true">3</span>Power, pairing and USB‑C on the right cup.</p></li>
  </ol>
  <figcaption class="figure-caption mt-2">Tessel Studio 2 wireless headphones, 32 hours per charge.</figcaption>
</figure></div>

<div class="ca"><span class="lbl">Callout pointing at a UI element</span>
<div class="ca-app card">
  <div class="ca-bar card-header bg-transparent d-flex align-items-center gap-1" role="group" aria-label="Board tools">
    <button class="btn ca-btn" type="button" aria-label="Bold">B</button>
    <button class="btn ca-btn" type="button" aria-label="Italic"><i>I</i></button>
    <button class="btn ca-btn" type="button" aria-label="Add link"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1"/><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1"/></svg></button>
    <span class="ca-sep" aria-hidden="true"></span>
    <span class="ca-anchor position-relative ms-auto">
      <button class="btn ca-btn export" type="button" aria-describedby="ca-new"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15V4M7.5 8.5 12 4l4.5 4.5M5 14v5h14v-5"/></svg>Export</button>
      <div class="ca-tip ca-pop" id="ca-new" role="note">
        <strong class="d-block mb-1">New: export to PDF</strong>
        <p>Send the whole board as a print-ready PDF, with or without comments.</p>
        <a class="link-light link-offset-1 fw-semibold" href="#">See how it works</a>
      </div>
    </span>
  </div>
  <div class="ca-doc" aria-hidden="true"><i></i><i></i><i></i></div>
</div></div>

<div class="ca"><span class="lbl">Annotations on a chart</span>
<figure class="ca-chart card card-body mb-0">
  <figcaption class="fw-bold mb-3">Orders per month, 2025</figcaption>
  <div class="ca-plot" role="img" aria-label="Bar chart of orders per month in 2025. Lowest: February, 410 orders, during a two-day site outage. Highest: November, 1,920 orders, in Black Friday week.">
    <span class="ca-col" style="--h:34%"></span>
    <span class="ca-col lo" style="--h:21%"><span class="ca-pin"><span class="ca-tip ca-t start">Site outage, 2 days: 410</span></span></span>
    <span class="ca-col" style="--h:40%"></span>
    <span class="ca-col" style="--h:44%"></span>
    <span class="ca-col" style="--h:49%"></span>
    <span class="ca-col" style="--h:46%"></span>
    <span class="ca-col" style="--h:42%"></span>
    <span class="ca-col" style="--h:47%"></span>
    <span class="ca-col" style="--h:55%"></span>
    <span class="ca-col" style="--h:63%"></span>
    <span class="ca-col hi" style="--h:100%"><span class="ca-pin"><span class="ca-tip ca-l">Black Friday week: 1,920</span></span></span>
    <span class="ca-col" style="--h:72%"></span>
  </div>
  <ol class="ca-months list-unstyled" aria-hidden="true"><li>Jan</li><li>Feb</li><li>Mar</li><li>Apr</li><li>May</li><li>Jun</li><li>Jul</li><li>Aug</li><li>Sep</li><li>Oct</li><li>Nov</li><li>Dec</li></ol>
</figure></div>
CSS
/* Callouts and annotation bubbles on Bootstrap (.figure, .card, .btn and utilities around them). A .ca-pin is a zero-size point placed with --x and --y; the bubble is positioned
   from that point and its border-triangle tail always lands on it. .ca-l / .ca-r / .ca-t / .ca-b say where the bubble sits. */
.ca{--ca-bg:#18213a;--ca-ink:#fff;--ca-accent:#e11d48;--ca-t:8px;--ca-gap:25px;--ca-muted:#565d70}
.ca-tip{position:absolute;z-index:1;width:max-content;max-width:210px;margin:0;padding:9px 13px;border-radius:10px;background:var(--ca-bg);color:var(--ca-ink);
  font-size:.84rem;line-height:1.4;box-shadow:0 10px 24px -10px rgba(24,33,58,.55)}
.ca-tip::after{content:"";position:absolute;border:var(--ca-t) solid transparent}
.ca-l{right:var(--ca-gap);top:0;translate:0 -50%}
.ca-l::after{left:100%;top:50%;margin-top:calc(var(--ca-t) * -1);border-right-width:0;border-left-color:var(--ca-bg)}
.ca-r{left:var(--ca-gap);top:0;translate:0 -50%}
.ca-r::after{right:100%;top:50%;margin-top:calc(var(--ca-t) * -1);border-left-width:0;border-right-color:var(--ca-bg)}
.ca-b{top:var(--ca-gap);left:0;translate:-50% 0}
.ca-b::after{bottom:100%;left:50%;margin-left:calc(var(--ca-t) * -1);border-top-width:0;border-bottom-color:var(--ca-bg)}
.ca-t{bottom:var(--ca-gap);left:0;translate:-50% 0}
.ca-t::after{top:100%;left:50%;margin-left:calc(var(--ca-t) * -1);border-bottom-width:0;border-top-color:var(--ca-bg)}
.ca-num{display:inline-grid;place-items:center;width:18px;height:18px;margin-right:6px;border-radius:50%;background:var(--ca-accent);color:#fff;font-size:.7rem;font-weight:700;vertical-align:1px}

/* 1. Photo hotspots: numbered dots over the image, notes in a list laid over the same box */
.ca-photo{position:relative;margin:0}
.ca-photo img{display:block;width:100%;height:auto;aspect-ratio:3/2;object-fit:cover;border-radius:16px}
.ca-layer{position:absolute;top:0;left:0;width:100%;aspect-ratio:3/2;margin:0;padding:0;list-style:none}
.ca-pin{position:absolute;left:var(--x);top:var(--y);width:0;height:0}
.ca-dot{position:absolute;left:var(--x);top:var(--y);display:grid;place-items:center;width:26px;height:26px;translate:-50% -50%;border-radius:50%;
  background:var(--ca-accent);color:#fff;font-size:.76rem;font-weight:700;box-shadow:0 0 0 4px rgba(255,255,255,.75),0 4px 10px rgba(24,33,58,.4)}
.ca-photo figcaption{margin-top:12px;font-size:.82rem;color:var(--ca-muted)}
/* Phones: the notes leave the photo and become a numbered list under it */
@media (max-width:640px){
  .ca-photo .ca-layer.notes{position:static;aspect-ratio:auto;display:grid;gap:8px;margin-top:14px}
  .ca-photo .ca-pin{position:static;width:auto;height:auto}
  .ca-photo .ca-tip{position:static;translate:none;width:auto;max-width:none}
  .ca-photo .ca-tip::after{display:none}
}

/* 2. Callout on a UI element: the bubble hangs under the button it introduces */
.ca-app{--bs-card-border-color:#dfe2ec;--bs-card-border-radius:16px;--bs-card-cap-padding-y:10px;--bs-card-cap-padding-x:12px;box-shadow:0 14px 34px -24px rgba(24,33,58,.45)}
.ca-bar{display:flex;align-items:center;gap:4px;padding:10px 12px;border-bottom:1px solid #e7e9f1}
.ca-btn{display:inline-grid;place-items:center;min-width:36px;height:36px;padding:0 8px;border:0;border-radius:8px;background:none;color:#2d3550;font:inherit;font-size:.9rem;font-weight:700;cursor:pointer}
.ca-btn:hover{background:#f0f2f8}
.ca-sep{width:1px;height:22px;margin:0 6px;background:#dfe2ec}
.ca-anchor{position:relative;margin-left:auto}
.ca-btn.export{display:inline-flex;gap:6px;padding:0 14px;background:#ede9fe;color:#4c1d95}
.ca-btn.export:hover{background:#ddd6fe}
.ca-pop{--ca-bg:#4c1d95;top:calc(100% + 14px);right:0;width:250px;max-width:calc(100vw - 64px);padding:14px 16px;font-size:.86rem}
.ca-pop::after{bottom:100%;right:38px;border-top-width:0;border-bottom-color:var(--ca-bg)}
.ca-pop strong{display:block;margin-bottom:3px;font-size:.92rem}
.ca-pop p{margin:0 0 8px}
.ca-doc{display:grid;gap:12px;padding:150px 24px 28px}
.ca-doc i{display:block;height:10px;border-radius:5px;background:#eceef5}
.ca-doc i:nth-child(2){width:86%}.ca-doc i:nth-child(3){width:64%}
.ca-btn:focus-visible{outline:2px solid #4c1d95;outline-offset:2px}
.ca-pop a:focus-visible{outline:2px solid #fff;outline-offset:2px;border-radius:2px}

/* 3. Chart annotation: bubbles pinned to the top of a bar */
.ca-chart{--bs-card-border-color:#dfe2ec;--bs-card-border-radius:16px;padding:22px 22px 16px}
.ca-chart figcaption{margin-bottom:18px;font-size:.9rem;font-weight:700}
.ca-plot{display:flex;align-items:flex-end;gap:6px;height:220px;padding-top:70px;border-bottom:1px solid #cfd3df}
.ca-col{position:relative;flex:1;height:var(--h);border-radius:6px 6px 0 0;background:#c7cbe0}
.ca-col.hi{background:var(--ca-accent)}
.ca-col.lo{background:#7c86a8}
.ca-col .ca-pin{left:50%;top:0}
.ca-months{display:flex;gap:6px;margin:6px 0 0;padding:0;list-style:none}
.ca-months li{flex:1;text-align:center;font-size:.7rem;color:var(--ca-muted)}
.ca-chart .ca-tip{max-width:170px;font-size:.78rem}
.ca-t.start{translate:-20px 0}
.ca-t.start::after{left:20px}
@media (max-width:640px){.ca-plot,.ca-months{gap:3px}.ca-months li{font-size:0}.ca-months li::first-letter{font-size:.7rem}.ca-chart{padding:18px 14px 14px}.ca-chart .ca-tip{max-width:150px}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-ca-muted uppercase">Hotspots on a product photo</span>
<figure class="relative m-0">
  <img class="block aspect-[3/2] h-auto w-full rounded-2xl object-cover" src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=1200&h=800&q=70" width="1200" height="800" alt="Black over-ear headphones lying on a bright yellow background">
  <div class="absolute top-0 left-0 aspect-[3/2] w-full" aria-hidden="true">
    <span class="absolute top-(--y) left-(--x) grid size-[26px] -translate-1/2 place-items-center rounded-full bg-ca-accent text-[.76rem] font-bold text-white shadow-[0_0_0_4px_rgba(255,255,255,.75),0_4px_10px_rgba(24,33,58,.4)] [--x:66%] [--y:17%]">1</span>
    <span class="absolute top-(--y) left-(--x) grid size-[26px] -translate-1/2 place-items-center rounded-full bg-ca-accent text-[.76rem] font-bold text-white shadow-[0_0_0_4px_rgba(255,255,255,.75),0_4px_10px_rgba(24,33,58,.4)] [--x:53%] [--y:60%]">2</span>
    <span class="absolute top-(--y) left-(--x) grid size-[26px] -translate-1/2 place-items-center rounded-full bg-ca-accent text-[.76rem] font-bold text-white shadow-[0_0_0_4px_rgba(255,255,255,.75),0_4px_10px_rgba(24,33,58,.4)] [--x:71%] [--y:73%]">3</span>
  </div>
  <ol class="absolute top-0 left-0 m-0 aspect-[3/2] w-full list-none p-0 max-sm:static max-sm:mt-3.5 max-sm:grid max-sm:aspect-auto max-sm:gap-2" aria-label="Product details">
    <li class="absolute top-(--y) left-(--x) size-0 max-sm:static max-sm:size-auto [--x:66%] [--y:17%]"><p class="ca-tip ca-l z-[1] m-0 w-max max-w-[210px] rounded-[10px] bg-(--ca-bg) px-[13px] py-[9px] text-[.84rem] leading-[1.4] text-white shadow-[0_10px_24px_-10px_rgba(24,33,58,.55)] max-sm:static! max-sm:w-auto max-sm:max-w-none max-sm:translate-none! max-sm:after:hidden"><span class="mr-1.5 inline-grid size-[18px] place-items-center rounded-full bg-ca-accent align-[1px] text-[.7rem] font-bold text-white" aria-hidden="true">1</span>Padded headband. 248 g in total.</p></li>
    <li class="absolute top-(--y) left-(--x) size-0 max-sm:static max-sm:size-auto [--x:53%] [--y:60%]"><p class="ca-tip ca-l z-[1] m-0 w-max max-w-[210px] rounded-[10px] bg-(--ca-bg) px-[13px] py-[9px] text-[.84rem] leading-[1.4] text-white shadow-[0_10px_24px_-10px_rgba(24,33,58,.55)] max-sm:static! max-sm:w-auto max-sm:max-w-none max-sm:translate-none! max-sm:after:hidden"><span class="mr-1.5 inline-grid size-[18px] place-items-center rounded-full bg-ca-accent align-[1px] text-[.7rem] font-bold text-white" aria-hidden="true">2</span>Memory-foam cushions that swap out in seconds.</p></li>
    <li class="absolute top-(--y) left-(--x) size-0 max-sm:static max-sm:size-auto [--x:71%] [--y:73%]"><p class="ca-tip ca-b z-[1] m-0 w-max max-w-[210px] rounded-[10px] bg-(--ca-bg) px-[13px] py-[9px] text-[.84rem] leading-[1.4] text-white shadow-[0_10px_24px_-10px_rgba(24,33,58,.55)] max-sm:static! max-sm:w-auto max-sm:max-w-none max-sm:translate-none! max-sm:after:hidden"><span class="mr-1.5 inline-grid size-[18px] place-items-center rounded-full bg-ca-accent align-[1px] text-[.7rem] font-bold text-white" aria-hidden="true">3</span>Power, pairing and USB‑C on the right cup.</p></li>
  </ol>
  <figcaption class="mt-3 text-[.82rem] text-ca-muted">Tessel Studio 2 wireless headphones, 32 hours per charge.</figcaption>
</figure></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-ca-muted uppercase">Callout pointing at a UI element</span>
<div class="rounded-2xl border border-[#dfe2ec] bg-white shadow-[0_14px_34px_-24px_rgba(24,33,58,.45)]">
  <div class="flex items-center gap-1 border-b border-[#e7e9f1] px-3 py-2.5" role="group" aria-label="Board tools">
    <button class="inline-grid h-9 min-w-9 cursor-pointer place-items-center rounded-lg px-2 text-[.9rem] font-bold text-[#2d3550] hover:bg-[#f0f2f8] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4c1d95]" type="button" aria-label="Bold">B</button>
    <button class="inline-grid h-9 min-w-9 cursor-pointer place-items-center rounded-lg px-2 text-[.9rem] font-bold text-[#2d3550] hover:bg-[#f0f2f8] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4c1d95]" type="button" aria-label="Italic"><i>I</i></button>
    <button class="inline-grid h-9 min-w-9 cursor-pointer place-items-center rounded-lg px-2 text-[.9rem] font-bold text-[#2d3550] hover:bg-[#f0f2f8] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4c1d95]" type="button" aria-label="Add link"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1"/><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1"/></svg></button>
    <span class="mx-1.5 h-[22px] w-px bg-[#dfe2ec]" aria-hidden="true"></span>
    <span class="relative ml-auto">
      <button class="inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-lg bg-[#ede9fe] px-3.5 text-[.9rem] font-bold text-[#4c1d95] hover:bg-[#ddd6fe] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4c1d95]" type="button" aria-describedby="ca-new"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15V4M7.5 8.5 12 4l4.5 4.5M5 14v5h14v-5"/></svg>Export</button>
      <div class="ca-tip ca-pop absolute top-[calc(100%+14px)] right-0 z-[1] w-[250px] max-w-[calc(100vw-64px)] rounded-[10px] bg-(--ca-bg) px-4 py-3.5 text-[.86rem] leading-[1.4] text-white shadow-[0_10px_24px_-10px_rgba(24,33,58,.55)] [--ca-bg:#4c1d95]" id="ca-new" role="note">
        <strong class="mb-[3px] block text-[.92rem]">New: export to PDF</strong>
        <p class="mb-2">Send the whole board as a print-ready PDF, with or without comments.</p>
        <a class="font-semibold text-white underline underline-offset-3 focus-visible:rounded-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white" href="#">See how it works</a>
      </div>
    </span>
  </div>
  <div class="grid gap-3 px-6 pt-[150px] pb-7" aria-hidden="true"><i class="block h-2.5 rounded-[5px] bg-[#eceef5]"></i><i class="block h-2.5 w-[86%] rounded-[5px] bg-[#eceef5]"></i><i class="block h-2.5 w-[64%] rounded-[5px] bg-[#eceef5]"></i></div>
</div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-ca-muted uppercase">Annotations on a chart</span>
<figure class="m-0 rounded-2xl border border-[#dfe2ec] bg-white px-[22px] pt-[22px] pb-4 max-sm:px-3.5 max-sm:pt-[18px] max-sm:pb-3.5">
  <figcaption class="mb-[18px] text-[.9rem] font-bold">Orders per month, 2025</figcaption>
  <div class="flex h-[220px] items-end gap-1.5 border-b border-[#cfd3df] pt-[70px] max-sm:gap-[3px]" role="img" aria-label="Bar chart of orders per month in 2025. Lowest: February, 410 orders, during a two-day site outage. Highest: November, 1,920 orders, in Black Friday week.">
    <span class="h-[34%] flex-1 rounded-t-md bg-[#c7cbe0]"></span>
    <span class="relative h-[21%] flex-1 rounded-t-md bg-[#7c86a8]"><span class="ca-pin absolute top-0 left-1/2"><span class="ca-tip ca-t ca-start z-[1] m-0 w-max max-w-[210px] rounded-[10px] bg-(--ca-bg) px-[13px] py-[9px] text-[.84rem] leading-[1.4] text-white shadow-[0_10px_24px_-10px_rgba(24,33,58,.55)] max-w-[170px] text-[.78rem] max-sm:max-w-[150px]">Site outage, 2 days: 410</span></span></span>
    <span class="h-[40%] flex-1 rounded-t-md bg-[#c7cbe0]"></span>
    <span class="h-[44%] flex-1 rounded-t-md bg-[#c7cbe0]"></span>
    <span class="h-[49%] flex-1 rounded-t-md bg-[#c7cbe0]"></span>
    <span class="h-[46%] flex-1 rounded-t-md bg-[#c7cbe0]"></span>
    <span class="h-[42%] flex-1 rounded-t-md bg-[#c7cbe0]"></span>
    <span class="h-[47%] flex-1 rounded-t-md bg-[#c7cbe0]"></span>
    <span class="h-[55%] flex-1 rounded-t-md bg-[#c7cbe0]"></span>
    <span class="h-[63%] flex-1 rounded-t-md bg-[#c7cbe0]"></span>
    <span class="relative h-full flex-1 rounded-t-md bg-ca-accent"><span class="ca-pin absolute top-0 left-1/2"><span class="ca-tip ca-l z-[1] m-0 w-max max-w-[210px] rounded-[10px] bg-(--ca-bg) px-[13px] py-[9px] text-[.84rem] leading-[1.4] text-white shadow-[0_10px_24px_-10px_rgba(24,33,58,.55)] max-w-[170px] text-[.78rem] max-sm:max-w-[150px]">Black Friday week: 1,920</span></span></span>
    <span class="h-[72%] flex-1 rounded-t-md bg-[#c7cbe0]"></span>
  </div>
  <ol class="m-0 mt-1.5 flex list-none gap-1.5 p-0 max-sm:gap-[3px]" aria-hidden="true"><li class="flex-1 text-center text-[.7rem] text-ca-muted"><span class="max-sm:hidden">Jan</span><span class="sm:hidden">J</span></li><li class="flex-1 text-center text-[.7rem] text-ca-muted"><span class="max-sm:hidden">Feb</span><span class="sm:hidden">F</span></li><li class="flex-1 text-center text-[.7rem] text-ca-muted"><span class="max-sm:hidden">Mar</span><span class="sm:hidden">M</span></li><li class="flex-1 text-center text-[.7rem] text-ca-muted"><span class="max-sm:hidden">Apr</span><span class="sm:hidden">A</span></li><li class="flex-1 text-center text-[.7rem] text-ca-muted"><span class="max-sm:hidden">May</span><span class="sm:hidden">M</span></li><li class="flex-1 text-center text-[.7rem] text-ca-muted"><span class="max-sm:hidden">Jun</span><span class="sm:hidden">J</span></li><li class="flex-1 text-center text-[.7rem] text-ca-muted"><span class="max-sm:hidden">Jul</span><span class="sm:hidden">J</span></li><li class="flex-1 text-center text-[.7rem] text-ca-muted"><span class="max-sm:hidden">Aug</span><span class="sm:hidden">A</span></li><li class="flex-1 text-center text-[.7rem] text-ca-muted"><span class="max-sm:hidden">Sep</span><span class="sm:hidden">S</span></li><li class="flex-1 text-center text-[.7rem] text-ca-muted"><span class="max-sm:hidden">Oct</span><span class="sm:hidden">O</span></li><li class="flex-1 text-center text-[.7rem] text-ca-muted"><span class="max-sm:hidden">Nov</span><span class="sm:hidden">N</span></li><li class="flex-1 text-center text-[.7rem] text-ca-muted"><span class="max-sm:hidden">Dec</span><span class="sm:hidden">D</span></li></ol>
</figure></div>
CSS
@theme {
  --color-ca-ink:#18213a; --color-ca-accent:#e11d48; --color-ca-muted:#565d70;
}
/* Utilities draw the bubbles; placement from the pin and the tails are repeated rules, so they live here.
   A .ca-pin is a zero-size point; .ca-l / .ca-r / .ca-t / .ca-b put the bubble on that side with its tail on the point. */
:root{--ca-bg:var(--color-ca-ink);--ca-t:8px;--ca-gap:25px}
.ca-tip{position:absolute}
.ca-tip::after{content:"";position:absolute;border:var(--ca-t) solid transparent}
.ca-l{right:var(--ca-gap);top:0;translate:0 -50%}
.ca-l::after{left:100%;top:50%;margin-top:calc(var(--ca-t) * -1);border-right-width:0;border-left-color:var(--ca-bg)}
.ca-b{top:var(--ca-gap);left:0;translate:-50% 0}
.ca-b::after{bottom:100%;left:50%;margin-left:calc(var(--ca-t) * -1);border-top-width:0;border-bottom-color:var(--ca-bg)}
.ca-t{bottom:var(--ca-gap);left:0;translate:-50% 0}
.ca-t::after{top:100%;left:50%;margin-left:calc(var(--ca-t) * -1);border-bottom-width:0;border-top-color:var(--ca-bg)}
.ca-t.ca-start{translate:-20px 0}
.ca-t.ca-start::after{left:20px}
.ca-pop::after{bottom:100%;right:38px;border-top-width:0;border-bottom-color:var(--ca-bg)}

About this set

Three kinds of bubble that point at something specific. Hotspots on a product photo puts numbered dots on a picture of headphones and a note beside each one whose tail lands on its dot. Each note is a zero-size pin placed with –x and –y in percent, and a class such as .ca-l or .ca-b says which side of the pin the bubble sits on, so the tail always ends on the point however long the text is. Below 640px the notes leave the photo and become a numbered list under it, because notes laid over a phone-sized photo would hide it. Callout pointing at a UI element hangs a new-feature bubble under a toolbar’s Export button, with a heading, one sentence and a link; the button points to it with aria-describedby. Annotations on a chart pins two bubbles to the tops of bars in a monthly orders chart, one beside the November peak and one above the February dip, and the same facts are in the chart’s text alternative. Every bubble uses the same border-triangle tail, so one block of CSS covers all three.

What’s included

  • Pins placed with --x and --y, with the tail always on the point
  • Notes become a numbered list under the photo on phones
  • New-feature callout linked to its button with aria-describedby
  • Chart annotations pinned to the top of a bar
  • One tail system for all bubbles: .ca-l, .ca-r, .ca-t, .ca-b

Accessibility

The photo notes are an ordered list named Product details, with the dot numbers repeated visibly in each note and the dots themselves aria-hidden. The toolbar is a labelled group whose buttons have accessible names, and the Export button is described by the callout. The chart is one image with a text alternative that includes both annotations, and buttons and the link show a 2px focus outline.

Customise it

Set --ca-bg, --ca-accent and --ca-t on .ca for bubble colour, dot colour and tail size, and --ca-gap for the distance from the pin. Move a hotspot by changing --x and --y on both its dot and its note. In Tailwind edit the --color-ca-* theme colours and the [--x:…] [--y:…] utilities; Bootstrap uses the same variables with .figure and .card.