3

Consider this simple example, where we wish to layout a <legend> and its associated input content side-by-side, with no border, rather than using the default <fieldset> display. First, the markup that works as intended:

fieldset {
  border: 0;
  display: flex;
}
<form>
  <fieldset>
    <div>
      <legend>Choose your favorite monster</legend>
    </div> 

    <div>
      <input type="radio" id="kraken" name="monster">
      <label for="kraken">Kraken</label><br />

      <input type="radio" id="sasquatch" name="monster">
      <label for="sasquatch">Sasquatch</label><br />
    </div>
  </fieldset>

</form>

Now we think, "Isn't that <div> around the <legend> superfluous?" That is, can't we simply remove it, change the <legend> to display:block, and expect the same behavior?

It turns out we cannot:

fieldset {
  border: 0;
  display: flex;
}

legend {
  display: block;
}
<form>
  <fieldset>
    <legend>Choose your favorite monster</legend>

    <div>
      <input type="radio" id="kraken" name="monster">
      <label for="kraken">Kraken</label><br />

      <input type="radio" id="sasquatch" name="monster">
      <label for="sasquatch">Sasquatch</label><br />
    </div>
  </fieldset>

</form>

It now displays top-to-bottom, rather than side-by-side? But why? And is there a way to make it display side-by-side without the extra <div> around the <legend>?

EDIT:

Please note: I am looking for a solution that allows me to layout the two elements ("legend" and "input content") using flexbox. In particular, floating the <legend> is not a valid solution for my use case.

Jonah
  • 15,806
  • 22
  • 87
  • 161
  • 5
    Well, wrapping the [`` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend) with any element other than a [`
    `](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset) renders the HTML invalid, since its: "*[permitted parents are a] `
    ` whose first child is this `` element.*"
    – David Thomas May 15 '22 at 16:12
  • Interesting, I didn't notice that. Surely there must be a way to achieve the desired layout with valid html though? – Jonah May 15 '22 at 16:17
  • It's sort of interesting; effectively the `` has no implicit Aria role, and no permitted Aria roles, so I'm not sure of its benefit as regards accessibility or semantics. I feel it could be replaced, in most situations, with a simple `
    `, or `

    `, `

    `... element. As for achieving your layout, I honestly don't know as yet.

    – David Thomas May 15 '22 at 16:20
  • "accessibility or semantics" Not an expert here, but while the lack of Aria roles might remove any accessibility benefit, semantically it at least expresses your intention more clearly than a `
    `, `
    `, etc, right?
    – Jonah May 15 '22 at 16:24
  • Yeah, I'm very much a non-expert myself. I feel - as you say - it has a role to play, but given its lack of Aria significance/use I'm not sure what that role is, exactly. – David Thomas May 15 '22 at 16:28
  • 1
    @DavidThomas Legends are read by screen readers such as JAWS when navigating inputs in "forms" mode, I believe. How that maps to wai-aria, if at all, I don't know. – Alohci May 15 '22 at 17:05
  • @Alohci Would using a `
    ` / `

    ` / etc here still be considered a valid, semantically correct solution?

    – Jonah May 15 '22 at 17:07
  • 1
    @Jonah. That's an interesting question. Either would be HTML5 conforming/valid. Semantically, `
    ` provides a heading for sectioning elements and fieldset isn't one of those, so we can rule that out. `` are general heading elements and probably provide satisfactorily semantics. I can recall the original editor of HTML5 complaining about the number of different header elements in HTML, each with their own use cases, and `legend` exists only because of its special behaviour when used with fieldset, in particular, its screen reader behaviour, which using `

    ` would not achieve.

    – Alohci May 15 '22 at 17:23

3 Answers3

3

Set the legend to float:left.

The rules for fieldset are somewhat "magic", but are described in the HTML5 rendering section.

If the [fieldset] element's box has a child box that matches the conditions in the list below, then the first such child box is the 'fieldset' element's rendered legend:

  • The child is a legend element.
  • The child's used value of 'float' is 'none'.
  • The child's used value of 'position' is not 'absolute' or 'fixed'.

position:absolute and position:fixed causes other issues. But float is perfect, because, since it's a flex item, it won't actually behave like a float, but as a regular flex item. I've added justify-content:space-around to demonstrate that that's actually happening.

fieldset {
  border: 0;
  display: flex;
  justify-content:space-around;
}

legend {
  display: block;
  float:left;
}
<form>
  <fieldset>
    <legend>Choose your favorite monster</legend>

    <div>
      <input type="radio" id="kraken" name="monster">
      <label for="kraken">Kraken</label><br />

      <input type="radio" id="sasquatch" name="monster">
      <label for="sasquatch">Sasquatch</label><br />
    </div>
  </fieldset>

</form>
Alohci
  • 78,296
  • 16
  • 112
  • 156
  • Sorry, I should have been more specific, but I don't want to float it. I'll update the OP. – Jonah May 15 '22 at 16:29
  • The legend won't actually float, because it's a flex item. – Alohci May 15 '22 at 16:34
  • Hmmm... ok, I am trying to parse your updated explanation. If it's not actually floating, what is the `float:left` doing? Also, based on what you quoted from the spec ("The child's used value of 'float' is 'none'"), does this `float:left` strategy somehow invalidate it from being a "proper legend"? – Jonah May 15 '22 at 16:36
  • It's not floating, it's just breaking the rules that control the fieldset/legend rendering behaviour. - Semantically, it _should_ still be a legend. Sometimes, sadly, browsers do break semantics on styling, which they shouldn't do. You'll have to check whether it remains "proper" to whatever definition of "proper" you're using. – Alohci May 15 '22 at 16:41
  • "It's not floating, it's just breaking the rules that control the fieldset/legend rendering behaviour" Can you break down for me (if you know) exactly why the `float:left` is not floating here, but _is_ making the flexbox work as expected? ; "to whatever definition of 'proper' you're using." I'm just using the definition you quoted. Maybe I misunderstood, but it sounded like it was saying it can only _be_ the "fieldset's rendered legend" if the float is none. So that `float:left` is making it technically no longer the fieldset's legend.... – Jonah May 15 '22 at 16:46
  • The point is that it's in the "Rendering" section of the HTML5 spec. So it _should_ only be affecting the styling. – Alohci May 15 '22 at 16:49
  • I've added a link to the relevant section of the specification, so you can read around the quote. You can see that it's discussing the treatment of CSS boxes, not semantics. – Alohci May 15 '22 at 16:53
  • Ok, I understand now what you're saying re: rendering, so the "not a proper legend" concern is a non-issue. And per your updated example it is clearly doing what I want. I'd still like to know _why_ it's necessary. I mean, it just seems so random that it requires a `float:left` in _this specific context_ to "activate" the normal, expected flexbox behavior. Why did you think to try that in the first place? – Jonah May 15 '22 at 16:54
  • 1
    As I say, Flexbox/Legend rendering is historically just "magic". Chances are, the float and positioning breaking of that "magic" rendering happened years before flexbox was invented. When flexbox was created the rules just persisted. Float broke the "magic" and flexbox just stops the floating behaviour from applying to flex items. – Alohci May 15 '22 at 17:01
  • Thanks for walking me through it. I'm going to keep the question open another day or two just in case anyone has more info, then I'll accept this answer. – Jonah May 15 '22 at 17:04
  • One final plot twist: This solution does not work in Safari :(, which is probably a Safari bug. The non-conforming `
    ` solution from my OP does, however.
    – Jonah May 15 '22 at 17:31
3

display: contents might be the best candidate here as it will remove the tag to keep only its content so no more issue with the behavior of <legend> tag. Then the flexbox algorithm will automatically make the text as an anonymous flex item so your legend is a flex item.

The element itself does not generate any boxes, but its children and pseudo-elements still generate boxes and text runs as normal. For the purposes of box generation and layout, the element must be treated as if it had been replaced in the element tree by its contents ref

fieldset {
  border: 0;
  display: flex;
}

legend {
  display: contents;
}
<form>
  <fieldset>
    <legend>Choose your favorite monster</legend>

    <div>
      <input type="radio" id="kraken" name="monster">
      <label for="kraken">Kraken</label><br />

      <input type="radio" id="sasquatch" name="monster">
      <label for="sasquatch">Sasquatch</label><br />
    </div>
  </fieldset>

</form>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • cannot test on Safari – Temani Afif May 15 '22 at 21:17
  • Very nice. This solution _does_ work on Safari as well. – Jonah May 15 '22 at 21:19
  • 1
    @Jonah for a safer result you can wrap all the text inside a `span` to make sure there is no strange behavior and to avoid relying on anonymous flex-item. The span inside the legend will become your flex item – Temani Afif May 15 '22 at 21:28
  • 1
    Brilliant work. I like `legend { display: contents }` the best of all the solutions on this page. – Rounin May 16 '22 at 17:33
  • @Rounin-StandingwithUkraine Agreed. From the accessbility POV, there is this unfortunate note from the MDN docs: "Current implementations in most browsers will remove from the accessibility tree any element with a display value of contents (but descendants will remain). This will cause the element itself to no longer be announced by screen reading technology. This is incorrect behavior according to the CSS specification." So the approach appears technically sound, but practically (as of now) imperfect. I still think it's the best of the options. – Jonah May 16 '22 at 19:34
  • 1
    @Jonah - Perhaps you could combine (somehow) Temani Afif's `legend { display: contents }` above with my `aria-describedby` markup, such that the `` remains for sighted visitors and the ARIA element (styled with `height: 0;` or similar) provides the accessibility? (There might be some really clever way to *make* the `` the ARIA element if `aria-describedby` puts it back into the accessibility tree...?) – Rounin May 16 '22 at 19:38
2

Approach 1 (the floating <legend> hack)

As @Alohci discovered, when using <legend>, the float: left hack is (still) unavoidable.

See this answer by @BorisZbarsky from 2011, more than a decade ago and long before CSS Flexbox was properly established:

Legends are special. In particular, their default rendering can't be described in CSS, so browsers use non-CSS means of rendering them. What that means is that a statically positioned legend will be treated like a legend and be separate from the actual content of the fieldset.

Source: Why won't my <legend> element display inline?

Since a float: left declaration cannot be avoided, the most concise CSS I came up with is:

fieldset {
  display: flex;
  border: none;
}

legend {
  float: left; /* Hack to prevent browsers applying special <legend> styling */
}

Working Example:

fieldset {
  display: flex;
  border: none;
}

legend {
  float: left; /* Hack to prevent browsers applying special <legend> styling */
}

legend::after {
  content: ':';
}
<form>
  <fieldset>
    <legend>Choose your favorite monster</legend>

    <div>
      <input type="radio" id="kraken" name="monster">
      <label for="kraken">Kraken</label><br />

      <input type="radio" id="sasquatch" name="monster">
      <label for="sasquatch">Sasquatch</label><br />
    </div>
  </fieldset>

</form>

Further Reading:

This technical blogger ran up against the same issue. It seems like the unique positioning behaviour of <legend> has been an issue for a long time:


Approach 2 (the ARIA alternative)

The <legend> element has undisputed semantic value, but we can deploy:

  • aria-labelledby; or
  • aria-describedby

in another element (eg. <div>) to replicate the semantic value of <legend>.

If we swap out <legend> for <div id="my-legend"> and support with ARIA, we gain stylability without losing semantics.


Working Example:

fieldset {
  display: flex;
  border: none;
}

fieldset div:first-of-type::after {
  content: ':';
}
<form>
  <fieldset aria-describedby="my-legend">
    <div id="my-legend">Choose your favorite monster</div>

    <div>
      <input type="radio" id="kraken" name="monster">
      <label for="kraken">Kraken</label><br />

      <input type="radio" id="sasquatch" name="monster">
      <label for="sasquatch">Sasquatch</label><br />
    </div>
  </fieldset>
</form>
Rounin
  • 27,134
  • 9
  • 83
  • 108
  • 1
    Thanks for the additional context. One thing I noticed is that even the `float:left` hack does not work in Safari... and I was not able to find any other html-conforming solution, which makes me think that cross-browser non-default styling of fieldsets/legends might be a fool's errand... – Jonah May 15 '22 at 18:00
  • 1
    Sadly, you may be right, @Jonah. I am increasingly exasperated with Safari (and particularly iOS Safari). It seems every second new thing I come across doesn't work or _works differently_ in Safari. More than 10 years ago I made a conscious decision to ignore IE and IE users (which gave me a lot of time back) but I don't feel I can do the same with Safari users in 2022. I genuinely have no idea what has happened at Apple that Safari today is hell-bent on becoming the ugly step-sister that IE used to be. – Rounin May 15 '22 at 18:06
  • 1
    While reading around this subject, I came across an alternative to styling `` which is to use something like `
    ` and then add the attribute `aria-labelledby="legend-stylable"` to `
    `. I'm still getting my head around whether `aria-describedby` or `aria-labelledby` is the more appropriate attribute, but once I do, I'll add this alternative approach to the answer above.
    – Rounin May 15 '22 at 18:13
  • `
    ` is not a good suggestion. a) it's not reusable, b) id-based CSS rules can easily cause specificity problems, especially when used with a helper-class based CSS framework like Tailwind or Bootstrap.
    – connexo May 15 '22 at 18:40
  • 1
    @connexo - In this instance, the `id` attribute isn't for the benefit of CSS and it's not even for the benefit of JS. It's for the benefit of **HTML + ARIA**. You know how when you use the `for` attribute in HTML ` – Rounin May 15 '22 at 20:14
  • @connexo - Here's MDN discussing how the `for` and `id` attributes work together: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/for – Rounin May 15 '22 at 20:19