Home > Back-end >  Why does <legend> behave differently from <div>?
Why does <legend> behave differently from <div>?

Time:05-16

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.

CodePudding user response:

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>

CodePudding user response:

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>

CodePudding user response:

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>

  • Related