A motion curve to shape by dragging its two handles, moving them with the arrow keys, or typing its four numbers.
Options
The curve editor has no choices: it is drawn one way.
Examples
Usage
An ease-out curve, its CSS written under it. Drag a handle, or Tab to it and use the arrow keys; or type the numbers.
Overshoot
A springy curve that goes past its end: the dashed line marks how far.
Without the numbers
The four boxes left out, where the numbers show elsewhere. The handles still move with the keys.
Guidelines
Purpose
Shapes a motion curve, the easing of an animation: time runs across and movement up, from the start in the lower left to the end in the upper right. The reader drags its two handles, moves them with the arrow keys, or types the four numbers CSS's cubic-bezier() takes. It is for tools that edit a design system's motion. To choose among a few named easings, use a select or a segmented control.
When to use
- To set or adjust the easing of a motion token or an animation.
When not to use
- To pick one of a few named easings. Offer them by name.
- For anything but a cubic curve: steps and springs are not curves of two handles.
Usage rules
Do
- Name the editor for the motion it shapes.
- Keep the numbers where the reader needs the exact curve.
Don't
- Leave out the numbers where the reader has nowhere else to read or type the curve.
Name the editor for the motion it shapes, such as “Enter easing”.
Leave out the numbers where the reader needs the exact curve and has nowhere else to type it.
Anatomy
- The block: a column of the plot over the numbers. It fills its width up to 240 px.
- The plot part: a square on the sunken surface. Its drawing holds the unit square (the square part), the straight line from start to end (the line part, dashed), the two arms from the ends to the handles (the arm part), the curve (the curve part, in the brand color) and, when the curve goes past its end or before its start, a dashed line at how far it goes (the limit part).
- The handle part, two of them: a 12 px dot with a brand ring, inside a 24 px target. Hover fills it with the hover surface; while it is dragged it fills with the brand color; with keyboard focus it shows the focus ring.
- The numbers part: four small number boxes, the first handle's time and movement, then the second's. They can be left out.
Behavior
A handle follows the pointer while it is dragged. Its time stays between 0 and 1, as CSS needs; its movement may go past either end, a springy curve that overshoots, as far as the plot's room above and below the square. The editor holds its curve from a first value, or follows the one the page gives it, and reports the four numbers after every move and every settled number. curveToCss writes them as CSS.
On a touch screen a press anywhere inside a number box lands on it, not only on its figures.
Accessibility
What it does for you
- The editor is a group. Each handle holds two range inputs, its time and its movement, named "First handle, time" and so on, and each says both values ("time 0.2, movement 0"). One of the two is the handle's stop in the Tab order.
- The keys move a handle: Left and Right move the time, Up and Down the movement, by 0.01, or 0.1 with Shift. Page Up and Page Down move the movement by 0.1; Home and End put the time at 0 and 1.
- The four number boxes give the same values to type, each named for its number ("x1, first handle's time"), and step with the up and down arrow keys. Dragging is never the only way.
- The drawing is hidden from screen readers; the inputs and the boxes are the content. In forced colors mode the curve and the handles take the reader's colors.
- On a touch screen a press anywhere inside a number box lands on it, not only on its figures.
What you must do
- Name the editor for the motion it shapes, such as "Enter easing".
- Keep the number boxes where the reader needs the exact curve, or has nowhere else to read or type it.
- Give every word it says by itself, such as the handles' and the number boxes' names, in the page's language; each keeps its English unless the page gives its own.
How to test
- Tab to a handle and move it with the arrow keys, with and without Shift, then with Page Up, Page Down, Home and End. The values change and the curve follows.
- Type a number in each box and step it with Up Arrow and Down Arrow.
- With a screen reader, the editor is read as a group by its name, each handle by its name and both values, each box by its number.
- With text at 200% and the page 320 pixels wide, the boxes and the plot stay whole.
- In a forced-color mode, the curve and the handles are visible.
Keyboard
| Key | What it does |
|---|---|
| TaborShift+Tab | Moves focus to the next or previous handle or number box. |
| Left ArroworRight Arrow | Moves the focused handle's time by 0.01, down or up. With Shift, by 0.1. |
| Up ArroworDown Arrow | Moves the focused handle's movement by 0.01, up or down. With Shift, by 0.1. In a number box, steps the number. |
| Page UporPage Down | Moves the focused handle's movement by 0.1, up or down. |
| HomeorEnd | Puts the focused handle's time at 0 or at 1. |
Source
The files the curve editor is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-curve-editor — Curve editor. Figma "Curve editor" 7416:192 (Type: Ease, Overshoot; Numbers switch) and its
* "CurveHandle" 7416:121 (State: Default, Hover, Pressed, Focus).
* A motion curve, time across and movement up: a square plot on the sunken surface inside a subtle edge holds the unit
* square (a subtle line), the straight line from start to end (dashed, subtle), the two arms from the ends to the handles
* (strong hairlines), the curve itself (two pixels in the brand action color) and, when the curve passes its end or its
* start, a dashed strong line at how far it goes. Under it, four small number boxes give the same four numbers to type.
* The drawing is an inline SVG whose shapes the wrapper places from the numbers; the lines' colors and widths are here.
* The unit square sits as Figma places it, with room above it and less below, so a springy curve shows.
* Each handle is a 12 px dot on the default surface with a two pixel brand ring and the control shadow, inside a 24 px
* target. Hover lays the hover surface in it; pressed (data-pressed, while dragged) fills it with the brand color; when
* one of its two hidden range inputs has keyboard focus it draws the focus ring outside a gap of the default surface. */
.smbk-curve-editor {
display: flex;
flex-direction: column;
gap: var(--smbk-space-stack-md);
width: 100%;
max-width: var(--smbk-size-width-240);
min-width: 0;
}
.smbk-curve-editor__plot {
position: relative;
box-sizing: border-box;
width: 100%;
aspect-ratio: 1;
border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
border-radius: var(--smbk-radius-control-md);
background: var(--smbk-color-surface-sunken);
touch-action: none;
}
.smbk-curve-editor__drawing {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
overflow: visible;
}
.smbk-curve-editor__square,
.smbk-curve-editor__line,
.smbk-curve-editor__arm,
.smbk-curve-editor__limit,
.smbk-curve-editor__curve {
fill: none;
stroke-width: var(--smbk-border-width-hairline);
vector-effect: non-scaling-stroke;
}
.smbk-curve-editor__square,
.smbk-curve-editor__line {
stroke: var(--smbk-color-border-subtle);
}
.smbk-curve-editor__arm,
.smbk-curve-editor__limit {
stroke: var(--smbk-color-border-strong);
}
.smbk-curve-editor__line,
.smbk-curve-editor__limit {
stroke-dasharray: var(--smbk-size-4) var(--smbk-size-4);
}
.smbk-curve-editor__curve {
stroke: var(--smbk-color-action-brand-background);
stroke-width: var(--smbk-border-width-2);
stroke-linecap: round;
}
/* ---- the handle (Figma's CurveHandle) ---- */
.smbk-curve-editor__handle {
position: absolute;
box-sizing: border-box;
width: var(--smbk-size-12);
height: var(--smbk-size-12);
border: var(--smbk-border-width-2) solid var(--smbk-color-action-brand-background);
border-radius: var(--smbk-radius-full);
background: var(--smbk-color-surface-default);
box-shadow: var(--smbk-elevation-control);
transform: translate(-50%, -50%);
cursor: grab;
touch-action: none;
}
/* A 24 px target round the 12 px dot, so a finger or a pointer finds it. */
.smbk-curve-editor__handle::before {
content: '';
position: absolute;
inset: calc((var(--smbk-size-12) - var(--smbk-size-24)) / 2 - var(--smbk-border-width-2));
border-radius: var(--smbk-radius-full);
}
@media (hover: hover) {
.smbk-curve-editor__handle:hover {
background: var(--smbk-color-surface-hover);
}
}
.smbk-curve-editor__handle[data-pressed] {
background: var(--smbk-color-action-brand-background);
cursor: grabbing;
}
/* Figma's focus style: the default surface in a gap, then the focus ring. The transparent outline is what forced
colors mode paints, since it drops shadows. */
.smbk-curve-editor__handle:has(:focus-visible) {
outline: var(--smbk-focus-ring-width) solid transparent;
outline-offset: var(--smbk-focus-ring-offset);
box-shadow:
0 0 0 var(--smbk-focus-ring-offset) var(--smbk-color-surface-default),
0 0 0 calc(var(--smbk-focus-ring-offset) + var(--smbk-focus-ring-width)) var(--smbk-color-border-focus);
}
/* The two hidden range inputs cover the dot, unseen; the handle takes the pointer. */
.smbk-curve-editor__handle input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
pointer-events: none;
}
/* ---- the four numbers ---- */
.smbk-curve-editor__numbers {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--smbk-space-inline-sm);
}
/* ---- touch: a press anywhere inside a box lands on it ----
On a touch screen each number box's text control reaches over the small well's padding at both ends, so the
whole box takes the press, not only the figures; the four boxes stand apart by the row's gap. The figures stay
where they are drawn. */
@media (pointer: coarse) {
.smbk-curve-editor__numbers .smbk-input__control {
margin-inline: calc(-1 * var(--smbk-space-inset-md));
padding-inline: var(--smbk-space-inset-md);
}
}
/* ---- high contrast (forced colors) ---- */
@media (forced-colors: active) {
.smbk-curve-editor__square,
.smbk-curve-editor__line,
.smbk-curve-editor__arm,
.smbk-curve-editor__limit {
stroke: GrayText;
}
.smbk-curve-editor__curve {
stroke: CanvasText;
}
.smbk-curve-editor__handle {
border-color: CanvasText;
background: Canvas;
}
.smbk-curve-editor__handle[data-pressed] {
background: Highlight;
}
.smbk-curve-editor__handle:has(:focus-visible) {
outline-color: Highlight;
}
}Tokens it reads
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| space.stack.md--smbk-space-stack-md | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| size.width.240--smbk-size-width-240 | 240px | Size | |
| border-width.hairline--smbk-border-width-hairline | 1px | Border width | |
| color.border.subtle--smbk-color-border-subtle | light: #c4cdd8dark: #2d3237changes with theme | Color | |
| radius.control.md--smbk-radius-control-md | 6px | Radius | |
| color.surface.sunken--smbk-color-surface-sunken | light: #373d430ddark: #00000066changes with theme | Color | |
| color.border.strong--smbk-color-border-strong | light: #9ba5b1dark: #373d43changes with theme | Color | |
| size.4--smbk-size-4 | 4px | Size | |
| color.action.brand.background--smbk-color-action-brand-background | #cc0d0d | Color | |
| border-width.2--smbk-border-width-2 | 2px | Border width | |
| size.12--smbk-size-12 | 12px | Size | |
| radius.full--smbk-radius-full | 9999px | Radius | |
| color.surface.default--smbk-color-surface-default | light: #f3f5f8dark: #181a1cchanges with theme | Color | |
| elevation.control--smbk-elevation-control | light: 0px 0px 1px 0px #373d430d, 0px 1px 2px 0px #373d431a, 0px 2px 4px 0px #373d430ddark: 0px 0px 1px 0px #0000001a, 0px 1px 2px 0px #00000026, 0px 2px 4px 0px #0000001a | Elevation | |
| size.24--smbk-size-24 | 24px | Size | |
| color.surface.hover--smbk-color-surface-hover | light: #373d4326dark: #606a754dchanges with theme | Color | |
| focus.ring.width--smbk-focus-ring-width | 2px | Focus | |
| focus.ring.offset--smbk-focus-ring-offset | 2px | Focus | |
| color.border.focus--smbk-color-border-focus | light: #cc0d0ddark: #f34336changes with theme | Color | |
| space.inline.sm--smbk-space-inline-sm | comfortable: 6pxcompact: 4pxchanges with density | Spacing | |
| space.inset.md--smbk-space-inset-md | comfortable: 8pxcompact: 6pxchanges with density | Spacing |
Manifest
{
"name": "curve-editor",
"block": "smbk-curve-editor",
"summary": "A motion curve to shape by dragging its two handles, moving them with the arrow keys, or typing its four numbers.",
"adapt": "css",
"axes": {},
"flags": {},
"parts": [
"plot",
"drawing",
"square",
"line",
"arm",
"limit",
"curve",
"handle",
"numbers"
],
"figmaNodeId": "7416:192"
}