Check out the demo + source code here: https://codesandbox.io/s/lr8pv8zz4z

Beveled Corners with CSS + React

Oliver Baker
Headstorm
Published in
4 min readJul 15, 2019

--

Let’s talk corners. 90º angles are by far the most popular, and for good reason. Rounded corners are hip, modern, and spreading like wildfire, just like how they did in 1998 when the square stylings of the Apple II were softened and rounded in the first iMac.

But the future is bleak and bevels are the future. They’re classic science fiction and dominate many “future user interface” designs, which is why I put them at the top of my list for my FUI library. They’re also shown off in the updated Google Material Design Spec!

I went through a few rounds of trial and error to achieve the look but eventually landed on clip-path masking to hide the boring 90º corners to reveal a much more exciting angle. While this works and is very tidy CSS, it needs special attention when it comes to borders and shadows (see my previous post on this subject).

Clip-path is a feature in CSS that allows masking of an element based on some arbitrary geometry. In our case, we’ll be adding extra vertices around our corners like the paper in Battlestar Galactica.

Battlestar Galactica (2003)

The Code

Here is a basic styled div to get us started:

const BeveledDiv = styled.div`
background: white;
display: inline-block;
padding: 2rem;
`;
const App = () => (
<BeveledDiv>
Hello World
</BeveledDiv>
);

We’ll use a cornerAngle prop, and default it to 30º for now.

cornerAngle={30}

Degrees are easy to understand but we’ll need to convert to radians to keep the CSS cleaner.

cornerAngleRadian={(cornerAngle * Math.PI) / 180)}

Back to our component:

const BeveledDiv = styled.div`
${({ cornerAngleRadian }) => `
background: white;
display: inline-block;
padding: 2rem;
`}
`;
const App = () => {
const cornerAngle={30};
return (
<BeveledDiv
cornerAngleRadian={(cornerAngle * Math.PI) / 180)}
>
Hello World
</BeveledDiv>
);
};

To draw the polygon we’ll specify where the points will be using a styled-component. So let’s break out our unit-circles and we’ll pick the appropriate trig function to get the coordinates we need based on the radian angle.

(not to scale)
clip-path: polygon(
${Math.sin(cornerAngleRadian)}rem 0%,
...

Starting in the top-left corner, we put a point on the top side, offset from the left by the correct amount, and draw a line to the left side, all the way near the top, but offset again by the requisite amount. We then repeat this pattern by putting two vertices at each corner so that the line connecting them is at the proper angle.

calc(100% — .5rem) How cool is that?
clip-path: polygon(
${Math.sin(cornerAngleRadian)}rem 0%,
0% ${Math.cos(cornerAngleRadian)}rem,
0% calc(100% — ${Math.cos(cornerAngleRadian)}rem),
${Math.sin(cornerAngleRadian)}rem 100%,
calc(100% — ${Math.sin(cornerAngleRadian)}rem) 100%,
100% calc(100% — ${Math.cos(cornerAngleRadian)}rem),
100% ${Math.cos(cornerAngleRadian)}rem,
calc(100% — ${Math.sin(cornerAngleRadian)}rem) 0%
);

With this block of styled-component CSS we have beveled 30º corners! Success! But… while setting each angle to be the same angle was easy… easy isn’t fun, is it? Plus, I wanted arbitrary angles on each corner for my design like you can achieve with border-radii — like this:

cornerAngles={[0, 30, 0, 30]}

Sticking with the border-radius pattern, the order here is top-left, top-right, bottom-right, bottom-left. Now, we’ll need to map over this array in our radian conversion:

cornerAngleRadians={map(cornerAngles, a => (a * Math.PI) / 180)}

After that, our CSS logic just needs to point to the corresponding index in the array!

clip-path: polygon(
${Math.sin(cornerAngleRadians[0])}rem 0%,
0% ${Math.cos(cornerAngleRadians[0])}rem,
0% calc(100% — ${Math.cos(cornerAngleRadians[3])}rem),
${Math.sin(cornerAngleRadians[3])}rem 100%,
calc(100% — ${Math.sin(cornerAngleRadians[2])}rem) 100%,
100% calc(100% — ${Math.cos(cornerAngleRadians[2])}rem),
100% ${Math.cos(cornerAngleRadians[1])}rem,
calc(100% — ${Math.sin(cornerAngleRadians[1])}rem) 0%
);

So there we have it! Check out the code sandbox demo below — the source code should look pretty familiar by now. After the break, we’ll learn how to add borders and shadows to our masked elements.

Shadows and Borders in the World of Clip-path

Adding borders and shadows when we’re already masking the element is.. tricky. For instance, a shadow will just get masked out by our clip-path. To solve this, we add a div to wrap the masked element and then apply an SVG filter. For shadows, we could use the drop-shadow function:

//                  x   y   blur color
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.5));

What we lose with this approach is Internet Explorer support, and the ability to “spread” the shadow. Not ideal, but maybe not the end of the world for you.

What we really want are custom SVG filters for borders and shadows. Here is an example of a border filter which accepts a custom color and thickness:

<filter id="border"}>  <feMorphology
operator="dilate"
radius={borderWidth}
in="SourceAlpha"
result="thicken"
/>
<feFlood
floodColor={borderColor}
result="color"
/>
<feComposite
in="color"
in2="thicken"
operator="in"
result="outline"
/>
<feMerge>
<feMergeNode in="outline" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>

It’s also possible to do separate left/right/top/bottom borders by applying an feOffset to the alpha instead of simply using the feMorphology used above.

For more on doing good shadows, check out my medium post on the topic.

--

--

Oliver Baker
Headstorm

Team Lead at Headstorm · CSS Connoisseur · React Revolutionary