Math.sign()
ES6+Returns the sign of a number: 1 for positive values, -1 for negative values, 0 for +0, -0 for -0, and NaN for anything that fails numeric coercion. It answers "which direction" while discarding magnitude, and is the standard companion to Math.abs(), which does the opposite.
Syntax
Math.sign(x)Parameters
x number A number
Return Value
1, -1, or 0 indicating the sign of the number
Examples
console.log(Math.sign(5));
console.log(Math.sign(-5));
console.log(Math.sign(0)); 📌 When to Use
Use Math.sign() when direction is the information you need: which way a character or camera should move given a velocity, whether a stock ticked up or down, whether a swipe went left or right, which arrow icon a diff or delta should show, and normalizing step directions in animation loops (position += speed * Math.sign(target - position) walks toward a target without overshooting logic scattered around). It collapses a signed quantity into one of three meaningful states, which makes downstream code table-driven: icons[Math.sign(change) + 1] indexes falling/flat/rising states without an if-chain. In comparator functions, Math.sign(a - b) is a tidy way to coerce a numeric difference into the -1/0/1 shape sort expects, though any negative/zero/positive number already satisfies sort's contract, so this is about normalization for reuse rather than necessity. Its five possible outputs deserve one careful read: the -0 return for -0 input preserves IEEE information but compares equal to 0 with ===, so ordinary sign checks are unaffected, while code that genuinely distinguishes zeros must use Object.is. And because non-numeric input returns NaN rather than throwing, validate upstream when the argument may come from user data, or the "flat" branch of your logic will quietly swallow corrupt values.
⚠️ Common Mistakes
Expecting the magnitude back: Math.sign(-42) is -1, not -42. Code that means "make this positive" wants Math.abs; code that means "keep the value but flip if negative" wants conditional negation. sign strictly classifies.
Forgetting the NaN path: Math.sign(undefined), Math.sign("abc"), and Math.sign(NaN) all return NaN, and NaN fails every comparison, so switch-style logic keyed on -1/0/1 silently matches nothing. Guard with Number.isFinite when input is untrusted.
Treating the output as strictly binary: zero is a real case, deltas are exactly 0 whenever nothing changed, and UI that maps sign to up/down arrows without a flat state renders nothing (or worse, a stale arrow) for unchanged values.
Misreading -0: Math.sign(-0) is -0, and while -0 === 0 is true (so if (Math.sign(x) === 0) still works), snapshot tests, Object.is comparisons, and JSON round-trips (which serialize -0 as 0) can surface confusing mismatches around it.
Reconstructing it with clever arithmetic like x / Math.abs(x): that form divides by zero at x = 0 (yielding NaN) and adds a needless division; the comparison form (x > 0) - (x < 0) works but obscures intent. The named function is the readable, edge-correct choice.
Using sign where a threshold belongs: mapping tiny float noise (a delta of 1e-15 from rounding) to a full "increase" arrow misleads users; real dashboards classify with a deadband, Math.abs(d) < eps ? 0 : Math.sign(d).
✅ Best Practices
Pair it with Math.abs to decompose values: const dir = Math.sign(v), mag = Math.abs(v); then format each independently, arrow from dir, number from mag, keeping presentation logic declarative and testable.
Use table dispatch on sign for three-way UI states: const labels = { "-1": "down", "0": "flat", "1": "up" }; labels[Math.sign(delta)] (string keys absorb the -0 case since String(-0) is "0").
Add a deadband before classifying noisy data: Math.abs(delta) < epsilon ? 0 : Math.sign(delta), so floating-point residue and sensor jitter read as "unchanged" rather than flapping between up and down.
In movement code, multiply speed by sign of remaining distance and clamp the final step: step = Math.min(Math.abs(remaining), speed) * Math.sign(remaining) reaches the target exactly without oscillating around it.
For comparators, Math.sign(a - b) is fine for numbers, but beware a - b overflow semantics do not exist for doubles while precision loss does: for very large values or mixed types, an explicit (a < b ? -1 : a > b ? 1 : 0) avoids subtraction artifacts (and works for BigInt, where Math.sign throws).
⚡ Performance Notes
Math.sign() is a trivial branchless classification that engines inline; it costs about as much as a comparison and never registers in profiles, so use it freely in per-frame movement code, comparators, and large array maps. The arithmetic alternatives ((x > 0) - (x < 0)) offer zero measurable advantage in modern engines and only reduce clarity. Two adjacent notes matter more in hot paths: first, feeding sign the result of a subtraction keeps values in floating-point registers and is cheaper than branching logic that recomputes conditions repeatedly; second, when classifying entire datasets, mapping once into a small integer array (Int8Array of -1/0/1) and reusing that classification beats re-deriving signs per render. As always with numeric kernels, type stability, keeping inputs consistently doubles, does more for speed than micro-choosing between sign idioms.
🌍 Real World Example
Game Character Movement Direction
Character movement is direction times speed, and this example uses Math.sign() to derive facing and velocity direction from the difference between target and current position, with a clamped final step so the sprite lands exactly on target instead of jittering around it. Decomposing motion into sign and magnitude keeps acceleration, animation-flip, and arrival logic independent and easy to test. The identical pattern drives camera panning toward a focus point, smooth-scrolling implementations, and the up/down tick indicators on live price displays.
function updateCharacterMovement(velocityX, velocityY) {
const direction = {
x: Math.sign(velocityX),
y: Math.sign(velocityY)
};
const facing = direction.x === 0
? (direction.y > 0 ? 'down' : 'up')
: (direction.x > 0 ? 'right' : 'left');
return {
direction,
facing,
isMoving: direction.x !== 0 || direction.y !== 0
};
}
console.log(updateCharacterMovement(5.5, 0));
// { direction: { x: 1, y: 0 }, facing: 'right', isMoving: true }
console.log(updateCharacterMovement(-2.3, -4.1));
// { direction: { x: -1, y: -1 }, facing: 'left', isMoving: true }