21 CSS Tooltips01 / 21

Pure CSSMIT licensed

Schematic Hotspot

For product diagrams and exploded views. Four pulsing red dots dot a wireframe drawing; hover any one and a leader line draws itself to a numbered callout box with part name, description, and a tiny spec table. The whole annotation reads like a CAD title block.

Published

Live Demo
Try it

The code

<div class="tp-01">
  <div class="tp-01__paper">
    <span class="tp-01__corner tp-01__tl"></span>
    <span class="tp-01__corner tp-01__tr"></span>
    <span class="tp-01__corner tp-01__bl"></span>
    <span class="tp-01__corner tp-01__br"></span>
    <div class="tp-01__meta">
      <span>DWG-0142-A · CAMERA / EXPLODED VIEW</span>
      <span class="tp-01__right">SCALE 1:1 · REV 04<br>14.MAY.2026</span>
    </div>
    <div class="tp-01__object">
      <svg class="tp-01__svg" viewBox="0 0 800 460" preserveAspectRatio="xMidYMid meet">
        <rect x="200" y="160" width="400" height="200" stroke="#1c2330" stroke-width="1.5" fill="none"/>
        <rect x="340" y="130" width="80" height="30" stroke="#1c2330" stroke-width="1.5" fill="none"/>
        <rect x="355" y="120" width="50" height="10" stroke="#1c2330" stroke-width="1" fill="none"/>
        <path d="M200 160 L160 180 L160 340 L200 360 Z" stroke="#1c2330" stroke-width="1.5" fill="none"/>
        <circle cx="450" cy="260" r="90" stroke="#1c2330" stroke-width="1.5" fill="none"/>
        <circle cx="450" cy="260" r="68" stroke="#1c2330" stroke-width="1" fill="none"/>
        <circle cx="450" cy="260" r="46" stroke="#1c2330" stroke-width="0.8" fill="none"/>
        <circle cx="450" cy="260" r="24" stroke="#1c2330" stroke-width="0.6" fill="none"/>
        <circle cx="260" cy="200" r="22" stroke="#1c2330" stroke-width="1" fill="none"/>
        <circle cx="260" cy="200" r="3" fill="#1c2330"/>
        <circle cx="540" cy="200" r="18" stroke="#1c2330" stroke-width="1" fill="none"/>
        <rect x="320" y="190" width="40" height="28" stroke="#1c2330" stroke-width="0.8" fill="none"/>
        <line x1="200" y1="380" x2="600" y2="380" stroke="#1c2330" stroke-width="0.5" stroke-dasharray="4,4"/>
        <text x="400" y="398" text-anchor="middle" font-family="IBM Plex Mono" font-size="10" fill="#1c2330">142mm</text>
      </svg>
      <div class="tp-01__hot tp-01__hot-1"></div>
      <svg class="tp-01__leader tp-01__leader-1"><line x1="0" y1="100" x2="180" y2="0" stroke="#1c2330" stroke-width="1"/></svg>
      <div class="tp-01__note tp-01__note-1">
        <div class="tp-01__num">01</div>
        <div class="tp-01__label">— Optical Assembly</div>
        <div class="tp-01__part">35mm <em>f/1.4</em> prime</div>
        <div class="tp-01__desc">Twelve-element design across nine groups. Aspherical front and rear, two ED elements mid-stack.</div>
        <div class="tp-01__spec">
          <span class="tp-01__k">Glass</span><span class="tp-01__v">12 / 9</span>
          <span class="tp-01__k">Coating</span><span class="tp-01__v">Nano-V</span>
          <span class="tp-01__k">Weight</span><span class="tp-01__v">410g</span>
        </div>
      </div>
      <div class="tp-01__hot tp-01__hot-2"></div>
      <svg class="tp-01__leader tp-01__leader-2"><line x1="160" y1="80" x2="0" y2="0" stroke="#1c2330" stroke-width="1"/></svg>
      <div class="tp-01__note tp-01__note-2">
        <div class="tp-01__num">02</div>
        <div class="tp-01__label">— Shutter Dial</div>
        <div class="tp-01__part">Mechanical <em>command</em> dial</div>
        <div class="tp-01__desc">Detented brass with knurled edge. Range 1/8000s to 30s plus bulb. Locks with a quarter-press.</div>
        <div class="tp-01__spec">
          <span class="tp-01__k">Steps</span><span class="tp-01__v">1/3 EV</span>
          <span class="tp-01__k">Travel</span><span class="tp-01__v">300°</span>
          <span class="tp-01__k">Cycles</span><span class="tp-01__v">500k</span>
        </div>
      </div>
      <div class="tp-01__hot tp-01__hot-3"></div>
      <svg class="tp-01__leader tp-01__leader-3"><line x1="0" y1="120" x2="80" y2="0" stroke="#1c2330" stroke-width="1"/></svg>
      <div class="tp-01__note tp-01__note-3">
        <div class="tp-01__num">03</div>
        <div class="tp-01__label">— Accessory Port</div>
        <div class="tp-01__part">ISO 518 <em>hot shoe</em></div>
        <div class="tp-01__desc">Standard mount with X-sync at 1/250s. Center pin plus four data contacts for TTL flash control.</div>
        <div class="tp-01__spec">
          <span class="tp-01__k">Sync</span><span class="tp-01__v">1/250s</span>
          <span class="tp-01__k">Pins</span><span class="tp-01__v">1+4</span>
          <span class="tp-01__k">Hot</span><span class="tp-01__v">12V</span>
        </div>
      </div>
      <div class="tp-01__hot tp-01__hot-4"></div>
      <svg class="tp-01__leader tp-01__leader-4"><line x1="160" y1="0" x2="0" y2="80" stroke="#1c2330" stroke-width="1"/></svg>
      <div class="tp-01__note tp-01__note-4">
        <div class="tp-01__num">04</div>
        <div class="tp-01__label">— Hand Grip</div>
        <div class="tp-01__part">Magnesium <em>undergrip</em></div>
        <div class="tp-01__desc">Cast magnesium core wrapped in pressed cowhide. Conceals battery, card slot, and focus-recall trigger.</div>
        <div class="tp-01__spec">
          <span class="tp-01__k">Wrap</span><span class="tp-01__v">Cowhide</span>
          <span class="tp-01__k">Core</span><span class="tp-01__v">MgAl</span>
          <span class="tp-01__k">Battery</span><span class="tp-01__v">7.4 Wh</span>
        </div>
      </div>
    </div>
  </div>
</div>
.tp-01 {
  min-height: 100vh;
  background: #f4f3ee;
  padding: 32px 24px;
  background-image: linear-gradient(rgba(28, 35, 48, 0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(28, 35, 48, 0.06) 1px, transparent 1px);
  background-size: 24px 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Inter', system-ui, sans-serif;
  color: #1c2330;
  /* Force full width of the stage. .cf-stage uses align-items: center
       which collapses children to their content-min-width when they
       don't declare an explicit width. Without this, .tp-01__paper's
       width: 100% resolves against a collapsed parent and produces the
       tiny-square + content-overflow we saw. */
  width: 100%;
  box-sizing: border-box;
}

.tp-01__paper {
  position: relative;
  width: 100%;
  max-width: 760px;
  aspect-ratio: 16 / 10;
  background: #fbfaf6;
  border: 1px solid rgba(28, 35, 48, 0.2);
  overflow: visible;
}

.tp-01__paper::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: linear-gradient(rgba(28, 35, 48, 0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(28, 35, 48, 0.05) 1px, transparent 1px);
  background-size: 18px 18px;
  pointer-events: none;
}

.tp-01__meta {
  position: absolute;
  top: 12px;
  left: 16px;
  right: 16px;
  display: flex;
  justify-content: space-between;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 9.5px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: rgba(28, 35, 48, 0.55);
  z-index: 2;
}

.tp-01__right {
  text-align: right;
  line-height: 1.6;
}

.tp-01__corner {
  position: absolute;
  width: 12px;
  height: 12px;
  border: 1px solid rgba(28, 35, 48, 0.4);
}

.tp-01__tl {
  top: 6px;
  left: 6px;
  border-right: none;
  border-bottom: none;
}

.tp-01__tr {
  top: 6px;
  right: 6px;
  border-left: none;
  border-bottom: none;
}

.tp-01__bl {
  bottom: 6px;
  left: 6px;
  border-right: none;
  border-top: none;
}

.tp-01__br {
  bottom: 6px;
  right: 6px;
  border-left: none;
  border-top: none;
}

.tp-01__object {
  position: absolute;
  inset: 50px 60px 50px 60px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.tp-01__svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}

.tp-01__hot {
  position: absolute;
  width: 16px;
  height: 16px;
  margin: -8px;
  border-radius: 50%;
  cursor: help;
  z-index: 5;
}

.tp-01__hot::before {
  content: '';
  position: absolute;
  inset: 5px;
  background: #c1422c;
  border-radius: 50%;
}

.tp-01__hot::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid #c1422c;
  border-radius: 50%;
  animation: tp-01-pulse 2.4s ease-out infinite;
}

@keyframes tp-01-pulse {
  0% {
    transform: scale(0.6);
    opacity: 0.8;
  }

  100% {
    transform: scale(2);
    opacity: 0;
  }
}

.tp-01__hot-1 {
  top: 56%;
  left: 56.25%;
}

.tp-01__hot-2 {
  top: 25%;
  left: 32.5%;
}

.tp-01__hot-3 {
  top: 30%;
  left: 47.5%;
}

.tp-01__hot-4 {
  top: 56%;
  left: 22.5%;
}
/* Leader lines hidden — they were stylish but only worked with the
   original off-paper annotation positions. Now that notes are clamped
   to the paper's four corners (so .cf-stage doesn't clip them), drawing
   a line from each hotspot to its corner would require per-hotspot
   trig that's not worth the complexity. Keeping the SVG markup in
   place so the adjacent-sibling selector chain
   (.tp-01__hot:hover + .tp-01__leader + .tp-01__note) still resolves. */

.tp-01__leader {
  display: none;
}

.tp-01__note {
  position: absolute;
  width: 190px;
  background: #fbfaf6;
  border: 1px solid #1c2330;
  padding: 11px 13px;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 10.5px;
  line-height: 1.6;
  color: #1c2330;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0s linear 0.3s;
  pointer-events: none;
  z-index: 10;
  box-shadow: 4px 4px 0 rgba(28, 35, 48, 0.08);
}

.tp-01__hot:hover + .tp-01__leader + .tp-01__note {
  opacity: 1;
  visibility: visible;
  transition-delay: 0.12s;
}
/* Annotations positioned in each corner of the schematic object's
   coordinate space — kept inside the paper bounds so they don't get
   clipped by .cf-stage's overflow:hidden. Hotspot 1 (lens) lower-right,
   note in upper-right; hotspot 2 (top dial) upper-left, note upper-left;
   hotspot 3 (hot shoe) top-center, note top-right; hotspot 4 (grip)
   lower-left, note lower-left. */

.tp-01__note-1 {
  top: 4%;
  right: 0;
  left: auto;
}

.tp-01__note-2 {
  top: 4%;
  left: 0;
}

.tp-01__note-3 {
  top: 4%;
  left: 50%;
}

.tp-01__note-4 {
  bottom: 4%;
  left: 0;
  top: auto;
}

.tp-01__num {
  display: inline-block;
  font-family: Georgia, serif;
  font-weight: 500;
  font-style: italic;
  color: #c1422c;
  font-size: 20px;
  line-height: 1;
  margin-bottom: 3px;
}

.tp-01__label {
  font-size: 9.5px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: rgba(28, 35, 48, 0.6);
  margin-bottom: 6px;
}

.tp-01__part {
  font-family: Georgia, serif;
  font-size: 15px;
  font-weight: 500;
  line-height: 1.2;
  margin-bottom: 6px;
  color: #1c2330;
}

.tp-01__part em {
  font-style: italic;
  color: #c1422c;
}

.tp-01__desc {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 10.5px;
  color: rgba(28, 35, 48, 0.75);
  line-height: 1.5;
}

.tp-01__spec {
  margin-top: 8px;
  padding-top: 6px;
  border-top: 1px dashed rgba(28, 35, 48, 0.3);
  display: grid;
  grid-template-columns: 54px 1fr;
  gap: 3px 8px;
  font-size: 9.5px;
}

.tp-01__k {
  color: rgba(28, 35, 48, 0.5);
}

.tp-01__v {
  color: #c1422c;
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Tooltip from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Schematic Hotspot
Source: https://codefronts.com/snippets/css-tooltips/schematic-hotspot/

For product diagrams and exploded views. Four pulsing red dots dot a wireframe drawing; hover any one and a leader line draws itself to a numbered callout box with part name, description, and a tiny spec table. The whole annotation reads like a CAD title block.
## HTML
```html
<div class="tp-01">
  <div class="tp-01__paper">
    <span class="tp-01__corner tp-01__tl"></span>
    <span class="tp-01__corner tp-01__tr"></span>
    <span class="tp-01__corner tp-01__bl"></span>
    <span class="tp-01__corner tp-01__br"></span>
    <div class="tp-01__meta">
      <span>DWG-0142-A · CAMERA / EXPLODED VIEW</span>
      <span class="tp-01__right">SCALE 1:1 · REV 04<br>14.MAY.2026</span>
    </div>
    <div class="tp-01__object">
      <svg class="tp-01__svg" viewBox="0 0 800 460" preserveAspectRatio="xMidYMid meet">
        <rect x="200" y="160" width="400" height="200" stroke="#1c2330" stroke-width="1.5" fill="none"/>
        <rect x="340" y="130" width="80" height="30" stroke="#1c2330" stroke-width="1.5" fill="none"/>
        <rect x="355" y="120" width="50" height="10" stroke="#1c2330" stroke-width="1" fill="none"/>
        <path d="M200 160 L160 180 L160 340 L200 360 Z" stroke="#1c2330" stroke-width="1.5" fill="none"/>
        <circle cx="450" cy="260" r="90" stroke="#1c2330" stroke-width="1.5" fill="none"/>
        <circle cx="450" cy="260" r="68" stroke="#1c2330" stroke-width="1" fill="none"/>
        <circle cx="450" cy="260" r="46" stroke="#1c2330" stroke-width="0.8" fill="none"/>
        <circle cx="450" cy="260" r="24" stroke="#1c2330" stroke-width="0.6" fill="none"/>
        <circle cx="260" cy="200" r="22" stroke="#1c2330" stroke-width="1" fill="none"/>
        <circle cx="260" cy="200" r="3" fill="#1c2330"/>
        <circle cx="540" cy="200" r="18" stroke="#1c2330" stroke-width="1" fill="none"/>
        <rect x="320" y="190" width="40" height="28" stroke="#1c2330" stroke-width="0.8" fill="none"/>
        <line x1="200" y1="380" x2="600" y2="380" stroke="#1c2330" stroke-width="0.5" stroke-dasharray="4,4"/>
        <text x="400" y="398" text-anchor="middle" font-family="IBM Plex Mono" font-size="10" fill="#1c2330">142mm</text>
      </svg>
      <div class="tp-01__hot tp-01__hot-1"></div>
      <svg class="tp-01__leader tp-01__leader-1"><line x1="0" y1="100" x2="180" y2="0" stroke="#1c2330" stroke-width="1"/></svg>
      <div class="tp-01__note tp-01__note-1">
        <div class="tp-01__num">01</div>
        <div class="tp-01__label">— Optical Assembly</div>
        <div class="tp-01__part">35mm <em>f/1.4</em> prime</div>
        <div class="tp-01__desc">Twelve-element design across nine groups. Aspherical front and rear, two ED elements mid-stack.</div>
        <div class="tp-01__spec">
          <span class="tp-01__k">Glass</span><span class="tp-01__v">12 / 9</span>
          <span class="tp-01__k">Coating</span><span class="tp-01__v">Nano-V</span>
          <span class="tp-01__k">Weight</span><span class="tp-01__v">410g</span>
        </div>
      </div>
      <div class="tp-01__hot tp-01__hot-2"></div>
      <svg class="tp-01__leader tp-01__leader-2"><line x1="160" y1="80" x2="0" y2="0" stroke="#1c2330" stroke-width="1"/></svg>
      <div class="tp-01__note tp-01__note-2">
        <div class="tp-01__num">02</div>
        <div class="tp-01__label">— Shutter Dial</div>
        <div class="tp-01__part">Mechanical <em>command</em> dial</div>
        <div class="tp-01__desc">Detented brass with knurled edge. Range 1/8000s to 30s plus bulb. Locks with a quarter-press.</div>
        <div class="tp-01__spec">
          <span class="tp-01__k">Steps</span><span class="tp-01__v">1/3 EV</span>
          <span class="tp-01__k">Travel</span><span class="tp-01__v">300°</span>
          <span class="tp-01__k">Cycles</span><span class="tp-01__v">500k</span>
        </div>
      </div>
      <div class="tp-01__hot tp-01__hot-3"></div>
      <svg class="tp-01__leader tp-01__leader-3"><line x1="0" y1="120" x2="80" y2="0" stroke="#1c2330" stroke-width="1"/></svg>
      <div class="tp-01__note tp-01__note-3">
        <div class="tp-01__num">03</div>
        <div class="tp-01__label">— Accessory Port</div>
        <div class="tp-01__part">ISO 518 <em>hot shoe</em></div>
        <div class="tp-01__desc">Standard mount with X-sync at 1/250s. Center pin plus four data contacts for TTL flash control.</div>
        <div class="tp-01__spec">
          <span class="tp-01__k">Sync</span><span class="tp-01__v">1/250s</span>
          <span class="tp-01__k">Pins</span><span class="tp-01__v">1+4</span>
          <span class="tp-01__k">Hot</span><span class="tp-01__v">12V</span>
        </div>
      </div>
      <div class="tp-01__hot tp-01__hot-4"></div>
      <svg class="tp-01__leader tp-01__leader-4"><line x1="160" y1="0" x2="0" y2="80" stroke="#1c2330" stroke-width="1"/></svg>
      <div class="tp-01__note tp-01__note-4">
        <div class="tp-01__num">04</div>
        <div class="tp-01__label">— Hand Grip</div>
        <div class="tp-01__part">Magnesium <em>undergrip</em></div>
        <div class="tp-01__desc">Cast magnesium core wrapped in pressed cowhide. Conceals battery, card slot, and focus-recall trigger.</div>
        <div class="tp-01__spec">
          <span class="tp-01__k">Wrap</span><span class="tp-01__v">Cowhide</span>
          <span class="tp-01__k">Core</span><span class="tp-01__v">MgAl</span>
          <span class="tp-01__k">Battery</span><span class="tp-01__v">7.4 Wh</span>
        </div>
      </div>
    </div>
  </div>
</div>
```
## CSS
```css
.tp-01 {
  min-height: 100vh;
  background: #f4f3ee;
  padding: 32px 24px;
  background-image: linear-gradient(rgba(28, 35, 48, 0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(28, 35, 48, 0.06) 1px, transparent 1px);
  background-size: 24px 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Inter', system-ui, sans-serif;
  color: #1c2330;
  /* Force full width of the stage. .cf-stage uses align-items: center
       which collapses children to their content-min-width when they
       don't declare an explicit width. Without this, .tp-01__paper's
       width: 100% resolves against a collapsed parent and produces the
       tiny-square + content-overflow we saw. */
  width: 100%;
  box-sizing: border-box;
}

.tp-01__paper {
  position: relative;
  width: 100%;
  max-width: 760px;
  aspect-ratio: 16 / 10;
  background: #fbfaf6;
  border: 1px solid rgba(28, 35, 48, 0.2);
  overflow: visible;
}

.tp-01__paper::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: linear-gradient(rgba(28, 35, 48, 0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(28, 35, 48, 0.05) 1px, transparent 1px);
  background-size: 18px 18px;
  pointer-events: none;
}

.tp-01__meta {
  position: absolute;
  top: 12px;
  left: 16px;
  right: 16px;
  display: flex;
  justify-content: space-between;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 9.5px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: rgba(28, 35, 48, 0.55);
  z-index: 2;
}

.tp-01__right {
  text-align: right;
  line-height: 1.6;
}

.tp-01__corner {
  position: absolute;
  width: 12px;
  height: 12px;
  border: 1px solid rgba(28, 35, 48, 0.4);
}

.tp-01__tl {
  top: 6px;
  left: 6px;
  border-right: none;
  border-bottom: none;
}

.tp-01__tr {
  top: 6px;
  right: 6px;
  border-left: none;
  border-bottom: none;
}

.tp-01__bl {
  bottom: 6px;
  left: 6px;
  border-right: none;
  border-top: none;
}

.tp-01__br {
  bottom: 6px;
  right: 6px;
  border-left: none;
  border-top: none;
}

.tp-01__object {
  position: absolute;
  inset: 50px 60px 50px 60px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.tp-01__svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}

.tp-01__hot {
  position: absolute;
  width: 16px;
  height: 16px;
  margin: -8px;
  border-radius: 50%;
  cursor: help;
  z-index: 5;
}

.tp-01__hot::before {
  content: '';
  position: absolute;
  inset: 5px;
  background: #c1422c;
  border-radius: 50%;
}

.tp-01__hot::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid #c1422c;
  border-radius: 50%;
  animation: tp-01-pulse 2.4s ease-out infinite;
}

@keyframes tp-01-pulse {
  0% {
    transform: scale(0.6);
    opacity: 0.8;
  }

  100% {
    transform: scale(2);
    opacity: 0;
  }
}

.tp-01__hot-1 {
  top: 56%;
  left: 56.25%;
}

.tp-01__hot-2 {
  top: 25%;
  left: 32.5%;
}

.tp-01__hot-3 {
  top: 30%;
  left: 47.5%;
}

.tp-01__hot-4 {
  top: 56%;
  left: 22.5%;
}
/* Leader lines hidden — they were stylish but only worked with the
   original off-paper annotation positions. Now that notes are clamped
   to the paper's four corners (so .cf-stage doesn't clip them), drawing
   a line from each hotspot to its corner would require per-hotspot
   trig that's not worth the complexity. Keeping the SVG markup in
   place so the adjacent-sibling selector chain
   (.tp-01__hot:hover + .tp-01__leader + .tp-01__note) still resolves. */

.tp-01__leader {
  display: none;
}

.tp-01__note {
  position: absolute;
  width: 190px;
  background: #fbfaf6;
  border: 1px solid #1c2330;
  padding: 11px 13px;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 10.5px;
  line-height: 1.6;
  color: #1c2330;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0s linear 0.3s;
  pointer-events: none;
  z-index: 10;
  box-shadow: 4px 4px 0 rgba(28, 35, 48, 0.08);
}

.tp-01__hot:hover + .tp-01__leader + .tp-01__note {
  opacity: 1;
  visibility: visible;
  transition-delay: 0.12s;
}
/* Annotations positioned in each corner of the schematic object's
   coordinate space — kept inside the paper bounds so they don't get
   clipped by .cf-stage's overflow:hidden. Hotspot 1 (lens) lower-right,
   note in upper-right; hotspot 2 (top dial) upper-left, note upper-left;
   hotspot 3 (hot shoe) top-center, note top-right; hotspot 4 (grip)
   lower-left, note lower-left. */

.tp-01__note-1 {
  top: 4%;
  right: 0;
  left: auto;
}

.tp-01__note-2 {
  top: 4%;
  left: 0;
}

.tp-01__note-3 {
  top: 4%;
  left: 50%;
}

.tp-01__note-4 {
  bottom: 4%;
  left: 0;
  top: auto;
}

.tp-01__num {
  display: inline-block;
  font-family: Georgia, serif;
  font-weight: 500;
  font-style: italic;
  color: #c1422c;
  font-size: 20px;
  line-height: 1;
  margin-bottom: 3px;
}

.tp-01__label {
  font-size: 9.5px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: rgba(28, 35, 48, 0.6);
  margin-bottom: 6px;
}

.tp-01__part {
  font-family: Georgia, serif;
  font-size: 15px;
  font-weight: 500;
  line-height: 1.2;
  margin-bottom: 6px;
  color: #1c2330;
}

.tp-01__part em {
  font-style: italic;
  color: #c1422c;
}

.tp-01__desc {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 10.5px;
  color: rgba(28, 35, 48, 0.75);
  line-height: 1.5;
}

.tp-01__spec {
  margin-top: 8px;
  padding-top: 6px;
  border-top: 1px dashed rgba(28, 35, 48, 0.3);
  display: grid;
  grid-template-columns: 54px 1fr;
  gap: 3px 8px;
  font-size: 9.5px;
}

.tp-01__k {
  color: rgba(28, 35, 48, 0.5);
}

.tp-01__v {
  color: #c1422c;
}
```

How this works

The stage is a title-block schematic — a warm paper card (#fbfaf6) laid on a graph-paper grid, framed by four .tp-01__corner registration ticks and a monospaced meta strip that reads SCALE 1:1, REV 04, 14.MAY.2026. A camera drawn as inline SVG sits inside .tp-01__object, and four .tp-01__hot discs pin the callouts at 32.5%, 47.5%, 56.25% and 22.5% along the drawing. Each hotspot is a 16px absolutely positioned circle with a schematic red inner dot (#c1422c) and an outer ring that pulses on the tp-01-pulse keyframe — scaling from 0.6 to 2 across 2.4s so the drawing reads as annotated live, not static.

The annotation itself is drafting-room, not product marketing. .tp-01__note is a 190px paper card with a hairline #1c2330 border and a 4px offset flat shadow, styled in JetBrains Mono at 10.5px so it matches CAD title-block typography. The Georgia italic .tp-01__num (20px, schematic red) prints the part number the way a draftsman would; a small caps .tp-01__label gives the callout name, and a spec grid (grid-template-columns: 54px 1fr) closes with monospaced key-value pairs — Glass 12 / 9, Sync 1/250s, Cycles 500k — separated from prose by a dashed rule.

Reveal is done by an adjacent-sibling chain: .tp-01__hot:hover + .tp-01__leader + .tp-01__note flips opacity to 1 and visibility to visible, with a 120ms transition-delay so the annotation lands after the pulse's next tick. The wrapper carries width: 100% so the stage's centering rule cannot collapse the paper to intrinsic width — a documented gotcha in the source comment. Notes are clamped to the four paper corners (top: 4%; right: 0, etc.) so overflow: hidden on the parent card never clips them, and the leader SVGs are kept in the DOM only to preserve that sibling selector chain.

Make it yours

  • Recolor the schematic red by search-replacing #c1422c across the hotspot dot, ring, italic numeral and spec value; a navy-on-cream drafting palette (#1c4a7a) or an ozone-blueprint palette (invert paper to #0f2b4a with cyan hotspots) reads equally credible for engineering docs.
  • Swap JetBrains Mono for IBM Plex Mono, Berkeley Mono or Space Mono without changing sizes — the 9.5px small caps meta strip and the 10.5px note body were tuned around 0.55em cap heights common to those faces.
  • Border-radius is intentionally 0 on .tp-01__paper and .tp-01__note — drawings are square. To soften for a hybrid docs UI, set --tp-01-radius: 4px on the paper only and leave the notes flat so the annotation still reads as engineering.
  • For a dark drafting-table variant flip the paper to #181a1e, grid lines to rgba(255,255,255,0.05), the note border to #3a4152 and the italic numeral to #f0b96b; the pulse tint (#c1422c) still reads as a warning against dark stock.
  • Tailwind arbitrary-value map: bg-[#fbfaf6] for paper, border-[#1c2330]/20 for the frame, bg-[#c1422c] for the hotspot dot, font-[Georgia] italic text-[#c1422c] for the numeral, text-[10.5px] leading-[1.6] for the note body and grid-cols-[54px_1fr] for the spec grid.

Gotchas — read before shipping

  • Hover-only reveal fails WCAG 2.1 SC 1.4.13 Content on Hover or Focus and SC 2.1.1 Keyboard. Give .tp-01__hot a real trigger — tabindex="0", role="button" and aria-describedby linking each hotspot to its note's id — and mirror the CSS hover rule with a :focus-visible and :focus-within selector chain so keyboard users see the same annotation.
  • The tp-01-pulse ring animates transform and opacity infinitely, which can flag SC 2.2.2 Pause, Stop, Hide if the demo runs longer than 5s. Wrap the ring animation in a @media (prefers-reduced-motion: reduce) block that sets animation: none and drops the ring to a static 1px outline so vestibular users still see the hotspot without motion.
  • SC 1.4.3 Contrast (Minimum) fails on .tp-01__label (rgba(28,35,48,0.6) on #fbfaf6 yields roughly 4.4:1 at 9.5px, which is small text) and on .tp-01__k (0.5 alpha). Raise the alphas to 0.75 or bump the label to 11px so the 4.5:1 threshold holds; verify with axe-core, not eyeball.
  • The stage rule width: 100% on .tp-01 is load-bearing — remove it and the parent flex container's align-items: center collapses the paper to its intrinsic content width and the whole drawing renders as a narrow strip. Documented in the source; keep it if you refactor.
  • Adjacent-sibling chains (.tp-01__hot:hover + .tp-01__leader + .tp-01__note) break the moment any wrapper is inserted between hotspot and note — common regressions when someone adds a React fragment or a hydration marker. Prefer :has(.tp-01__hot:hover) on the parent object once Firefox parity ships, and keep DOM order stable in the meantime.
  • The 4 corner notes are pinned with percentage offsets against .tp-01__paper's 16:10 aspect ratio. Under 480px viewport widths aspect-ratio keeps the paper proportional but the 190px note width plus 4% offset overflows the frame. Add a @media (max-width: 520px) block that stacks notes below the drawing and hides the corner registration ticks.

Browser support

ChromeSafariFirefoxEdge
88+15+89+88+

aspect-ratio on .tp-01__paper needs Safari 15+ and Firefox 89+; the rest is transitions, keyframes and adjacent-sibling selectors that have been baseline since 2017.

Techniques used in this demo

Search CodeFronts

Loading…