Skip to content
Entangle UI v0.13.0

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 { CurveEditor } from 'entangle-ui';
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

// Controlled
<CurveEditor value={curve} onChange={setCurve} />
// Uncontrolled with default
<CurveEditor defaultValue={myCurveData} />
  • 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

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

| Mode | Description | | ---------- | ------------------------------------------------------ | | free | Each handle moves independently | | aligned | Handles stay co-linear but can differ in length | | mirrored | Handles are symmetric (same angle and length) | | auto | Smooth catmull-rom style, auto-computed from neighbors | | linear | No handles, straight line segments | | step | Constant value until next keyframe (step function) |

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

Tangent Modes

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

Lock Tangents

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

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.

Presets

<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"
/>

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

Snap to Grid

<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
/>

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

Responsive Mode

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

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

Custom Background

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

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

Custom Bottom Bar

<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>
)}
/>

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

Change Complete

<CurveEditor
value={curve}
onChange={setCurve}
onChangeComplete={finalCurve => {
undoStack.push(finalCurve);
}}
/>
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.

| Property | Type | Description | | -------------- | ------------------------------------------------- | --------------------------------------- | | keyframes | CurveKeyframe[] | 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. |

| Property | Type | Description | | ------------- | -------------------------- | ------------------------------------------- | | x | number | X position in domain space. | | y | number | Y value at this position. | | handleIn | { x: number; y: number } | Left tangent handle offset (relative). | | handleOut | { x: number; y: number } | Right tangent handle offset (relative). | | tangentMode | TangentMode | Tangent mode for this keyframe. | | id | string | Unique ID (auto-generated if not provided). |

  • 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