05TikTok embed mock — a card that recomputes its like count from elapsed time
The time since the card appeared is measured at fixed intervals, and at each interval the like count is recomputed by adding a small increment, giving the vertical video mock a sense of life.
Best forInfluencer profile pages, curated content listings
writing-modeobject-fitwhite-space

<main class="stage">
<div class="card">
<div class="left">
<div class="thumbWrap" id="thumbWrap">
<canvas class="thumb" id="thumb" width="132" height="176"></canvas>
<div class="flash" id="flash" aria-hidden="true"></div>
<div class="burst" id="burst" aria-hidden="true">
<svg class="burstIcon" viewBox="0 0 256 256"><g fill="currentColor">
<path d="M232 102c0 66-104 122-104 122S24 168 24 102a54 54 0 0 1 54-54c22.59 0 41.94 12.31 50 32c8.06-19.69 27.41-32 50-32a54 54 0 0 1 54 54" opacity=".2"/>
<path d="M178 40c-20.65 0-38.73 8.88-50 23.89C116.73 48.88 98.65 40 78 40a62.07 62.07 0 0 0-62 62c0 70 103.79 126.66 108.21 129a8 8 0 0 0 7.58 0C136.21 228.66 240 172 240 102a62.07 62.07 0 0 0-62-62m-50 174.8c-18.26-10.64-96-59.11-96-112.8a46.06 46.06 0 0 1 46-46c19.45 0 35.78 10.36 42.6 27a8 8 0 0 0 14.8 0c6.82-16.67 23.15-27 42.6-27a46.06 46.06 0 0 1 46 46c0 53.61-77.76 102.15-96 112.8"/>
</g></svg>
</div>
</div>
<p class="caption">@channelA · original sound</p>
</div>
<div class="rail">
<div class="heartWrap" id="heartWrap">
<svg class="heartIcon" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor">
<path d="M232 102c0 66-104 122-104 122S24 168 24 102a54 54 0 0 1 54-54c22.59 0 41.94 12.31 50 32c8.06-19.69 27.41-32 50-32a54 54 0 0 1 54 54" opacity=".2"/>
<path d="M178 40c-20.65 0-38.73 8.88-50 23.89C116.73 48.88 98.65 40 78 40a62.07 62.07 0 0 0-62 62c0 70 103.79 126.66 108.21 129a8 8 0 0 0 7.58 0C136.21 228.66 240 172 240 102a62.07 62.07 0 0 0-62-62m-50 174.8c-18.26-10.64-96-59.11-96-112.8a46.06 46.06 0 0 1 46-46c19.45 0 35.78 10.36 42.6 27a8 8 0 0 0 14.8 0c6.82-16.67 23.15-27 42.6-27a46.06 46.06 0 0 1 46 46c0 53.61-77.76 102.15-96 112.8"/>
</g></svg>
</div>
<span class="likeCount" id="likeCount">1,247</span>
<span class="railLabel">Likes</span>
</div>
</div>
<div class="readout" id="readout">Interval 0.4s · likes 1,247</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 세로형 카드 임베드 — 좋아요 계단 카운터(계산 겹)와 하트 팝은 JS 계산 겹의 재료.
// CSS keyframes 는 두지 않는다 — 전부 rAF 실시간 루프.
@include stage($stage-ink);
.stage {
overflow: hidden;
}
.card {
display: flex;
align-items: stretch;
gap: $sp-3;
padding: $sp-3;
border-radius: $r-card;
background: $subject-cream;
box-shadow: $shadow-raised;
--thumb-w: 120px;
}
.left {
display: flex;
flex-direction: column;
gap: $sp-2;
min-width: 0;
width: var(--thumb-w, 120px);
flex-shrink: 0;
}
.thumbWrap {
position: relative;
width: var(--thumb-w, 120px);
aspect-ratio: 3 / 4;
border-radius: $r-sm;
overflow: hidden;
}
.thumb {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.flash {
position: absolute;
inset: 0;
background: $subject-cream;
opacity: 0;
pointer-events: none;
}
.burst {
position: absolute;
top: 50%;
left: 50%;
width: 46px;
height: 46px;
color: $subject-cream;
opacity: 0;
pointer-events: none;
}
.burstIcon {
width: 100%;
height: 100%;
display: block;
filter: drop-shadow(0 2px 6px rgba(23, 20, 26, .5));
}
.caption {
margin: 0;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 600;
font-size: 11px;
line-height: 1.4;
color: $subject-ink;
overflow: hidden;
text-overflow: clip;
white-space: nowrap;
}
.rail {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: $sp-1;
width: 40px;
flex-shrink: 0;
}
.heartWrap {
width: 24px;
height: 24px;
display: block;
}
.heartIcon {
width: 100%;
height: 100%;
display: block;
}
.heartIcon path {
fill: $subject-pink;
}
.likeCount {
font-family: "Space Mono", ui-monospace, monospace;
font-weight: 700;
font-size: 11px;
line-height: 1.2;
color: $subject-ink;
}
.railLabel {
writing-mode: vertical-rl;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 600;
font-size: 9px;
line-height: 1;
color: $subject-ink;
opacity: .72;
}
.readout {
position: relative;
margin-top: $sp-3;
display: inline-flex;
align-items: center;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: $subject-blue-deep;
font-family: "Space Mono", ui-monospace, monospace;
font-weight: 700;
font-size: 11px;
line-height: 1.3;
color: $subject-cream;
}
@media (max-width: 340px) {
.card {
padding: $sp-2;
gap: $sp-2;
--thumb-w: 56px;
}
.rail {
width: 30px;
}
.heartWrap {
width: 20px;
height: 20px;
}
.caption {
font-size: 10px;
}
.likeCount {
font-size: 10px;
}
.railLabel {
font-size: 8px;
}
.readout {
font-size: 10px;
padding: $sp-1 $sp-2;
margin-top: $sp-2;
}
}
@media (prefers-reduced-motion: reduce) {
.burst,
.flash {
opacity: 0;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
overflow: hidden;
}
.card {
display: flex;
align-items: stretch;
gap: 12px;
padding: 12px;
border-radius: 16px;
background: #fff7e6;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
--thumb-w: 120px;
}
.left {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
width: var(--thumb-w, 120px);
flex-shrink: 0;
}
.thumbWrap {
position: relative;
width: var(--thumb-w, 120px);
aspect-ratio: 3/4;
border-radius: 8px;
overflow: hidden;
}
.thumb {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.flash {
position: absolute;
inset: 0;
background: #fff7e6;
opacity: 0;
pointer-events: none;
}
.burst {
position: absolute;
top: 50%;
left: 50%;
width: 46px;
height: 46px;
color: #fff7e6;
opacity: 0;
pointer-events: none;
}
.burstIcon {
width: 100%;
height: 100%;
display: block;
filter: drop-shadow(0 2px 6px rgba(23, 20, 26, 0.5));
}
.caption {
margin: 0;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 600;
font-size: 11px;
line-height: 1.4;
color: #17141a;
overflow: hidden;
text-overflow: clip;
white-space: nowrap;
}
.rail {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
width: 40px;
flex-shrink: 0;
}
.heartWrap {
width: 24px;
height: 24px;
display: block;
}
.heartIcon {
width: 100%;
height: 100%;
display: block;
}
.heartIcon path {
fill: #ff9ebb;
}
.likeCount {
font-family: "Space Mono", ui-monospace, monospace;
font-weight: 700;
font-size: 11px;
line-height: 1.2;
color: #17141a;
}
.railLabel {
writing-mode: vertical-rl;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 600;
font-size: 9px;
line-height: 1;
color: #17141a;
opacity: 0.72;
}
.readout {
position: relative;
margin-top: 12px;
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 999px;
background: #0a4fe8;
font-family: "Space Mono", ui-monospace, monospace;
font-weight: 700;
font-size: 11px;
line-height: 1.3;
color: #fff7e6;
}
@media (max-width: 340px) {
.card {
padding: 8px;
gap: 8px;
--thumb-w: 56px;
}
.rail {
width: 30px;
}
.heartWrap {
width: 20px;
height: 20px;
}
.caption {
font-size: 10px;
}
.likeCount {
font-size: 10px;
}
.railLabel {
font-size: 8px;
}
.readout {
font-size: 10px;
padding: 4px 8px;
margin-top: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.burst,
.flash {
opacity: 0;
}
}// 세로형 카드 임베드(05) — 좋아요는 상수 간격마다 계단으로 오르고, 오른 순간 하트가 튕긴다.
// 엄지가 두 번 두드릴 때 나오는 큰 하트도 같은 간격으로 썸네일 위에서 커지며 사라진다.
// 실키·실서버 요청 없음 — 좌표·수치는 고정 시나리오로 자동 재생한다.
(function () {
function mulberry32(seed) {
var a = seed >>> 0;
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function drawSeedThumb(canvas, seed) {
var ctx = canvas.getContext('2d');
if (!ctx) return;
var rnd = mulberry32(seed);
var w = canvas.width, h = canvas.height;
var hues = [216, 32, 268, 168];
ctx.fillStyle = 'hsl(' + hues[Math.floor(rnd() * hues.length)] + ', 38%, 22%)';
ctx.fillRect(0, 0, w, h);
for (var i = 0; i < 6; i++) {
var cx = rnd() * w, cy = rnd() * h, r = (0.18 + rnd() * 0.3) * Math.min(w, h);
ctx.fillStyle = 'hsla(' + hues[Math.floor(rnd() * hues.length)] + ', ' + (44 + rnd() * 22).toFixed(1) + '%, ' + (38 + rnd() * 26).toFixed(1) + '%, .5)';
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.fill();
}
ctx.fillStyle = 'rgba(255, 247, 230, .06)';
for (var j = 0; j < h; j += 4) ctx.fillRect(0, j, w, 1);
}
var thumbEl = document.getElementById('thumb');
var flashEl = document.getElementById('flash');
var burstEl = document.getElementById('burst');
var heartWrapEl = document.getElementById('heartWrap');
var likeCountEl = document.getElementById('likeCount');
var readoutEl = document.getElementById('readout');
if (!thumbEl || !flashEl || !burstEl || !heartWrapEl || !likeCountEl || !readoutEl) return;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
drawSeedThumb(thumbEl, 105);
var CYCLE_MS = 2000;
var LIKE_INTERVAL = 400;
var LIKE_STEP = 3;
var LIKE_BASE = 1247;
function apply(t) {
// 계산 겹: 간격마다 Likes 수를 다시 계산 — 트윈이 아니라 계단식 카운터
var likes = LIKE_BASE + Math.floor(t / LIKE_INTERVAL) * LIKE_STEP;
var since = t % LIKE_INTERVAL;
var pop = since < 180 ? 1 + 0.28 * (1 - since / 180) : 1;
heartWrapEl.style.transform = 'scale(' + pop.toFixed(3) + ')';
likeCountEl.textContent = likes.toLocaleString('en-US');
readoutEl.textContent = 'Interval ' + (LIKE_INTERVAL / 1000).toFixed(1) + 's · likes ' + likes.toLocaleString('en-US');
// 두 번 두드림 하트 — 같은 간격에서 0→1로 커지며 옅어진다(썸네일 위 큰 겹)
var prog = since / LIKE_INTERVAL;
var burstScale = 0.3 + prog * 2.3;
var burstOpacity = Math.max(0, 0.85 * (1 - prog * 1.15));
burstEl.style.transform = 'translate(-50%, -50%) scale(' + burstScale.toFixed(3) + ')';
burstEl.style.opacity = burstOpacity.toFixed(3);
// 좋아요가 오르는 순간 썸네일 전체가 짧게 밝아진다(플래시) — 하트 겹과 같은 since 를 공유
var flashOpacity = since < 160 ? 0.4 * (1 - since / 160) : 0;
flashEl.style.opacity = flashOpacity.toFixed(3);
}
if (reduce) {
apply(0);
return;
}
var start = null;
function loop(ts) {
if (start === null) start = ts;
var t = (ts - start) % CYCLE_MS;
apply(t);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();import { useEffect, useRef } from "react";
import styles from "./EmbedTiktok.module.scss";
// 세로형 카드 임베드 — 좋아요는 상수 간격마다 계단으로 오르고, 오른 순간 하트가 튕긴다.
// 두 번 두드림 하트도 같은 간격으로 썸네일 위에서 커지며 사라진다. 실키·실서버 요청 없음.
const CYCLE_MS = 2000;
const LIKE_INTERVAL = 400;
const LIKE_STEP = 3;
const LIKE_BASE = 1247;
function mulberry32(seed: number) {
let a = seed >>> 0;
return function () {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function drawSeedThumb(canvas: HTMLCanvasElement, seed: number) {
const ctx = canvas.getContext("2d");
if (!ctx) return;
const rnd = mulberry32(seed);
const w = canvas.width;
const h = canvas.height;
const hues = [216, 32, 268, 168];
ctx.fillStyle = "hsl(" + hues[Math.floor(rnd() * hues.length)] + ", 38%, 22%)";
ctx.fillRect(0, 0, w, h);
for (let i = 0; i < 6; i++) {
const cx = rnd() * w;
const cy = rnd() * h;
const r = (0.18 + rnd() * 0.3) * Math.min(w, h);
ctx.fillStyle =
"hsla(" + hues[Math.floor(rnd() * hues.length)] + ", " + (44 + rnd() * 22).toFixed(1) + "%, " + (38 + rnd() * 26).toFixed(1) + "%, .5)";
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = "rgba(255, 247, 230, .06)";
for (let j = 0; j < h; j += 4) ctx.fillRect(0, j, w, 1);
}
export default function EmbedTiktok() {
const thumbRef = useRef<HTMLCanvasElement>(null);
const flashRef = useRef<HTMLDivElement>(null);
const burstRef = useRef<HTMLDivElement>(null);
const heartWrapRef = useRef<HTMLDivElement>(null);
const likeCountRef = useRef<HTMLSpanElement>(null);
const readoutRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const thumbEl = thumbRef.current;
const flashEl = flashRef.current;
const burstEl = burstRef.current;
const heartWrapEl = heartWrapRef.current;
const likeCountEl = likeCountRef.current;
const readoutEl = readoutRef.current;
if (!thumbEl || !flashEl || !burstEl || !heartWrapEl || !likeCountEl || !readoutEl) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
drawSeedThumb(thumbEl, 105);
function apply(t: number) {
// 계산 겹: 간격마다 좋아요 수를 다시 계산 — 트윈이 아니라 계단식 카운터
const likes = LIKE_BASE + Math.floor(t / LIKE_INTERVAL) * LIKE_STEP;
const since = t % LIKE_INTERVAL;
const pop = since < 180 ? 1 + 0.28 * (1 - since / 180) : 1;
heartWrapEl!.style.transform = "scale(" + pop.toFixed(3) + ")";
likeCountEl!.textContent = likes.toLocaleString("en-US");
readoutEl!.textContent = "간격 " + (LIKE_INTERVAL / 1000).toFixed(1) + "초 · 좋아요 " + likes.toLocaleString("en-US");
// 두 번 두드림 하트 — 같은 간격에서 0→1로 커지며 옅어진다(썸네일 위 큰 겹)
const prog = since / LIKE_INTERVAL;
const burstScale = 0.3 + prog * 2.3;
const burstOpacity = Math.max(0, 0.85 * (1 - prog * 1.15));
burstEl!.style.transform = "translate(-50%, -50%) scale(" + burstScale.toFixed(3) + ")";
burstEl!.style.opacity = burstOpacity.toFixed(3);
// 좋아요가 오르는 순간 썸네일 전체가 짧게 밝아진다(플래시) — 하트 겹과 같은 since 를 공유
const flashOpacity = since < 160 ? 0.4 * (1 - since / 160) : 0;
flashEl!.style.opacity = flashOpacity.toFixed(3);
}
if (reduce) {
apply(0);
return;
}
let start: number | null = null;
let raf = 0;
function loop(ts: number) {
if (start === null) start = ts;
const t = (ts - start) % CYCLE_MS;
apply(t);
raf = requestAnimationFrame(loop);
}
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, []);
return (
<div className={styles.stage}>
<div className={styles.card}>
<div className={styles.left}>
<div className={styles.thumbWrap}>
<canvas className={styles.thumb} ref={thumbRef} width={132} height={176} />
<div className={styles.flash} ref={flashRef} aria-hidden="true" />
<div className={styles.burst} ref={burstRef} aria-hidden="true">
<svg className={styles.burstIcon} viewBox="0 0 256 256">
<g fill="currentColor">
<path
d="M232 102c0 66-104 122-104 122S24 168 24 102a54 54 0 0 1 54-54c22.59 0 41.94 12.31 50 32c8.06-19.69 27.41-32 50-32a54 54 0 0 1 54 54"
opacity=".2"
/>
<path d="M178 40c-20.65 0-38.73 8.88-50 23.89C116.73 48.88 98.65 40 78 40a62.07 62.07 0 0 0-62 62c0 70 103.79 126.66 108.21 129a8 8 0 0 0 7.58 0C136.21 228.66 240 172 240 102a62.07 62.07 0 0 0-62-62m-50 174.8c-18.26-10.64-96-59.11-96-112.8a46.06 46.06 0 0 1 46-46c19.45 0 35.78 10.36 42.6 27a8 8 0 0 0 14.8 0c6.82-16.67 23.15-27 42.6-27a46.06 46.06 0 0 1 46 46c0 53.61-77.76 102.15-96 112.8" />
</g>
</svg>
</div>
</div>
<p className={styles.caption}>@channelA · 오리지널 사운드</p>
</div>
<div className={styles.rail}>
<div className={styles.heartWrap} ref={heartWrapRef}>
<svg className={styles.heartIcon} viewBox="0 0 256 256" aria-hidden="true">
<g fill="currentColor">
<path
d="M232 102c0 66-104 122-104 122S24 168 24 102a54 54 0 0 1 54-54c22.59 0 41.94 12.31 50 32c8.06-19.69 27.41-32 50-32a54 54 0 0 1 54 54"
opacity=".2"
/>
<path d="M178 40c-20.65 0-38.73 8.88-50 23.89C116.73 48.88 98.65 40 78 40a62.07 62.07 0 0 0-62 62c0 70 103.79 126.66 108.21 129a8 8 0 0 0 7.58 0C136.21 228.66 240 172 240 102a62.07 62.07 0 0 0-62-62m-50 174.8c-18.26-10.64-96-59.11-96-112.8a46.06 46.06 0 0 1 46-46c19.45 0 35.78 10.36 42.6 27a8 8 0 0 0 14.8 0c6.82-16.67 23.15-27 42.6-27a46.06 46.06 0 0 1 46 46c0 53.61-77.76 102.15-96 112.8" />
</g>
</svg>
</div>
<span className={styles.likeCount} ref={likeCountRef}>
1,247
</span>
<span className={styles.railLabel}>좋아요</span>
</div>
</div>
<div className={styles.readout} ref={readoutRef}>
간격 0.4초 · 좋아요 1,247
</div>
</div>
);
}05 TikTok embed mock — a card that recomputes its like count from elapsed time — Measured area 13.598% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · 9 YouTube Embed Code UI Patterns (Copy-Paste)
The post this demo lives in
9 YouTube Embed Code UI Patterns (Copy-Paste)Nine embed UIs: a frame's height recomputes from its width, a lite facade times itself into a full frame after a delay, See all demos in this post


