Skip to content

Anchor Interactive

Use autoupdate to reposition anchored content as its anchor moves.

Anchor Element

Anchored

Code

The following example assumes that you have imported the component and set up the theme.

Anchor
<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>