Skills/Editor Toolkit/Transform Handles Element

ef-transform-handles

Attributes

boundsRequired
TransformBounds

Bounding box to display handles for

target
string | HTMLElement

Target element (for rotation calculation)

canvas-scale
number

Canvas zoom scale (fallback when context unavailable)

Default:1
enable-rotation
boolean

Show rotation handle

Default:false
enable-resize
boolean

Show resize handles

Default:true
enable-drag
boolean

Enable drag to move

Default:true
corners-only
boolean

Show only corner handles (hide edge handles)

Default:false
lock-aspect-ratio
boolean

Maintain aspect ratio during resize

Default:false
rotation-step
number

Snap rotation to degrees (e.g. 15 for 15deg increments)

min-size
number

Minimum width/height during resize

Default:10

Properties

interactionMode
'idle' | 'dragging' | 'resizing' | 'rotating'

Current interaction state

Interactive resize and rotation handles for elements.

Basic Usage

Display handles for a positioned element:

<div class="relative w-[600px] h-[400px] border border-gray-300 rounded overflow-hidden bg-gray-50">
<ef-transform-handles
.bounds=${{ x: 50, y: 50, width: 200, height: 150 }}
enable-rotation
enable-resize
></ef-transform-handles>
</div>

Drag handles to resize, drag rotation handle to rotate.

Bounds

Transform handles require bounds in screen coordinates:

const handles = document.querySelector('ef-transform-handles');
handles.bounds = {
x: 100, // Left position
y: 100, // Top position
width: 200, // Width
height: 150, // Height
rotation: 0 // Optional rotation in degrees
};

Resize Handles

Enable resize with corner and edge handles:

<ef-transform-handles
.bounds=${{ x: 50, y: 50, width: 200, height: 150 }}
enable-resize
></ef-transform-handles>

Corner Handles

Four corner handles resize proportionally by default:

  • Northwest: Top-left corner
  • Northeast: Top-right corner
  • Southwest: Bottom-left corner
  • Southeast: Bottom-right corner

Edge Handles

Four edge handles resize in one dimension:

  • North: Top edge (height only)
  • East: Right edge (width only)
  • South: Bottom edge (height only)
  • West: Left edge (width only)

Corners Only

Hide edge handles, show only corners:

<ef-transform-handles
.bounds=${{ x: 50, y: 50, width: 200, height: 150 }}
enable-resize
corners-only
></ef-transform-handles>

Rotation Handle

Enable rotation with top-center handle:

<div class="relative w-[600px] h-[400px] border border-gray-300 rounded overflow-hidden bg-gray-50">
<ef-transform-handles
.bounds=${{ x: 150, y: 100, width: 200, height: 150, rotation: 15 }}
enable-rotation
enable-resize
></ef-transform-handles>
</div>

Drag the rotation handle to rotate the bounds.

Rotation Step

Snap rotation to specific increments:

<ef-transform-handles
.bounds=${{ x: 50, y: 50, width: 200, height: 150 }}
enable-rotation
rotation-step="15"
></ef-transform-handles>

Rotation snaps to 15deg increments: 0deg, 15deg, 30deg, 45deg, etc.

Aspect Ratio Lock

Maintain aspect ratio during resize:

<ef-transform-handles
.bounds=${{ x: 50, y: 50, width: 200, height: 150 }}
enable-resize
lock-aspect-ratio
></ef-transform-handles>

All resize operations maintain the original aspect ratio.

Drag to Move

Enable dragging to move the bounds:

<ef-transform-handles
.bounds=${{ x: 50, y: 50, width: 200, height: 150 }}
enable-drag
></ef-transform-handles>

Click and drag the overlay to move.

Disable Drag

<ef-transform-handles
.bounds=${{ x: 50, y: 50, width: 200, height: 150 }}
enable-drag="false"
></ef-transform-handles>

Events

Listen for transformation events:

const handles = document.querySelector('ef-transform-handles');
// Bounds changed (resize or move)
handles.addEventListener('bounds-change', (e) => {
const { bounds } = e.detail;
console.log('New bounds:', bounds);
// Update element
element.style.left = `${bounds.x}px`;
element.style.top = `${bounds.y}px`;
element.style.width = `${bounds.width}px`;
element.style.height = `${bounds.height}px`;
});
// Rotation changed
handles.addEventListener('rotation-change', (e) => {
const { rotation } = e.detail;
console.log('New rotation:', rotation);
// Update element
element.style.transform = `rotate(${rotation}deg)`;
});

Interaction Modes

Transform handles track current interaction:

const handles = document.querySelector('ef-transform-handles');
console.log(handles.interactionMode);
// 'idle' | 'dragging' | 'resizing' | 'rotating'
// Only one mode active at a time

Coordinate System

Transform handles work in screen pixel coordinates:

// Bounds are in screen pixels (not canvas coordinates)
handles.bounds = {
x: 100, // Screen x
y: 100, // Screen y
width: 200, // Screen width
height: 150 // Screen height
};
// Events dispatch screen coordinates
handles.addEventListener('bounds-change', (e) => {
const { bounds } = e.detail;
// bounds.x, bounds.y are screen coordinates
});

When using with canvas, convert between canvas and screen coordinates.

Canvas Scale

Provide canvas scale for zoom-aware rendering:

const handles = document.querySelector('ef-transform-handles');
// Set scale directly
handles.canvasScale = 1.5;
// Or via context (automatic with pan-zoom)
// <ef-pan-zoom>
// <ef-transform-handles></ef-transform-handles>
// </ef-pan-zoom>

Canvas scale ensures handles render at consistent size regardless of zoom.

Target Element

Provide target element for rotation calculations:

const handles = document.querySelector('ef-transform-handles');
const element = document.getElementById('my-element');
// String selector
handles.target = '#my-element';
// Or element reference
handles.target = element;

Target element's center is used as rotation origin.

Minimum Size

Set minimum bounds during resize:

<ef-transform-handles
.bounds=${{ x: 50, y: 50, width: 200, height: 150 }}
enable-resize
min-size="50"
></ef-transform-handles>

Width and height cannot be resized below 50 pixels.

Styling

Transform handles use CSS custom properties:

ef-transform-handles {
/* Border color */
--ef-transform-handles-border-color: #2196f3;
/* Border during drag */
--ef-transform-handles-dragging-border-color: #1976d2;
/* Handle background */
--ef-transform-handles-handle-color: #fff;
/* Handle border */
--ef-transform-handles-handle-border-color: #2196f3;
/* Rotation handle color */
--ef-transform-handles-rotate-handle-color: #4caf50;
}

One-Way Data Flow

Transform handles follow one-way data flow:

  1. Parent sets bounds prop
  2. User interacts with handles
  3. Handles dispatch events
  4. Parent updates element
  5. Parent updates bounds prop
  6. Handles re-render

Never mutate the bounds prop directly from handle events.

Wheel Events

Transform handles forward wheel events to parent pan-zoom for seamless zooming:

<ef-pan-zoom>
<ef-canvas>
<!-- Wheel events on handles zoom the canvas -->
</ef-canvas>
</ef-pan-zoom>

Mouse wheel over handles zooms the canvas instead of being blocked.

Rotation Calculation

Rotation is calculated relative to target element's center:

// For rotated elements
handles.target = element;
handles.bounds = {
x: element.offsetLeft,
y: element.offsetTop,
width: element.offsetWidth,
height: element.offsetHeight,
rotation: getCurrentRotation(element)
};
// Rotation handle calculates delta from target center

Usage with Canvas

Transform handles integrate with ef-canvas:

const canvas = document.querySelector('ef-canvas');
const handles = document.querySelector('ef-transform-handles');
// Get element bounds from canvas
const data = canvas.getElementData('element-id');
// Convert to screen coordinates
const screenPos = canvas.canvasToScreenCoords(data.x, data.y);
const scale = panZoom.scale;
handles.bounds = {
x: screenPos.x,
y: screenPos.y,
width: data.width * scale,
height: data.height * scale,
rotation: data.rotation
};
// Listen for changes
handles.addEventListener('bounds-change', (e) => {
// Convert back to canvas coordinates
const canvasPos = canvas.screenToCanvasCoords(e.detail.bounds.x, e.detail.bounds.y);
canvas.updateElementPosition('element-id', canvasPos.x, canvasPos.y);
});

Handle Features

  • 8 resize handles: nw, n, ne, e, se, s, sw, w
  • Rotation handle: Top-center circular handle (when enabled)
  • Drag area: Click anywhere inside bounds to drag
  • Visual feedback: Handles highlight on hover
  • Smart cursors: Automatically set appropriate resize cursors
  • Shift key: Hold to maintain aspect ratio during resize

CSS Customization

Use CSS variables to customize handle appearance:

.transform-handles {
--ef-transform-handles-border-color: #3b82f6;
--ef-transform-handles-handle-color: #ffffff;
--ef-transform-handles-handle-border-color: #3b82f6;
--ef-transform-handles-rotate-handle-color: #10b981;
}

Pointer Events

Transform handles have pointer-events: auto for interactivity. Parent overlay layer should have pointer-events: none.

Important Notes

  • Bounds are in absolute pixel coordinates
  • Use canvas-scale when inside pan-zoom to maintain handle size
  • Rotation is in degrees (0-360)
  • lock-aspect-ratio maintains the initial aspect ratio
  • Handles capture pointer events but allow wheel events to pass through
  • Minimum size prevents resizing below practical limits