How we implemented SVG Arrows in React: Final touches (3/3)

In the previous part of this series, we played with Bezier curves. We showed how we represent them in SVG, how we calculate Control points, and how we adjust the size of the canvas.

If you want to skip ahead, you can go directly to our code on CodeSandbox or Github.

We’ll now look at a few final touches. The most important one is something that makes an arrow an arrow: The arrowhead.

Arrowhead

For the arrowhead, we will use a simple path component, where a polyline similar to the arrow will be drawn. We do not have to deal with the rotation of the arrow, because in our case, the arrow always points to the right.

It’s important to move the arrow to the correct position so that it is always drawn at the End point coordinate. This coordinate will still need to be adjusted so that the line always ends in the center of the arrow.

Rendering of the circle at the Start point of the arrow is easy:

When we style the arrow a bit, it looks pretty good at first glance:

The curved arrow displayed correctly
The straight arrow displayed correctly

Edge cases

But there is still at least one case that we have not solved — when the line is straight and the Start point is on the right and the End point is on the left. In such cases, the arrow is misplaced, because the arrowhead of the arrow on the right side crosses the line, and the whole thing looks weird:

Badly displayed inverted arrow

However, the line should look right in this case:

This is how an inverted arrow should look

This situation occurs when the delta Y (ΔY) is too small, while at the same time [first point X coordinate] > [last point X coordinate]. Under these conditions, all we need to do is move the Control points a little up or down.

The arrow should behave continuously. This means that if we move the Start point or the End point, the way the arrow is drawn should not change in such a way that the curve of the arrow suddenly skips if we exceed a value.

We need to find a feature that suits us. We have defined that if ΔY <200, we will slowly begin to solve the Y coordinate correction at the Control points. The correction will be highest if ΔY == 0 (a state where the Start point and the End point have the same Y coordinate).

When we then implement this function to correct the Y coordinates at the Control points, we ensure that for delta <200, the Control points move vertically and continuously according to the following function:

Representation of function y=a\left(0.9^{1.2^{\frac{x}{10}}}\right): (Nástroj: https://www.desmos.com/calculator)

At the same time, we need to slightly modify the function for calculating theControl points to take this adjustment into account:

export const calculateControlPoints = ({
  absDx,
  absDy,
  dx,
  dy,
}: {
  absDx: number;
  absDy: number;
  dx: number;
  dy: number;
}): {
  p1: Point;
  p2: Point;
  p3: Point;
  p4: Point;
} => {
  let startPointX = 0;
  let startPointY = 0;
  let endPointX = absDx;
  let endPointY = absDy;
  if (dx < 0) [startPointX, endPointX] = [endPointX, startPointX];
  if (dy < 0) [startPointY, endPointY] = [endPointY, startPointY];

  const fixedLineInflectionConstant = calculateFixedLineInflectionConstant(
    absDx,
    absDy,
  );
  const lowDyYShift = calculateLowDyControlPointShift(dx, dy); // Added

  const p1 = {
    x: startPointX,
    y: startPointY,
  };
  const p2 = {
    x: startPointX + fixedLineInflectionConstant,
    y: startPointY + lowDyYShift, // Changed
  };
  const p3 = {
    x: endPointX - fixedLineInflectionConstant,
    y: endPointY - lowDyYShift, // Changed
  };
  const p4 = {
    x: endPointX,
    y: endPointY,
  };

  return { p1, p2, p3, p4 };
};

At this point, the arrow is rendering better, although it’s still not perfect:

Initially, we tried to address this situation by dynamically moving the Control points, but we found that we would ideally need to add more Control points (to have more than a cubic Bezier curve). However, this complicates the whole arrow again, so in the end, we decided to solve this situation, according to Occam’s razor, in the simplest way that occurred to us. We made a straight line right next to the arrowhead:

Now everything looks as it should:

You can display the Control points and a bounding box to the curve in a fairly simple way. The curve then looks something like this:

Bounding box is highlighted with red dotted rectangle

The bounding box is highlighted with a red dotted rectangle

Example of how the Y coordinates of the Control points change dynamically

Interactions

One of the things we dealt with a lot is the interaction of the arrow. Fortunately, the SVG path component easily solves this problem, because functions such as onMouseEnteronMouseLeave, andonClick can be hooked on it. In our case, the line at the arrow is only 1px thick, which makes it an element that is very difficult to hit.

That’s why we’ve added another duplicate SVG path component, which differs in only two ways — it’s thicker and transparent.

This allows us to create a layer for interactions with a thickness of 10px, which is far easier to hover over than the original 1px curve.

Try arrows on your own

Parting words

There is still plenty of room for improvement, but it’s far beyond the scope of this series, and for our use case, this solution is just fine.

We tried to cover the whole process of implementing SVG arrows, including the caveats, edge cases, and hacks we used.

We hope we will be able to save you some time during the implementation of your custom arrows. If you have any comments or suggestions for how we can improve, we will be happy to read them all!

The whole implementation of SVG arrows in React is available on CodeSandbox or Github.

Interested in joining our growing team? Well, we’re hiring across the board! Check out our careers page for the latest vacancies.

You might also like

Productboard expanding San Francisco engineering presence
Life at Productboard

Productboard expanding San Francisco engineering presence

Jiri Necas
Jiri Necas
Refactoring Productboard’s design system to the next level
Life at Productboard

Refactoring Productboard’s design system to the next level

Jonathan Atkins
Jonathan Atkins
The Role of Growth Engineering at Productboard: Significance, key skills, and responsibilities
Life at Productboard

The Role of Growth Engineering at Productboard: Significance, key skills, and responsibilities

Stuart Cavill
Stuart Cavill