The HTML form element does nothing to arrange its subordinates in an appropriate layout. We must do it ourselves. Such layout expectations could be specified as:
Fields and Sets:
A form can contain a mix of fields and field-sets, where a field-set is a titled vertically arranged group of fields.
Horizontal Layout:
Portrait-format (mobile): one-column table, label on top, input field below, both in same table-cell.
Exception checkbox: label right, box left.
Landscape-format (desktop): two columns, label in left column, input field in right.
Label is right aligned, so that it is near to the field it describes.
Exception checkbox: label right, box left.
Newspaper layout: to exploit width, distribute fields across several horizontally ordered boxes.
Vertical Layout:
A left-side label should be on same height as its right-side input field, vertically aligned "middle".
In case the field or field-set is much higher than its left-side label, the label should be aligned to the upper edge of the field or field-set.
Nested Layouts:
A field can be organized as several fields with its own layout, examples are "Date of Birth" with "Age" output, or "Year", "Month" and "Day" input fields in a row.
In this article I will introduce CSS implementing this. I won't yet cover the checkbox case, and also I won't think about "newspaper layout" (distributing label/field pairs across several columns when having a restricted height). You can visit the current state of this project on my homepage.
Form Sample
Here is a HTML form example that contains all fields available from HTML-5 browsers, so that we can examine whether the layout works for all types of fields. This is also to try out the field-features your browser provides.
E.g. Firefox doesn't support datetime-local, week, month fields by now. Chrome supports all, but paints box-shadow for radio-buttons badly, and the default shape of fields is ugly.
Use the ☞ button to toggle the layout from desktop ("left-label") to mobile ("top-label"). Use the ↔ button to stretch the form to full width.
Notice that required field labels are also bold for field-sets, in case the set contains at least one required field. Example is the "More Text" group. On the other hand there is no required field in the "Date / Time" group, thus its label is not bold.
Core Form Layout CSS
I will present this in small doses. You can find the entire source code of the example on bottom of the article.
Most important to know is that I used line-height to get rid of vertical alignment problems (still open problems!), and box-sizing: border-box; to stop fields hanging out at right side.
About CSS coding: I recommend to write comments on any CSS line, at least one per rule-set. Only this keeps CSS code maintainable! Remember that a rule like position: relative; can serve several purposes, so we need to make explicit why we apply it.
/* Add colon after any label. */ formlabel::after { content:":"; } /* But not on label after radio. */ forminput[type='radio']+label::after { content:""; }
The label should have a smaller font than the field. This also will uncover vertical alignment problems in the layout. Further this CSS sets a colon after the label, and avoids it for radio buttons where the label is right of the button.
Labels of Required Fields
The JavaScript I introduced in my recent Blog copies the attribute required from field to label, the CSS below relies on that.
/* Set asterisk before required fields. */ formlabel[required]::before { content:"* "; }
/* Make labels for required fields better visible. */ formlabel[required] { font-weight:bold; }
This is for setting an asterisk (*) before any label of a required field, and displaying it in a bold font.
DIV Table Layout
form.table { display: table; width:100%; /* if parent is 100%, go with it */ }
This defines the table-layout inside the form, not including the "Submit" and "Reset" buttons. It will always fill its parent's width fully, so you should wrap it into either a display: inline-block; (natural width) or a display: block; (full width) DIV container.
The table-row receives its line-height here, important to avoid vertical alignment problems. The table-cell is not yet defined, it will be set specifically later for either mobile or desktop.
Layout of Fields Inside Tables
/* Stretch fields to full cell width, except checkboxes and radios. */ form.table>div>input:not([type='radio']):not([type='checkbox']), form.table>div>textarea, form.table>div>select { width:100%; }
/* Avoid text fields hanging out at right due to content-box sizing. */ form.tableinput, form.tabletextarea, form.tableselect { box-sizing:border-box; }
/* Let the user resize big fields vertically. */ form.tableselect[multiple], form.tabletextarea { resize: vertical; }
Fields should fill their cell's full width, not keep their "natural" width which is browser-specific. Force the browser to calculate their border-box, not content-box, else they will hang out at right side. Optionally we should allow the user to resize choosers that may contain lots of rows, like <textarea> and <select multiple>.
margin-top:0.2em; /* vertical line distance */ margin-bottom:0.2em; }
form.table.left-label>divlabel { text-align:right; padding-right:0.2em; /* distance to right field */
vertical-align:top; /* necessary for labels of nested fieldsets */ }
So here is a list of element names that can be inside a table-cell. For a desktop-suited table with two columns, all these elements should behave as table-cell. We allow beside input, select and textarea also fieldset and div elements. Their vertical alignment should be "middle" by default. To create a little space between layout rows there is margin at bottom.
Labels should be aligned right in left-label variant, to be as near as possible to the field, but nevertheless keep a little padding-distance. For labels we overwrite the default vertical alignment with "top" here, because in case a whole fieldset (high) is on right side, we want the label to stay in a visible range.
Mind that there are no further rules for the top-label layout variant. As any input field has been stretched to 100%, its preceding label will automatically go to top. As a side-effect top-label will be the default, unless you put a CSS class="left-label" onto the DIV table root-element.
Optional Additions
Following introduces useful code to style initial validation, button bars and nested right-side containers (horizontal bars).
Visible Initial Validation
form:not(output):not(fieldset):invalid/* fieldset would also be red */ { box-shadow:1px1px1pxred; }
If you want to make the validation markers already visible on document-load, add this rule-set. By default HTML forms would be validated by the browser only on a submit-event, and only then you would see the browser-made validation markers and tips.
Buttons and Toolbar
forminput[type='submit'], forminput[type='reset'] { margin-top:0.4em; /* space to form above */ font-size:140%; /* using Unicode character as icon */ width:33%; /* take a third of available space */ }
/* Variant label-top: put "Ok" to very right, "Cancel" will be at very left. */ form:not(.left-label)+.button-containerinput[type='submit'] { float:right; }
/* Variant label-left: align "Cancel" and "Ok" to middle. */ form.left-label+.button-container { text-align:center; }
Try out of you like this appearance of buttons and their bar. It is different for top-label and left-label variants. Such things depend on ergonomics, current fashion, company-rules, and last not least customer wishes. The "Submit" button right now establishes slowly, but still buttons top or bottom is undecided.
Horizontal Layout
.horizontal-bar/* flex-direction row */ { display: flex !important; /* else overwritten by table-cell! */ align-items:center; /* do not stretch vertically */ }
.horizontal-bar-natural/* natural size */ { flex: initial; } .horizontal-bar-stretch/* takes all available space */ { flex:auto; }
This is a general purpose layout done through flexbox. Look at the "Date" - "Years Since" fields above for an example. It is a frequent case that you have several fields in a line, and one of them should take the most available space. Put a CSS class="horizontal-bar" onto the container DIV, and one of class="horizontal-bar-natural" (just once!) or class="horizontal-bar-stretch" onto the elements in row.
General Purpose CSS
/* Enable a fluid responsive display of forms and fieldsets. */ div.responsive { display:inline-block; /* take just the space needed */ vertical-align:top; /* align to top if any peer DIV is present */ }
/* Keep label and radio-button always on same layout row. */ .sameline { white-space:nowrap; }
/** Round border for fieldset. */ fieldset { border-radius:0.5em; }
If you want DIV containers to arrange themselves responsively side-by-side, you can set a CSS class="responsive" into it.
The CSS class="sameline" is necessary to tie radio buttons to their label in a way that never the label is in a different layout row than the button. This could be quite misleading! Look at this example:
The same when <span class="sameline"> was wrapped around label and input:
Entire Source Code
Press the arrow-button to see HTML and CSS source code of this example.
/* Add colon after any label. */ formlabel::after { content:":"; } /* But not on label after radio. */ forminput[type='radio']+label::after { content:""; } </style>
<style>/** Style labels of required fields. */
/* Set asterisk before required fields. */ formlabel[required]::before { content:"* "; }
/* Make labels for required fields better visible. */ formlabel[required] { font-weight:bold; } </style>
<style>/** DIV table layout. */
form.table { display: table; width:100%; /* if parent is 100%, go with it */ }
/* Stretch fields to full cell width, except checkboxes and radios. */ form.table>div>input:not([type='radio']):not([type='checkbox']), form.table>div>textarea, form.table>div>select { width:100%; }
/* Avoid text fields hanging out at right due to content-box sizing. */ form.tableinput, form.tabletextarea, form.tableselect { box-sizing:border-box; }
/* Let the user resize big fields vertically. */ form.tableselect[multiple], form.tabletextarea { resize: vertical; } </style>
margin-top:0.2em; margin-bottom:0.2em; /* vertical line distance */ }
form.table.left-label>divlabel { text-align:right; padding-right:0.2em; /* distance to right field */
vertical-align:top; /* necessary for labels of nested fieldsets */ } </style>
<style>/* Layout for label-above variant. */ /* * Because any input field has been stretched to 100%, the * preceding label will automatically go to top. Nothing to do! */ </style>
<!-- Optional additions -->
<style>/** Make validation visible initially. */
form:not(output):not(fieldset):invalid/* fieldset would also be red */ { box-shadow:1px1px1pxred; } </style>
<style>/* Button and toolbar styles. */
forminput[type='submit'], forminput[type='reset'] { margin-top:0.4em; /* space to form above */ font-size:140%; /* using Unicode character as icon */ width:33%; /* take a third of available space */ }
/* Variant label-top: put "Ok" to very right, "Cancel" will be at very left. */ form:not(.left-label)+.button-containerinput[type='submit'] { float:right; }
/* Variant label-left: align "Cancel" and "Ok" to middle. */ form.left-label+.button-container { text-align:center; } </style>
<style>/** General purpose horizontal bar. */
.horizontal-bar/* flex-direction row */ { display: flex !important; /* else overwritten by table-cell! */ align-items:center; /* do not stretch vertically */ }
.horizontal-bar-natural/* natural size */ { flex: initial; } .horizontal-bar-stretch/* takes all available space */ { flex:auto; } </style>
<style>/** General purpose layout styles. */
/* Enable a fluid responsive display of forms and fieldsets. */ div.responsive { display:inline-block; /* take just the space needed */ vertical-align:top; /* align to top if any peer DIV is present */ }
/* Keep label and radio-button always on same layout row. */ .sameline { white-space:nowrap; }
<script> /** * Avoid duplication of "required" and "id" attributes in HTML forms. * Any LABEL before an INPUT must get the INPUT's "id" value into its "for" attribute. * Any LABEL before an INPUT must copy the INPUT's "required" if it exists. * Sometimes the "required" attribute is not in the next element but in its sub-DOM. */ function manageIdAndRequired() { var count =1; /* sequence number for unique ids */
/** Searches for "required" in given element and all sub-elements. */ function containsRequired(element) { if (element.getAttribute("required") !==null) returntrue;
var children = element.children; for (var i =0; i < children.length; i++) if (containsRequired(children[i])) returntrue;
returnfalse; } /* end containsRequired() */
/** Copies the optional "required" attribute to LABEL, chains it with INPUT's id. */ function chainLabelToIdAndCopyRequired(label) { var nextSibling = label.nextElementSibling; var nextTagName = nextSibling ? nextSibling.tagName :undefined;
/* previous is just for radio-buttons */ var previousSibling = label.previousElementSibling; var previousTagName = previousSibling ? previousSibling.tagName :undefined; var isRadioLabel = (previousTagName ==="INPUT");
if (isRadioLabel || nextTagName ==="INPUT"|| nextTagName ==="SELECT"|| nextTagName ==="TEXTAREA"|| nextTagName ==="DIV"|| nextTagName ==="FIELDSET") { if ( ! isRadioLabel && containsRequired(nextSibling)) label.setAttribute("required", ""); /* copy attribute to non-radios */
if (label.getAttribute("for") ===null) { /* chain id */ var sibling = isRadioLabel ? previousSibling : nextSibling; var id = sibling.getAttribute("id");
if ( ! id && sibling.getAttribute("name")) { /* generate id from name */ var identity ="_"+count; count++; id = sibling.getAttribute("name")+identity; sibling.setAttribute("id", id); }
if (id) label.setAttribute("for", id); } } } /* end chainLabelToIdAndCopyRequired() */
/* Iterate all forms on page */ var forms =document.getElementsByTagName("form"); for (var f =0; f < forms.length; f++) { var elementsInForm = forms[f].getElementsByTagName("*"); for (var i =0; i < elementsInForm.length; i++) { var element = elementsInForm[i]; if (element.tagName ==="LABEL") chainLabelToIdAndCopyRequired(element); } } }
/* Execute function on page-load */ window.addEventListener("load", manageIdAndRequired);
</script>
</head>
<body>
<form> <divclass="responsive"> <fieldset> <legend>HTML Form Layout Example</legend>
</div><!-- end table --> </fieldset><!-- end cell --> </div><!-- end row -->
<div> <label>Other Inputs</label>
<fieldset> <divclass="table left-label">
<div> <label>Radiobuttons</label> <div> <!-- span.sameline: don't let the label and the button separate to different layout rows --> <spanclass="sameline"><inputname="gender"type="radio"value="male"required><label>Male</label></span> <spanclass="sameline"><inputname="gender"type="radio"value="female"required><label>Female</label></span> <spanclass="sameline"><inputname="gender"type="radio"value="other"required><label>Other</label></span> </div> </div>
<div> <label>More Radiobuttons</label> <div> <!-- div: display each radio-button on a new layout row --> <div><inputname="RGB"type="radio"value="red"><label>Red</label></div> <div><inputname="RGB"type="radio"value="green"><label>Green</label></div> <div><inputname="RGB"type="radio"value="blue"><label>Blue</label></div> </div> </div>