New Jan 20, 2026

Faking a Fieldset-Legend

Company/Startup Blogs All from Cloud Four View Faking a Fieldset-Legend on cloudfour.com

My buddy Christopher Kirk-Nielsen wanted to mimic the look of a <legend> inside a <fieldset> for a section of a blog post: Specifically, the way the <legend> element magically overlays and partially clips the border of the containing <fieldset>.

Chris posed this challenge on Mastodon, where I suggested a solution he ended up building upon for his 2025 Yearnotes.

Here’s a refined version of the demo I shared:

A few details I’m proud of:

So, how’s it work?

HTML

Our markup consists of three elements:

<div class="legendary">
  <h3>This is not a fieldset</h3>
  <div>
    <!-- content -->
  </div>
</div>

A few quick notes:

CSS

Instead of struggling to overlay the “legend” while clipping the border beneath, we’re going to slice the containing shape into three chunks: One for either side of our legend, and one for everything below.

Hand-drawn sketch of the intended layout, with pencil lines marking slices for the northwest and northeast corners in addition to legend and content elements

We already have elements for our legend and lower content section. To avoid cluttering the markup, we’ll use pseudo elements to represent the “northwest” and “northeast” slices.

First, let’s translate our sketch to a CSS Grid. I like to use grid-template-areas to make a little text-based representation of the layout:

.legendary {
  display: grid;
  grid-template-areas:
    "nw      legend  ne"
    "content content content";
}

To keep our legend middle-aligned to the top of the adjacent borders, we’ll have it span an additional row (one earlier than the corner areas):

.legendary {
  display: grid;
  grid-template-areas:
    ".       legend  ."
    "nw      legend  ne"
    "content content content";
}

We should also add some column and row definitions so the browser knows to divide the legend space evenly, and to stretch the northeast corner (right of the legend):

.legendary {
  display: grid;
  grid-template-areas:
    ".       legend  ."
    "nw      legend  ne"
    "content content content";
  grid-template-columns:
    1em
    auto
    minmax(1em, 1fr);
  grid-template-rows: 1fr 1fr auto;
}

Now we can use a content view to render the aforementioned pseudo elements:

.legendary {
  /* ...  */

  &::before,
  &::after {
    content: "";
  }
}

And assign the grid areas we’ve defined:

.legendary {
  /* ...  */

  &::before {
    grid-area: nw;
  }

  &::after {
    grid-area: ne;
  }
  
  > :first-child {
    grid-area: legend;
  }

  > :last-child {
    grid-area: content;
  }
}

Now for the visual appearance!

Since this technique hinges on coordinating the same styles across separate elements, we’ll define a few custom properties up top:

.legendary {
  --border-color: currentColor;
  --border-radius: 0.25em;
  --border-style: solid;
  --border-width: 1px;
  --legend-gap: 0.375em;
  --padding: 1em;

  /* ... */
}

Which we’ll pepper throughout our final styles to draw borders and manage spacing:

.legendary {
  --border-color: currentColor;
  --border-radius: 0.25em;
  --border-style: solid;
  --border-width: 1px;
  --legend-gap: 0.375em;
  --padding: 1em;

  column-gap: var(--legend-gap);
  display: grid;
  grid-template-areas:
    ".       legend  ."
    "nw      legend  ne"
    "content content content";
  grid-template-columns:
    calc(var(--border-width) + var(--padding) - var(--legend-gap))
    auto
    minmax(calc(var(--border-width) + var(--padding) - var(--legend-gap)), 1fr);
  grid-template-rows: 1fr 1fr auto;

  &::before,
  &::after,
  > :last-child {
    border: var(--border-width) var(--border-style) var(--border-color);
  }

  &::before,
  &::after {
    border-bottom-width: 0;
    content: "";
  }

  &::before {
    border-right-width: 0;
    border-top-left-radius: var(--border-radius);
    grid-area: nw;
  }

  &::after {
    border-left-width: 0;
    border-top-right-radius: var(--border-radius);
    grid-area: ne;
  }
	
  > :first-child {
    font: inherit;
    grid-area: legend;
    margin: 0;
  }
	
  > :last-child {
    border-bottom-left-radius: var(--border-radius);
    border-bottom-right-radius: var(--border-radius);
    border-top-width: 0;
    grid-area: content;
    padding-top: var(--padding);
  }
}

(Note the use of calc in the first and last columns. This keeps the main content aligned with that of the heading while taking into account gaps between the legend and border.)

Variations

Depending on the needs of your project, there may be ways to adjust or simplify this technique.

If your background is a flat color and known ahead of time, you can give the legend the same background and use a faux container instead of separate corners:

A similar trick could work for varied backgrounds if you’re willing to set a blend mode (and accept any resulting color shifts):

And if minimal markup is the goal, you can pull this off without the inner <div> element, it’ll just impose a few more constraints:

We may one day get a CSS feature for mimicking <legend>’s display (as pointed out by Amelia Bellamy-Royds in the original thread). For now, it’s another fun excuse to solve an interesting (if eerily familiar) challenge with the niceties of modern CSS!


We’re Cloud Four

We solve complex responsive web design and development challenges for ecommerce, healthcare, fashion, B2B, SaaS, and nonprofit organizations.

See our work

Scroll to top