Anchor Interactive
Use autoupdate to reposition anchored content as its anchor moves.
Code
The following example assumes that you have imported the component and set up the theme.
<div class="viewport"> <div class="anchor-element"> <p class="anchor-element-label">Anchor Element</p> </div>
<sv-anchor anchor-element=".anchor-element" autoupdate preferred-position-order="top center center, left center center, right center center, bottom center center" viewport-element=".viewport" > <div class="anchored-element"> <p>Anchored</p> </div> </sv-anchor></div>
47 collapsed lines
<script type="module"> // Drag implementation
/** @type {HTMLDivElement} */ const anchorEl = document.querySelector(".anchor-element");
let isDragging = false; let startX = 0; let startY = 0; let currentTranslateX = 0; let currentTranslateY = 0;
/** @param {PointerEvent} event */ const onPointerDown = event => { isDragging = true; startX = event.clientX; startY = event.clientY; anchorEl.setPointerCapture(event.pointerId); };
/** @param {PointerEvent} event */ const onPointerMove = event => { if (!isDragging) return; const dx = event.clientX - startX; const dy = event.clientY - startY; const newTranslateX = currentTranslateX + dx; const newTranslateY = currentTranslateY + dy; anchorEl.style.transform = `translate(${newTranslateX}px, ${newTranslateY}px)`; };
/** @param {PointerEvent} event */ const onPointerUp = event => { isDragging = false;
const transform = anchorEl.style.transform.match(/translate\((.+?)px, (.+?)px\)/); if (transform) { currentTranslateX = parseFloat(transform[1]); currentTranslateY = parseFloat(transform[2]); }
anchorEl.releasePointerCapture(event.pointerId); };
anchorEl.addEventListener("pointerdown", onPointerDown); anchorEl.addEventListener("pointermove", onPointerMove); anchorEl.addEventListener("pointerup", onPointerUp);</script>
75 collapsed lines
<style> .viewport { overflow: hidden; overflow-x: clip; overscroll-behavior: none; border: solid 1px red; border-radius: 0.4em;
.anchor-element { position: relative; z-index: 50; display: flex; align-items: center; justify-content: center; inline-size: 160px; block-size: 160px; margin: auto; margin-block: 10em; color: white; text-align: center; touch-action: none; cursor: pointer; border: solid 1px #aeae18; border-radius: 1em;
&::after { position: absolute; inset-block-start: 50%; inset-inline: -10% 0; inline-size: 120%; block-size: 1px; content: ""; background-color: #aeae18; }
&::before { position: absolute; inset-block: -10% 0; inset-inline-start: 50%; inline-size: 1px; block-size: 120%; content: ""; background-color: #aeae18; } }
.anchored-element { display: flex; flex-direction: column; align-items: center; justify-content: center; inline-size: 120px; block-size: 120px; color: white; text-align: center; background-color: red; border-radius: 1em;
&::after { content: ""; filter: invert(); }
p { margin: 0; } }
.anchor-element-label { z-index: 2; color: var(--sb-color-text); pointer-events: none; background-color: var(--sb-color-black); }
sv-anchor:state(--top) .anchored-element { margin-block-end: 1em;7 collapsed lines
background-color: #ff5733;
&::after { content: "Top"; } }
sv-anchor:state(--bottom) .anchored-element { margin-block-start: 1em;7 collapsed lines
background-color: #1fba3b;
&::after { content: "Bottom"; } }
sv-anchor:state(--left) .anchored-element { margin-inline-end: 1em;7 collapsed lines
background-color: #3357ff;
&::after { content: "Left"; } }
sv-anchor:state(--right) .anchored-element { margin-inline-start: 1em;7 collapsed lines
background-color: #ff33a8;
&::after { content: "Right"; } }
sv-anchor:state(--misaligned) .anchored-element { display: none;2 collapsed lines
} }</style>