Skip to content

Instantly share code, notes, and snippets.

@nikfox3
Last active July 20, 2025 21:22
Show Gist options
  • Save nikfox3/e270d4fec67ad5ec194e1d0ac06b101d to your computer and use it in GitHub Desktop.
Save nikfox3/e270d4fec67ad5ec194e1d0ac06b101d to your computer and use it in GitHub Desktop.
Sticker Component
<svg height="0" width="0">
<defs>
<filter id=pointLight>
<feGaussianBlur stdDeviation="1" result="blur" />
<feSpecularLighting result="spec" in="blur" specularExponent="100" specularConstant="0.1" lightingColor="white">
<fePointLight x=100 y=100 z="300" />
</feSpecularLighting>
<feComposite in="spec" in2="SourceGraphic" operator="screen" result="lit" />
<feComposite in="lit" in2="SourceAlpha" operator="in" />
</filter>
<filter id=pointLightFlipped>
<feGaussianBlur stdDeviation="10" result="blur" />
<feSpecularLighting result="spec" in="blur" specularExponent="100" specularConstant="0.7" lightingColor="white">
<fePointLight x=100 y=100 z="300" id="fePointLightFlipped" />
</feSpecularLighting>
<feComposite in="spec" in2="SourceGraphic" operator="screen" result="lit" />
<feComposite in="lit" in2="SourceAlpha" operator="in" />
</filter>
<filter id="dropShadow">
<feDropShadow dx="2" dy="4" stdDeviation="3" flood-color="black" flood-opacity="0.6" />
</filter>
<!-- STROKE EFFECT (1/2) -->
<!-- Uncomment the outerStroke filter to add a stroke effect -->
<!-- Set the stroke size by changing the radius value in feMorphology -->
<!-- For strokes to work, you need to update expandAndFill filter too -->
<!-- <filter id="outerStroke">
<feMorphology operator="dilate" radius="10" in="SourceAlpha" result="expanded" />
<feColorMatrix in="expanded" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
result="whiteExpanded" />
<feComposite operator="xor" in="whiteExpanded" in2="SourceAlpha" result="outerStroke" />
<feComposite operator="over" in="SourceGraphic" in2="outerStroke" />
</filter> -->
<filter id="expandAndFill">
<feOffset dx="0" dy="0" in="SourceAlpha" result="shape" />
<!-- STROKE EFFECT (2/2) -->
<!-- Uncomment the feMorphology filter to add a stroke effect -->
<!-- <feMorphology operator="dilate" radius="10" in="SourceAlpha" result="shape" /> -->
<feFlood flood-color="rgb(179, 179, 179)" result="flood" />
<feComposite operator="in" in="flood" in2="shape" />
</filter>
</defs>
</svg>
<style>
</style>
<main style="width: 100dvw; height: 100dvh; display: flex; justify-content: center; align-items: center;">
<div class="draggable">
<div class="sticker-container">
<div class="sticker-main">
<div class="sticker-lighting">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/Figma-logo.svg/256px-Figma-logo.svg.png"
class="sticker-image"
draggable="false"
oncontextmenu="return false" />
<div class="shadow">
<div class="flap">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/Figma-logo.svg/256px-Figma-logo.svg.png"
class="shadow-image"
draggable="false"
oncontextmenu="return false" />
</div>
</div>
<div class="flap">
<div class="flap-lighting">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/Figma-logo.svg/256px-Figma-logo.svg.png"
class="flap-image"
draggable="false"
oncontextmenu="return false" />
</div>
</div>
</div>
</div>
</main>
const pointLight = document.querySelector("fePointLight");
const pointLightFlipped = document.getElementById("fePointLightFlipped");
const stickerContainer = document.querySelector(".sticker-container");
const draggable = document.querySelector(".draggable");
let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
function getContainingBlockOffset(element) {
let containingBlock = element.offsetParent || document.documentElement;
const containingBlockRect = containingBlock.getBoundingClientRect();
return {
left: containingBlockRect.left,
top: containingBlockRect.top
};
}
function updateLightPosition(mouseX, mouseY) {
const rect = stickerContainer.getBoundingClientRect();
const relativeX = mouseX - rect.left;
const relativeY = mouseY - rect.top;
pointLight.setAttribute("x", relativeX);
pointLight.setAttribute("y", relativeY);
pointLightFlipped.setAttribute("x", relativeX);
pointLightFlipped.setAttribute("y", rect.height - relativeY);
}
function startDrag(e) {
isDragging = true;
const rect = draggable.getBoundingClientRect();
const containingBlockOffset = getContainingBlockOffset(draggable);
dragOffsetX = e.clientX - rect.left;
dragOffsetY = e.clientY - rect.top;
draggable.style.left = rect.left - containingBlockOffset.left + "px";
draggable.style.top = rect.top - containingBlockOffset.top + "px";
}
function updateDragPosition(e) {
if (isDragging) {
const containingBlockOffset = getContainingBlockOffset(draggable);
draggable.style.left =
e.clientX - dragOffsetX - containingBlockOffset.left + "px";
draggable.style.top =
e.clientY - dragOffsetY - containingBlockOffset.top + "px";
}
}
function stopDrag() {
isDragging = false;
}
draggable.addEventListener("mousedown", startDrag);
document.addEventListener("mouseup", stopDrag);
document.addEventListener("mousemove", (e) => {
updateLightPosition(e.clientX, e.clientY);
updateDragPosition(e);
});
:root {
/* CONFIGURATION */
--sticker-rotate: 10deg;
/** If the image is clipped after rotation,
/* increase this value! **/
--sticker-p: 50px;
--sticker-peelback-hover: 10%;
--sticker-peelback-active: 50%;
--sticker-peel-easing: 2s
linear(
0,
0.002 0.4%,
0.008 0.9%,
0.02 1.4%,
0.035 1.9%,
0.055 2.4%,
0.083 3%,
0.11 3.5%,
0.146 4.1%,
0.214 5.1%,
0.297 6.2%,
0.624 10.2%,
0.756 11.9%,
0.821 12.8%,
0.874 13.6%,
0.93 14.5%,
0.975 15.3%,
1.016 16.1%,
1.053 16.9%,
1.085 17.7%,
1.116 18.6%,
1.139 19.4%,
1.16 20.3%,
1.176 21.2%,
1.187 22.1%,
1.195 23.2%,
1.197 24.4%,
1.193 25.6%,
1.183 26.9%,
1.17 28.1%,
1.153 29.4%,
1.055 35.6%,
1.031 37.3%,
1.012 38.8%,
0.994 40.6%,
0.98 42.3%,
0.97 44.1%,
0.964 45.9%,
0.961 48.3%,
0.964 51.1%,
0.97 53.7%,
0.997 62.7%,
1.003 66%,
1.007 69.3%,
1.007 74.4%,
1 89.2%,
1
);
--sticker-peel-hover-easing: 1s
linear(
0,
0.008 1.1%,
0.031 2.2%,
0.129 4.8%,
0.257 7.2%,
0.671 14.2%,
0.789 16.5%,
0.881 18.6%,
0.957 20.7%,
1.019 22.9%,
1.063 25.1%,
1.094 27.4%,
1.114 30.7%,
1.112 34.5%,
1.018 49.9%,
0.99 59.1%,
1
);
/* HELPERS */
--sticker-start: calc(-1 * var(--sticker-p));
--sticker-end: calc(100% + var(--sticker-p));
}
body {
padding: 0;
margin: 0;
background-image: linear-gradient(to bottom, #302f31, #1a1a1a);
}
.sticker-container {
position: relative;
}
.sticker-container * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
.sticker-main {
clip-path: polygon(
var(--sticker-start) var(--sticker-start),
var(--sticker-end) var(--sticker-start),
var(--sticker-end) var(--sticker-end),
var(--sticker-start) var(--sticker-end)
);
transition: clip-path var(--sticker-peel-hover-easing);
filter: url(#dropShadow);
}
.sticker-lighting {
filter: url(#pointLight);
}
.sticker-container:hover .sticker-main {
clip-path: polygon(
var(--sticker-start) var(--sticker-peelback-hover),
var(--sticker-end) var(--sticker-peelback-hover),
var(--sticker-end) var(--sticker-end),
var(--sticker-start) var(--sticker-end)
);
transition: clip-path var(--sticker-peel-hover-easing);
}
.sticker-container:active .sticker-main {
clip-path: polygon(
var(--sticker-start) var(--sticker-peelback-active),
var(--sticker-end) var(--sticker-peelback-active),
var(--sticker-end) var(--sticker-end),
var(--sticker-start) var(--sticker-end)
);
transition: clip-path var(--sticker-peel-easing);
}
.sticker-image {
transform: rotate(var(--sticker-rotate));
filter: url(#outerStroke);
}
.flap {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: calc(-100% - var(--sticker-p) - var(--sticker-p));
clip-path: polygon(
var(--sticker-start) var(--sticker-start),
var(--sticker-end) var(--sticker-start),
var(--sticker-end) var(--sticker-start),
var(--sticker-start) var(--sticker-start)
);
transform: scaleY(-1);
transition: all var(--sticker-peel-hover-easing);
}
.sticker-container:hover .flap {
clip-path: polygon(
var(--sticker-start) var(--sticker-start),
var(--sticker-end) var(--sticker-start),
var(--sticker-end) var(--sticker-peelback-hover),
var(--sticker-start) var(--sticker-peelback-hover)
);
top: calc(-100% + 2 * var(--sticker-peelback-hover) - 1px);
transition: all var(--sticker-peel-hover-easing);
}
.sticker-container:active .flap {
clip-path: polygon(
var(--sticker-start) var(--sticker-start),
var(--sticker-end) var(--sticker-start),
var(--sticker-end) var(--sticker-peelback-active),
var(--sticker-start) var(--sticker-peelback-active)
);
top: calc(-100% + 2 * var(--sticker-peelback-active) - 1px);
transition: all var(--sticker-peel-easing);
}
.flap-lighting {
filter: url(#pointLightFlipped);
}
.flap-image,
.shadow-image {
transform: rotate(var(--sticker-rotate));
filter: url(#expandAndFill);
}
.shadow {
position: absolute;
top: 1rem;
left: 0.5rem;
width: 100%;
height: 100%;
filter: brightness(0) blur(8px);
opacity: 0.4;
}
.sticker-main,
.flap {
will-change: clip-path, transform;
}
.draggable {
position: absolute;
cursor: grab;
}
.draggable:active {
cursor: grabbing;
}
.sticker-image,
.shadow-image,
.flap-image {
width: 200px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment