home about me

border-radius for the content-box

As CSS border-radius will only work in the place the border is supposed to appear - between margin and padding - we need to come up with a more creative solution if we want a round edge where the content is.

The Problem

Working with Vuetify 3’s V-Main component, it became noticeable that this main wrapper would always span the full width and height, but have a dynamic padding based on the surrounding menus. Because of outside constraints, I was also unable to add another wrapping element.

Quick mockup - hover over it, or check the code in devtools:

.topnav
.sidenav
.main

Since .main expands below .topnav and .sidenav, setting a border radius would not work here, because it would be hidden by the navigation elements.

Solution 1

The preferrable solution would be to set the background color to the nav menu color, add another wrapping element inside .main, give that a border-top-left-radius and hide the overflow. This wasn’t possible in my situation, but I was able to add a helper element inside .main.

Using this element, an SVG helper element and some CSS, we can simulate the border-radius effect with clip-path.

.topnav
.sidenav
.main

For this, we’re using an SVG to define the path that should be filled with the navigation background color:

<svg height="0" width="0">
  <defs>
    <clipPath id="edge-clippath" clipPathUnits="objectBoundingBox">
      <path d="M.5,0H0v.5C0,.224,.224,0,.5,0z"></path>
    </clipPath>
  </defs>
</svg>

clipPathUnits="objectBoundingBox" is necessary for the path to scale with the element. Then, we create a square filled with the navigation background color, and clip it to only show the part from the SVG definition:

.rounded-edge {
  /* this is the border radius we want to simulate */
  --border-radius: 10px;
  /* our pseudo elements need to be of this size */
  --border-element-size: calc(var(--border-radius) * 2);

  /* this will position it at the top left corner of .main's padding's start */
  position: absolute;
  left: 0;
  top: 0;

  /* since it'll be overlaying other elements, ignore mouse clicks */
  pointer-events: none;

  /* by inheriting .main's padding, its content will start in the same position */
  padding: inherit;

  /* don't set z-index by hand, use an SCSS helper! */
  z-index: 2;
}

.rounded-edge::before {
  /* navigation background color */
  background: var(--navigation-background-color);

  content: '';
  display: block;
  
  /* this uses the path given in the SVG definition to clip the rendered content */
  clip-path: url(#edge-clippath);

  height: calc(var(--border-element-size));
  width: calc(var(--border-element-size));
}

Solution 2

If using clip-path is not possible, we can come up with a more complex solution. This has the drawback of possibly occluding anything that is in the top left corner of .main.

.topnav
.sidenav
.main

The rounded border helper element inside it gets these CSS definitions:

.rounded-edge {
  position: relative;

  /* everything else the same as in solution #1 */
  /* ... */
}

.rounded-edge::after,
.rounded-edge::before {
  content: '';
  position: absolute;
}

.rounded-edge::before {
  /* the color of the surrounding navigation's background */
  background: var(--navigation-background-color);

  /*
    the parent .rounded-edge element is exactly as big
    as the padding on .main, so we need to move to the
    bottom and right by half our border element's
    desired size
  */
  bottom: calc(-1 * var(--border-element-size) / 2);
  right: calc(-1 * var(--border-element-size) / 2);

  /*
    this is a square exactly half the border element's length and width,
    starting at the top left corner
  */
  height: calc(var(--border-element-size) / 2);
  width: calc(var(--border-element-size) / 2);
}

.rounded-edge::after {
  /* color of the .main element's background */
  background: var(--main-background-color);

  /* this clips a circle */
  border-radius: 50%;

  /*
    the parent .rounded-edge element is exactly as big
    as the padding on .main, so we need to move to the
    bottom and right by exactly our border element's
    desired size
  */
  bottom: calc(-1 * var(--border-element-size));
  right: calc(-1 * var(--border-element-size));

  height: var(--border-element-size);
  width: var(--border-element-size);
}

What we are doing here is creating a square filled with the background of the navigation elements, and overlaying a circle filled with the .main element’s background color.

To see how and why this works, take a look at this enlargened color-shifted demonstration:

.topnav
.sidenav
.main