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: 1rem; place-items: center; }
main { width: min(40rem, 100%); }
.arena {
position: relative;
width: 100%;
height: min(14.75rem, calc(100vh - 2rem));
overflow: hidden;
border: 1.5px solid #1c1c1c;
border-radius: 8px;
background: #f1f2e9;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.35);
}
.piece {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
border: 0;
background: transparent;
box-shadow: none;
cursor: grab;
touch-action: none;
user-select: none;
}
.piece:active { cursor: grabbing; }
.hat { width: 4.5rem; height: 4.5rem; }
.cat { width: 5.25rem; height: 9.5rem; }
.piece img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none;
}
</style>
</head>
<body>
<main>
<div id="ph-cap-move-arena" class="arena" aria-label="Drag the hat and cat">
<div
id="ph-cap-hat"
class="piece hat"
can-move
can-move-bounds="ph-cap-move-arena"
>
<img src="https://playhtml.fun/docs/yankees-hat.png" alt="" draggable="false" />
</div>
<div
id="ph-cap-cat"
class="piece cat"
can-move
can-move-bounds="ph-cap-move-arena"
>
<img src="https://playhtml.fun/docs/long-cat.png" alt="" draggable="false" />
</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 images to one arena.
// ABOUTME: Uses stable ids so positions persist and synchronize across browsers.
import { PlayProvider, CanMoveElement } from "@playhtml/react";
const ARENA_ID = "ph-cap-move-arena";
export default function App() {
return (
<PlayProvider initOptions={{ developmentMode: true }}>
<main>
<div id={ARENA_ID} className="arena" aria-label="Drag the hat and cat">
<CanMoveElement bounds={ARENA_ID}>
<div id="ph-cap-hat" className="piece hat">
<img src="https://playhtml.fun/docs/yankees-hat.png" alt="" draggable={false} />
</div>
</CanMoveElement>
<CanMoveElement bounds={ARENA_ID}>
<div id="ph-cap-cat" className="piece cat">
<img src="https://playhtml.fun/docs/long-cat.png" alt="" draggable={false} />
</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: 1rem; place-items: center; }
main { width: min(40rem, 100%); }
.arena {
position: relative;
width: 100%;
height: min(14.75rem, calc(100vh - 2rem));
overflow: hidden;
border: 1.5px solid #1c1c1c;
border-radius: 8px;
background: #f1f2e9;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.35);
}
.piece {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
border: 0;
background: transparent;
cursor: grab;
touch-action: none;
user-select: none;
}
.piece:active { cursor: grabbing; }
.hat { width: 4.5rem; height: 4.5rem; }
.cat { width: 5.25rem; height: 9.5rem; }
.piece img { display: block; width: 100%; height: 100%; object-fit: contain; pointer-events: none; }
`}</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="ph-cap-move-arena">
<div
id="ph-cap-hat"
can-move
can-move-bounds="ph-cap-move-arena"
>
<img src="/docs/yankees-hat.png" alt="" />
</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.