Skip to content

Instantly share code, notes, and snippets.

@Sharktheone
Created June 17, 2026 13:51
Show Gist options
  • Select an option

  • Save Sharktheone/f05c85670025d18bbcbd63bf525d6925 to your computer and use it in GitHub Desktop.

Select an option

Save Sharktheone/f05c85670025d18bbcbd63bf525d6925 to your computer and use it in GitHub Desktop.
Webkit svg trails
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebKit note-drag trail repro</title>
<style>
html, body {
margin: 0;
height: 100%;
font-family: system-ui, sans-serif;
background: #ffffff;
color: #eee;
overflow: hidden;
}
/* --- the "infinite canvas" viewport -------------------------------- */
/* Mirrors styles.module.scss .noteAreaWrapper { overflow:hidden } */
.viewport {
position: absolute;
inset: 0;
overflow: hidden;
}
/* Mirrors .notearea — the scaled+translated canvas.
transform: scale() creates a new containing block + compositing
layer; this is what makes WebKit's incremental repaint of children
go wrong at fractional device-pixel scales. */
.canvas {
position: relative;
width: 100%;
height: 100%;
transform: scale(1.09) translate(0px, 0px); /* odd scale on purpose */
transform-origin: center center;
}
/* Mirrors the wrapper div in NoteArea.tsx that gets left/top set.
position:absolute + animating left/top is the trigger. */
.note {
position: absolute;
width: 233px;
user-select: none;
cursor: grab;
}
.note:active { cursor: grabbing; }
/* --bordercolor is bright on purpose: the border is what WebKit
fails to clear, so it's the color you see smeared in the trail. */
.note-body {
background: var(--notecolor);
border: solid var(--bordercolor);
border-width: 4px 4px 0 4px;
padding: 12px;
color: #111;
min-height: 70px;
font-size: 14px;
}
/* Dogear with gradient + pseudo-element, same shape as the real app.
Gradients / pseudo-elements give WebKit more to (fail to) repaint. */
.dogears {
height: 25px;
background: var(--notecolor);
width: calc(100% - 25px);
position: relative;
border: solid var(--bordercolor);
border-width: 0 0 4px 4px;
}
.dogears::before {
content: '';
position: absolute;
top: 0; right: -25px;
width: 25px; height: 25px;
background: linear-gradient(135deg,
color-mix(in srgb, var(--notecolor) 70%, var(--bordercolor) 30%) 0%,
color-mix(in srgb, var(--notecolor) 70%, var(--bordercolor) 30%) calc(50% - 4px),
var(--bordercolor) calc(50% - 4px), var(--bordercolor) 50%,
transparent 50%, transparent 100%);
border: solid var(--bordercolor);
border-width: 4px 0 0 4px;
}
/* The SVG context the bug report calls out. Tabler icons render as
inline SVG all over the canvas, so each note carries one. */
.note svg { display: block; margin-bottom: 6px; color: #111; }
/* --- HUD ----------------------------------------------------------- */
.hud {
position: absolute;
top: 12px; left: 50%;
transform: translateX(-50%);
z-index: 10;
display: flex;
gap: 6px;
align-items: center;
background: rgba(20,20,20,.85);
border: 1px solid #555;
border-radius: 8px;
padding: 8px 10px;
font-size: 12px;
}
.hud button {
font: inherit;
background: #444;
color: #eee;
border: 1px solid #666;
border-radius: 5px;
padding: 4px 8px;
cursor: pointer;
}
.hud button.on { background: #2f7; color: #111; border-color: #2f7; }
.hud .val { font-variant-numeric: tabular-nums; min-width: 3ch; text-align: right; }
.hint {
position: absolute;
bottom: 12px; left: 12px;
z-index: 10;
max-width: 520px;
background: rgba(20,20,20,.85);
border: 1px solid #555;
border-radius: 8px;
padding: 10px 12px;
font-size: 12px;
line-height: 1.5;
}
kbd {
background:#333;border:1px solid #666;border-bottom-width:2px;
border-radius:4px;padding:0 5px;font-family:inherit;
}
</style>
</head>
<body>
<div class="viewport" id="viewport">
<div class="canvas" id="canvas"></div>
</div>
<div class="hud">
<span>scale</span>
<button id="scaleDown">−</button>
<span class="val" id="scaleVal">1.09</span>
<button id="scaleUp">+</button>
<span style="opacity:.4">|</span>
<button id="toggleScale" class="on">scaled parent</button>
<button id="toggleSvg" class="on">SVG in notes</button>
<button id="toggleMove" class="on">move via left/top</button>
</div>
<div class="hint">
<b>Repro:</b> set the browser zoom to <b>109%</b> (<kbd>⌘</kbd>+ a few times),
then drag a note around in Safari/WebKit. The note smears a trail of stale
copies instead of repainting cleanly.<br />
Toggle the buttons to isolate the trigger. The trail needs all three on:
a <i>scaled parent</i> + a <i>fractional effective scale</i> (page-zoom ×
canvas-scale) + moving the child by <i>left/top</i>. Flip “move via left/top”
to off (uses <code>transform: translate</code> instead) and the trail
disappears — that’s the fix for the real app.
</div>
<script>
const NOTE_COLOR = '#ffffff';
const BORDER_COLOR = '#000000';
const ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z" />
<line x1="9" y1="9" x2="10" y2="9" />
<line x1="9" y1="13" x2="15" y2="13" />
<line x1="9" y1="17" x2="15" y2="17" />
</svg>`;
const canvas = document.getElementById('canvas');
const scaleVal = document.getElementById('scaleVal');
let scale = 1.09;
let useSvg = true;
let moveViaLeftTop = true;
// A single note, centered-ish, so the trail is the only thing on screen.
const notes = [];
function buildNotes() {
canvas.innerHTML = '';
notes.length = 0;
const x = 120, y = 120;
const note = document.createElement('div');
note.className = 'note';
note.style.setProperty('--notecolor', NOTE_COLOR);
note.style.setProperty('--bordercolor', BORDER_COLOR);
note.style.left = x + 'px';
note.style.top = y + 'px';
note.dataset.x = x;
note.dataset.y = y;
note.innerHTML =
`<div class="note-body">${useSvg ? ICON : ''}drag me around</div>` +
`<div class="dogears"></div>`;
attachDrag(note);
canvas.appendChild(note);
notes.push(note);
}
function place(note, x, y) {
note.dataset.x = x;
note.dataset.y = y;
if (moveViaLeftTop) {
note.style.transform = '';
note.style.left = x + 'px';
note.style.top = y + 'px';
} else {
// The fix: keep left/top fixed, move with a compositor-friendly transform.
note.style.left = '0px';
note.style.top = '0px';
note.style.transform = `translate(${x}px, ${y}px)`;
}
}
function attachDrag(note) {
note.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
e.preventDefault();
const startX = e.clientX / scale;
const startY = e.clientY / scale;
const orig = { x: +note.dataset.x, y: +note.dataset.y };
function onMove(ev) {
const dx = ev.clientX / scale - startX;
const dy = ev.clientY / scale - startY;
place(note, orig.x + dx, orig.y + dy);
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
function applyCanvasTransform(scaleOn) {
canvas.style.transform = scaleOn
? `scale(${scale}) translate(0px, 0px)`
: 'none';
}
// --- HUD wiring ------------------------------------------------------
const toggleScale = document.getElementById('toggleScale');
const toggleSvg = document.getElementById('toggleSvg');
const toggleMove = document.getElementById('toggleMove');
document.getElementById('scaleUp').onclick = () => setScale(scale + 0.01);
document.getElementById('scaleDown').onclick = () => setScale(scale - 0.01);
function setScale(s) {
scale = Math.round(Math.max(0.2, Math.min(3, s)) * 100) / 100;
scaleVal.textContent = scale.toFixed(2);
applyCanvasTransform(toggleScale.classList.contains('on'));
}
toggleScale.onclick = () => {
toggleScale.classList.toggle('on');
applyCanvasTransform(toggleScale.classList.contains('on'));
};
toggleSvg.onclick = () => {
toggleSvg.classList.toggle('on');
useSvg = toggleSvg.classList.contains('on');
buildNotes();
};
toggleMove.onclick = () => {
toggleMove.classList.toggle('on');
moveViaLeftTop = toggleMove.classList.contains('on');
// re-place every note with the new strategy
notes.forEach(n => place(n, +n.dataset.x, +n.dataset.y));
};
buildNotes();
applyCanvasTransform(true);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment