Start
//

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:

  1. Set bounds on the handles (or use target to let them track automatically)
  2. User interacts — handles fire events
  3. Your code updates the element
  4. Update bounds if 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 coordinate
const screenX = canvasX * canvasScale;
// screen coordinate → canvas coordinate
const canvasX = screenX / canvasScale;

Snapping Rotation

Use rotation-step to constrain rotation to fixed increments:

<!-- Snaps to 0°, 15°, 30°, 45°, ... -->
<ef-transform-handles
target="#element"
enable-rotation
rotation-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-handles
target="#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;
}