Consistent, cross-browser legend positioning within fieldsets

Mar 22, 2011

The <legend> element has always been something of an enigma. When placed inside a <fieldset> element, it alters the fieldset box by reducing its height and positioning the content of the legend within a break in the fieldset border.

What makes this so enigmatic is that the positioning cannot fully be described using CSS. The most obvious aberration being the interruption in the parent element's border even though no background colour need be applied to the legend to achieve the effect. Another reason is that in the Opera and Firefox 3.x browsers, <fieldset> elements are somehow magical and resist the application of certain CSS border effects like border-radius.

The rationale for the <legend> element most likely arose from the early DOS days where "graphical" interfaces were composed of cleverly integrated box-drawing glyphs. Often the title of each box was embedded in the upper border, similar to this example:

╔═ Title ══════════════════════╗
║ Box content                  ║
╚══════════════════════════════╝

Porting the same sort of interface to web documents seemed a no-brainer, however the exact implementation was largely left up to the browser vendors. The HTML4.01 specification describes the legend element as allowing "authors to assign a caption to a FIELDSET. The legend improves accessibility when the FIELDSET is rendered non-visually." Surprisingly, that's about it. No description at all of what the legend should look like when rendered visually.

So what we ended up with is a mish-mash of implementations across the major browsers which all generally do the same thing, but pretty much all extra CSS modification of these elements is undefined. Because of this lack of specification, the visual display of fieldsets and legends can never be a significant part of any Acid test for example.

What we really need is a pure CSS definition of the legend when its contained within a fieldset. A consistent implementation across all browsers will allow web authors greater control over their forms and how the accessibility elements (such as <legend>) appear when visual rendering is enabled. To research this, we'll start with a plain element instead of a <legend> and see if we can make it look the "same".

According to the HTML4.01 specification, the <legend> element is inline, so we'll use a <span> element as our psuedo-legend.

<fieldset>
  <span class="legend">Legend</span>
  This is the first line of content.
</fieldset>

Using just margins in plain CSS, we can easily position the <span> element where the legend should be, however there are two big problems. The first problem is that a plain CSS solution cannot account for changes in font-sizes, padding or margins when positioning the span and resizing the fieldset. Any changes you make to these properties on the fieldset or legend itself are very likely to make the whole thing look wrong. Just like a stopped clock is right two times a day, a plain CSS solution is only right for one set of starting conditions. The actual solution is one that is calculated based on the current conditions. The pseudo-CSS would look something like this:

fieldset {
  [if contains a legend]
    margin-top:[current margin-top] + [legend offsetHeight / 2];
    height:[current height] - [legend offsetHeight / 2];
  [/end if]
}
fieldset legend {
  margin-top:[current margin-top] - ([parent padding-top] + [legend offsetHeight / 2]);
  margin-bottom:[current margin-bottom] + [parent padding-top];
}

To implement dynamic values like these, we need to resort to Javascript.

window.onload = function() {
  var fieldsets = document.getElementsByTagName('fieldset');
  for (var x = 0, foo, fooCS = {}, barCS = {}; foo = fieldsets[x++];) {
    var bar = foo.getElementsByTagName('span')[0];
    if (bar && bar.className.match(/\blegend\b/)) {
      if (!window.getComputedStyle) {
        for (var prop in foo.style)
          fooCS[prop] = foo.currentStyle[prop];
      } else fooCS = window.getComputedStyle(foo, null);
      foo.style.marginTop = parseInt(fooCS.marginTop) + Math.ceil(bar.offsetHeight / 2) + "px";
      foo.style.height = parseInt(fooCS.height) - Math.ceil(bar.offsetHeight / 2) + "px";

      if (!window.getComputedStyle) {
        for (var prop in bar.style)
          barCS[prop] = bar.currentStyle[prop];
      } else barCS = window.getComputedStyle(bar, null);
      bar.style.marginTop = (parseInt(barCS.marginTop) || 0) - (parseInt(fooCS.paddingTop) + Math.ceil(bar.offsetHeight / 2)) + "px";
      bar.style.marginBottom = (parseInt(barCS.marginBottom) || 0) + parseInt(fooCS.paddingTop) + "px";
    }
  }
};

The code above will properly position the <span> element just as the <legend> element would be. It also resizes the fieldset, just as it normally would in most browsers, to accommodate the added legend.

However, here we come to the second problem: The border of the fieldset passes right through the pseudo-legend like a strikethrough effect. We need some way to hide it, and the only reasonable way to do this is with CSS by applying a background colour to the legend element. This is an unfortunate, but necessary evil. In their arcane internal workings, browsers have their own means to interrupt the border for the legend without using a background colour, but these methods are not available to us. One could actually make the case that the mere presence of such methods which are basically used for only this one purpose, are a slight waste of resources.

In any case, to interrupt our border, we add a background colour, and also pad out the legend and enclosing fieldset so they look the same across all browsers:

fieldset {
  padding:4px 11px 13px 11px;
}
fieldset span.legend {
  display:table;
  background-color:#ffffff;
  padding:1px 2px 0px 2px;
}

At this point you might be wondering... Why display:table;? Well, there are few CSS display types that allow freely adding padding, are affected by negative margins, and collapse to fit their contents, all while not using CSS float. Using a display value of table here is a hack which allows our mock-up to behave the same way in all modern browsers. Of course the legend isn't a table, but it's an easy way out of a tricky situation!

With that addition (and assuming the fieldset is placed on a white background itself) this system generates a fieldset + legend lookalike that behaves the same way in all major browsers. This, as opposed to the wildly different default renderings currently possible when using fieldset and legend straight, especially in Internet Explorer.

I actually don't recommend you use this system, mostly because it destroys the accessibility benefits of using an actual legend element with a form. That being said, you could probably modify the javascript above to take fieldsets containing legends and swap them for ones containing modified spans, however that's outside the scope of this post.

My main conclusion is that the behaviour of fieldset and legend elements needs to be properly defined. Having browsers which natively employ a system as described here in javascript, would be beneficial to all developers hoping to retain their form accessibility as well as gain the freedom to style their forms without fear.

For your perusal I made an HTML page which contains all of the example code above and can be easily viewed in any browser.


Comments closed

Recent posts

  1. Customize Clipboard Content on Copy: Caveats Dec 2023
  2. Orcinus Site Search now available on Github Apr 2023
  3. Looking for Orca Search 3.0 Beta Testers! Apr 2023
  4. Simple Wheel / Tire Size Calculator Feb 2023
  5. Dr. Presto - Now with MUSIC! Jan 2023
  6. Archive