Faking a Fieldset-Legend
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:
- It’s actually transparent (backgrounds show through)
- The border remains middle-aligned with the “legend,” even when it breaks to multiple lines
- You can easily tweak its appearance via CSS custom properties
So, how’s it work?
HTML
Our markup consists of three elements:
- An outer container (our fake
<fieldset>) - A heading (our fake
<legend>) - A wrapper for the inner content
<div class="legendary">
<h3>This is not a fieldset</h3>
<div>
<!-- content -->
</div>
</div>Code language: HTML, XML (xml)
A few quick notes:
- This pattern won’t rely on specific elements. You should use whatever containers, heading levels, etc. make the most semantic sense for your use case.
- The inner content wrapper may not be necessary if you’re willing to accept some compromises. (More on this later.)
- I stole the excellent class name from Mr. Kirk-Nielsen’s implementation. Thanks, Chris!
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.
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";
}Code language: CSS (css)
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";
}
Code language: CSS (css)
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;
}
Code language: CSS (css)
Now we can use a content view to render the aforementioned pseudo elements:
.legendary {
/* ... */
&::before,
&::after {
content: "";
}
}Code language: CSS (css)
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;
}
}Code language: CSS (css)
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;
/* ... */
}Code language: CSS (css)
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);
}
}
Code language: CSS (css)
(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!

Tyler Sticka has over 20 years of experience designing interfaces for the web and beyond. He co-owns Cloud Four, where he provides multidisciplinary creative direction for a variety of organizations. He’s also an artist, writer, speaker and developer. You can follow Tyler on his personal site, Mastodon or Bluesky.