Editor Toolkit
ef-transform-handles Tutorial
This guide walks through using ef-transform-handles to add interactive transform controls to elements. Each step builds on the previous one.
Step 1: Basic Drag and Resize
Attach handles to a positioned element to enable dragging and resizing.
<div class="relative w-full h-[400px] bg-gray-100 border-2 border-gray-200 rounded"><div id="th-target-1" class="absolute bg-blue-500 rounded-lg" style="left:100px;top:100px;width:200px;height:150px;"></div><ef-transform-handles target="#th-target-1"></ef-transform-handles></div>
Drag the border to move the element, or drag the corner and edge handles to resize it. The handles automatically track the element's position and size.
Step 2: Responding to Transform Events
Listen for bounds-change to apply transforms back to your element.
<div class="relative w-full h-[400px] bg-gray-100 border-2 border-gray-200 rounded"><div id="th-target-2" class="absolute bg-blue-500 rounded-lg" style="left:100px;top:100px;width:200px;height:150px;"></div><ef-transform-handles id="th-handles-2" target="#th-target-2"></ef-transform-handles></div><script>const handles = document.getElementById('th-handles-2');const target = document.getElementById('th-target-2');handles.addEventListener('bounds-change', (e) => {const { bounds } = e.detail;target.style.left = bounds.x + 'px';target.style.top = bounds.y + 'px';target.style.width = bounds.width + 'px';target.style.height = bounds.height + 'px';});</script>
The bounds-change event fires continuously during drag and resize. e.detail.bounds contains { x, y, width, height } in screen pixels.
Step 3: Enabling Rotation
Add the enable-rotation attribute to show the rotation handle.
<div class="relative w-full h-[400px] bg-gray-100 border-2 border-gray-200 rounded"><div id="th-target-3" class="absolute bg-blue-500 rounded-lg" style="left:100px;top:100px;width:200px;height:150px;transform-origin:center;"></div><ef-transform-handles id="th-handles-3" target="#th-target-3" enable-rotation></ef-transform-handles></div><script>const handles = document.getElementById('th-handles-3');const target = document.getElementById('th-target-3');handles.addEventListener('bounds-change', (e) => {const { bounds } = e.detail;target.style.left = bounds.x + 'px';target.style.top = bounds.y + 'px';target.style.width = bounds.width + 'px';target.style.height = bounds.height + 'px';});handles.addEventListener('rotation-change', (e) => {target.style.transform = 'rotate(' + e.detail.rotation + 'deg)';});</script>
The green rotation handle appears above the element. rotation-change fires during rotation with e.detail.rotation in degrees.
Step 4: Locking Aspect Ratio
Use lock-aspect-ratio to preserve proportions during resize.
<div class="relative w-full h-[400px] bg-gray-100 border-2 border-gray-200 rounded"><div id="th-target-4" class="absolute bg-purple-500 rounded-lg" style="left:100px;top:100px;width:320px;height:180px;"></div><ef-transform-handles id="th-handles-4" target="#th-target-4" lock-aspect-ratio></ef-transform-handles></div><script>const handles = document.getElementById('th-handles-4');const target = document.getElementById('th-target-4');handles.addEventListener('bounds-change', (e) => {const { bounds } = e.detail;target.style.left = bounds.x + 'px';target.style.top = bounds.y + 'px';target.style.width = bounds.width + 'px';target.style.height = bounds.height + 'px';});</script>
All resize handles maintain the original 16:9 ratio. Hold Shift during resize as an alternative way to lock aspect ratio on the fly.
Step 5: Canvas Scale
When your canvas is zoomed or scaled, provide the current scale so handles render at consistent screen size and hit-test correctly.
<div class="relative w-full h-[400px] bg-gray-100 border-2 border-gray-200 rounded overflow-hidden"><div style="transform:scale(0.5);transform-origin:top left;width:200%;height:200%;position:relative;"><div id="th-target-5" class="absolute bg-green-500 rounded-lg" style="left:200px;top:200px;width:300px;height:200px;"></div><ef-transform-handles id="th-handles-5" target="#th-target-5" canvas-scale="0.5"></ef-transform-handles></div></div><script>const handles = document.getElementById('th-handles-5');const target = document.getElementById('th-target-5');handles.addEventListener('bounds-change', (e) => {const { bounds } = e.detail;target.style.left = bounds.x + 'px';target.style.top = bounds.y + 'px';target.style.width = bounds.width + 'px';target.style.height = bounds.height + 'px';});</script>
Without canvas-scale, the handles would appear at double their intended size inside a 0.5× scaled canvas. When using ef-pan-zoom, the scale is provided automatically via context.
Step 6: Complete Editor Pattern
A full editor maintains element state and applies all transforms together.
<div class="relative w-full h-[400px] bg-gray-100 border-2 border-gray-200 rounded"><div id="th-target-6" class="absolute bg-blue-500 rounded-lg" style="left:100px;top:100px;width:200px;height:150px;transform-origin:center;"></div><ef-transform-handles id="th-handles-6" target="#th-target-6" enable-rotation min-size="50"></ef-transform-handles></div><script>const handles = document.getElementById('th-handles-6');const target = document.getElementById('th-target-6');let state = { x: 100, y: 100, width: 200, height: 150, rotation: 0 };function apply() {target.style.left = state.x + 'px';target.style.top = state.y + 'px';target.style.width = state.width + 'px';target.style.height = state.height + 'px';target.style.transform = 'rotate(' + state.rotation + 'deg)';}handles.addEventListener('bounds-change', (e) => {Object.assign(state, e.detail.bounds);apply();});handles.addEventListener('rotation-change', (e) => {state.rotation = e.detail.rotation;apply();});apply();</script>
This pattern — state object + apply function + event listeners — scales to multi-element editors. To add selection, track which element is selected and render a single ef-transform-handles instance pointing to it.
One-Way Data Flow
Transform handles follow one-way data flow:
- Set
boundson the handles (or usetargetto let them track automatically) - User interacts — handles fire events
- Your code updates the element
- Update
boundsif you are managing them manually
Never mutate the bounds object in-place from inside an event handler. Always produce a new object.
Coordinate System
Bounds are in screen pixel coordinates — the same coordinate space as element.offsetLeft / element.getBoundingClientRect(). When working with a scaled canvas, you need to convert between canvas coordinates and screen coordinates before setting bounds, and convert back when applying event results.
// canvas coordinate → screen coordinateconst screenX = canvasX * canvasScale;// screen coordinate → canvas coordinateconst canvasX = screenX / canvasScale;
Snapping Rotation
Use rotation-step to constrain rotation to fixed increments:
<!-- Snaps to 0°, 15°, 30°, 45°, ... --><ef-transform-handlestarget="#element"enable-rotationrotation-step="15"></ef-transform-handles>
This is useful for enforcing 90° snapping (use rotation-step="90") or 45° diagonal locks.
Setting a Minimum Size
Prevent elements from being resized too small:
<ef-transform-handlestarget="#element"min-size="50"></ef-transform-handles>
Width and height cannot be resized below 50 pixels.
CSS Customization
Override the default blue/green color scheme with CSS custom properties:
ef-transform-handles {--ef-transform-handles-border-color: #f59e0b;--ef-transform-handles-handle-color: #fff;--ef-transform-handles-handle-border-color: #f59e0b;--ef-transform-handles-rotate-handle-color: #10b981;}