Drag with can-move
can-move makes an element draggable, persistent, and shared. Add a stable
id, then use can-move-bounds when the element must stay inside a container.
- Keep this page open in your normal window.
- Copy the private-window link, then paste it into a private or incognito window.
- Interact in either window and watch the other one update.
Copy the code
Both versions use the same shared data and behavior. The live playground runs the Vanilla HTML version.
Save this as index.html, or open it in the playground to
test and change it.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Drag with can-move</title>
<style>
:root {
color: #1c1c1c;
background: #f4efe5;
font-family: ui-sans-serif, system-ui, sans-serif;
}
* { box-sizing: border-box; }
body { display: grid; min-height: 100vh; margin: 0; padding: 1.5rem; place-items: center; }
main { width: min(36rem, 100%); }
h1 { margin: 0 0 0.35rem; font-size: clamp(2rem, 8vw, 3.5rem); line-height: 1; }
.intro { margin: 0 0 1.25rem; line-height: 1.5; }
.arena {
position: relative;
height: 20rem;
overflow: hidden;
border: 2px solid #1c1c1c;
background: #dce8cf;
box-shadow: 5px 5px 0 #1c1c1c;
}
.piece {
position: absolute;
display: grid;
width: 8rem;
height: 8rem;
place-items: center;
font-size: 6rem;
touch-action: none;
user-select: none;
}
.hat { top: 2rem; left: 2rem; }
.cat { right: 2rem; bottom: 1.5rem; }
</style>
</head>
<body>
<main>
<h1>Drag together</h1>
<p class="intro">Move either sticker. Its position updates in every connected browser.</p>
<div id="move-arena" class="arena">
<div
id="move-hat"
class="piece hat"
can-move
can-move-bounds="move-arena"
role="img"
aria-label="A draggable baseball cap"
>🧢</div>
<div
id="move-cat"
class="piece cat"
can-move
can-move-bounds="move-arena"
role="img"
aria-label="A draggable cat"
>🐈</div>
</div>
</main>
<script type="module">
import { playhtml } from "playhtml";
// Built-in capabilities need no custom handlers.
await playhtml.init({ developmentMode: true });
</script>
</body>
</html>
Start with a React + TypeScript project, install the packages below,
then replace src/App.tsx with the component.
npm install playhtml @playhtml/react // ABOUTME: Constrains two shared draggable stickers to one arena.
// ABOUTME: Uses stable ids so positions persist and synchronize across browsers.
import { PlayProvider, CanMoveElement } from "@playhtml/react";
const ARENA_ID = "move-arena";
export default function App() {
return (
<PlayProvider initOptions={{ developmentMode: true }}>
<main>
<h1>Drag together</h1>
<p>Move either sticker. Its position updates in every connected browser.</p>
<div id={ARENA_ID} className="arena">
<CanMoveElement bounds={ARENA_ID}>
<div
id="move-hat"
className="piece hat"
role="img"
aria-label="A draggable baseball cap"
>
🧢
</div>
</CanMoveElement>
<CanMoveElement bounds={ARENA_ID}>
<div
id="move-cat"
className="piece cat"
role="img"
aria-label="A draggable cat"
>
🐈
</div>
</CanMoveElement>
</div>
</main>
<style>{`
:root { color: #1c1c1c; background: #f4efe5; font-family: system-ui, sans-serif; }
* { box-sizing: border-box; }
body { margin: 0; }
#root { display: grid; min-height: 100vh; padding: 1.5rem; place-items: center; }
main { width: min(38rem, 100%); }
h1 { margin: 0 0 0.35rem; font-size: clamp(2rem, 8vw, 3.5rem); }
p { margin: 0 0 1rem; line-height: 1.5; }
.arena {
position: relative;
height: 20rem;
overflow: hidden;
border: 2px solid #1c1c1c;
background: #dce8cf;
box-shadow: 5px 5px 0 #1c1c1c;
}
.piece {
position: absolute;
display: grid;
width: 8rem;
height: 8rem;
place-items: center;
font-size: 6rem;
touch-action: none;
user-select: none;
}
.hat { top: 2rem; left: 2rem; }
.cat { right: 2rem; bottom: 1.5rem; }
`}</style>
</PlayProvider>
);
} Keep elements inside a container
Section titled “Keep elements inside a container”Set can-move-bounds to the container’s id:
<div id="move-arena">
<div
id="move-hat"
can-move
can-move-bounds="move-arena"
role="img"
aria-label="A draggable baseball cap"
>🧢</div>
</div>
PlayHTML saves each element’s position under its own id. Reloading the page
or opening the same room in another browser restores that position.
Test the shared position
Section titled “Test the shared position”Drag either sticker, then open the private-window link. Both windows should show the same positions. Dragging in either window updates the other.
See can-move for partial-overhang controls and the complete attribute reference.