Skip to content

CurveEditor

Interactive bezier curve editor for professional tools. Supports keyframe editing with multiple tangent modes, built-in and custom presets, grid and axis labels, snap-to-grid, and custom render props for backgrounds and bottom bars. Used for animation curves, color grading, easing functions, and value remapping.

Live Preview

Import

import { CurveEditor } from 'entangle-ui';

Usage

const [curve, setCurve] = useState(undefined);
<CurveEditor value={curve} onChange={setCurve} width={400} height={250} />;

The component defaults to an ease-in-out curve when no value is provided.

Controlled vs Uncontrolled

// Controlled
<CurveEditor value={curve} onChange={setCurve} />
// Uncontrolled with default
<CurveEditor defaultValue={myCurveData} />

Keyframe Interaction

  • Double-click on the curve to add a new keyframe
  • Click a keyframe to select it (Shift+Click for multi-select, box select by dragging on empty space)
  • Drag a keyframe to move it
  • Delete/Backspace to remove selected keyframes
  • Double-click a keyframe to cycle tangent modes
  • Keyboard shortcuts 1-6 set tangent modes on selected keyframes

Tangent Modes

Each keyframe has a tangent mode controlling how the curve passes through it.

ModeDescription
freeEach handle moves independently
alignedHandles stay co-linear but can differ in length
mirroredHandles are symmetric (same angle and length)
autoSmooth catmull-rom style, auto-computed from neighbors
linearNo handles, straight line segments
stepConstant value until next keyframe (step function)

The toolbar provides buttons for switching tangent modes on selected keyframes.

Lock Tangents

When lockTangents is true, tangent handle editing is hidden and disabled. The curve still renders normally using each keyframe’s existing tangent mode.

<CurveEditor value={curve} onChange={setCurve} lockTangents />

Presets

Built-in presets include ease-in, ease-out, ease-in-out, linear, and more. You can add custom presets that appear alongside them in the toolbar.

<CurveEditor
value={curve}
onChange={setCurve}
presets={[
{
id: 'custom-bounce',
label: 'Bounce',
category: 'Custom',
curve: bounceCurveData,
},
]}
/>

Grid and Labels

<CurveEditor
value={curve}
onChange={setCurve}
showGrid
gridSubdivisions={8}
showAxisLabels
labelX="Time"
labelY="Value"
/>

Snap to Grid

Hold Ctrl to toggle snapping, or enable it as the default.

<CurveEditor
value={curve}
onChange={setCurve}
snapToGrid
gridSubdivisions={4}
/>

Constraints

<CurveEditor
value={curve}
onChange={setCurve}
lockEndpoints // First/last keyframes locked on X axis
maxKeyframes={10} // Limit number of keyframes
minKeyframeDistance={0.01}
clampY // Clamp Y values to domain bounds
/>

Responsive Mode

When responsive is true, the editor fills its parent container using a ResizeObserver, ignoring the width prop.

<div style={{ width: '100%' }}>
<CurveEditor responsive height={200} value={curve} onChange={setCurve} />
</div>

Custom Background

Use renderBackground to draw behind the curve, such as a histogram or gradient (like Photoshop/Lightroom curves).

<CurveEditor
value={curve}
onChange={setCurve}
renderBackground={(ctx, info) => {
// Draw a gradient behind the curve
const grad = ctx.createLinearGradient(
info.area.x,
info.area.y + info.area.height,
info.area.x,
info.area.y
);
grad.addColorStop(0, 'rgba(0, 0, 0, 0.3)');
grad.addColorStop(1, 'rgba(255, 255, 255, 0.1)');
ctx.fillStyle = grad;
ctx.fillRect(info.area.x, info.area.y, info.area.width, info.area.height);
}}
/>

Custom Bottom Bar

Use renderBottomBar for custom content below the canvas, such as coordinate readouts or channel selectors.

<CurveEditor
value={curve}
onChange={setCurve}
renderBottomBar={({ selectedKeyframes, evaluate }) => (
<div>
{selectedKeyframes.length > 0 && (
<span>
Selected: ({selectedKeyframes[0].x.toFixed(2)},
{selectedKeyframes[0].y.toFixed(2)})
</span>
)}
</div>
)}
/>

Change Complete

Use onChangeComplete for undo system integration. It fires on drag end, keyframe add, and keyframe delete.

<CurveEditor
value={curve}
onChange={setCurve}
onChangeComplete={finalCurve => {
undoStack.push(finalCurve);
}}
/>

Props

Prop Type Default Description
value CurveData Curve data (controlled).
defaultValue CurveData ease-in-out preset Default curve data (uncontrolled).
width number 320 Width of the editor in pixels.
height number 200 Height of the editor in pixels.
responsive boolean false Whether the editor fills its parent container via ResizeObserver.
showToolbar boolean true Whether to show the toolbar with presets and tangent mode buttons.
showGrid boolean true Whether to show grid lines.
gridSubdivisions number 4 Number of grid subdivision lines between domain bounds.
showAxisLabels boolean true Whether to display X/Y axis value labels on the grid.
allowAdd boolean true Whether double-clicking the curve adds a new keyframe.
allowDelete boolean true Whether Delete/Backspace removes selected keyframes.
maxKeyframes number Infinity Maximum number of keyframes allowed.
lockEndpoints boolean true Whether first/last keyframe X positions are locked.
minKeyframeDistance number 0.001 Minimum distance between keyframes on the X axis.
clampY boolean true Whether Y values are clamped to domain bounds.
snapToGrid boolean false Snap to grid while dragging (Ctrl toggles).
precision number 3 Number format precision for displayed values.
labelX string X axis label (e.g., "Time", "Input").
labelY string Y axis label (e.g., "Value", "Output").
presets CurvePreset[] Custom preset curves, merged with built-in presets.
size 'sm' | 'md' | 'lg' 'md' Component size affecting toolbar and label sizing.
disabled boolean false Whether the editor is disabled.
readOnly boolean false Whether the editor is read-only (viewable but not editable).
curveColor string theme accent color CSS color for the curve line.
curveWidth number 2 Curve line width in pixels.
lockTangents boolean false Hides tangent handles and disables tangent editing UI.
onChange (curve: CurveData) => void Callback fired continuously during drag.
onChangeComplete (curve: CurveData) => void Callback fired when editing is committed (drag end, add, delete).
onSelectionChange (selectedIds: string[]) => void Callback when keyframe selection changes.
renderBackground (ctx: CanvasRenderingContext2D, info: CurveBackgroundInfo) => void Custom background renderer for the canvas.
renderBottomBar (info: CurveBottomBarInfo) => ReactNode Render prop for custom content below the canvas.
className string Additional CSS class names.
testId string Test identifier for automated testing.

CurveData

PropertyTypeDescription
keyframesCurveKeyframe[]Ordered array of keyframes sorted by x.
domainX[number, number]Domain bounds for the X axis.
domainY[number, number]Domain bounds for the Y axis.
preInfinity'constant' | 'linear' | 'cycle' | 'pingpong'Behavior before the first keyframe.
postInfinity'constant' | 'linear' | 'cycle' | 'pingpong'Behavior after the last keyframe.

CurveKeyframe

PropertyTypeDescription
xnumberX position in domain space.
ynumberY value at this position.
handleIn{ x: number; y: number }Left tangent handle offset (relative).
handleOut{ x: number; y: number }Right tangent handle offset (relative).
tangentModeTangentModeTangent mode for this keyframe.
idstringUnique ID (auto-generated if not provided).

Accessibility

  • Canvas-based rendering with keyboard support for keyframe manipulation
  • Arrow keys move selected keyframes
  • Delete/Backspace removes selected keyframes
  • Number keys 1-6 set tangent modes on selected keyframes
  • Box select via Shift+drag on empty canvas area
  • Multi-select via Shift+Click on keyframes