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.
- Portrait-format (mobile): one-column table, label on top, input field below, both in same table-cell.
- 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 likeposition: relative;
can serve several purposes, so we need to make explicit why we apply it.
Basic Label Styling
/* Smaller sans-serif font. */ form label { font-size: 76%; font-family: sans-serif; } /* Add colon after any label. */ form label::after { content: ":"; } /* But not on label after radio. */ form input[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. */ form label[required]::before { content: "* "; } /* Make labels for required fields better visible. */ form label[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 */ } form .table > div { display: table-row; line-height: 2em; /* needed for vertical line-alignment */ }
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 .table input, form .table textarea, form .table select { box-sizing: border-box; } /* Let the user resize big fields vertically. */ form .table select[multiple], form .table textarea { 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>
.
Left Label for Desktop
form .left-label > div > label, form .left-label > div > input, form .left-label > div > select, form .left-label > div > textarea, form .left-label > div > div, form .left-label > div > fieldset { display: table-cell; /* display elements horizontally */ vertical-align: middle; /* avoid vertical alignment problems */ margin-top: 0.2em; /* vertical line distance */ margin-bottom: 0.2em; } form .table.left-label > div label { 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: 1px 1px 1px red; }
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
form input[type = 'submit'], form input[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 */ } form input[type = 'submit'] { color: green; } form input[type = 'reset'] { color: red; } /* Variant label-top: put "Ok" to very right, "Cancel" will be at very left. */ form :not(.left-label) + .button-container input[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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 | <!DOCTYPE HTML> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>HTML Form Test</title> <!-- Core form layout --> <style> /** Style labels of fields. */ /* Smaller sans-serif font. */ form label { font-size: 76%; font-family: sans-serif; } /* Add colon after any label. */ form label::after { content: ":"; } /* But not on label after radio. */ form input[type = 'radio'] + label::after { content: ""; } </style> <style> /** Style labels of required fields. */ /* Set asterisk before required fields. */ form label[required]::before { content: "* "; } /* Make labels for required fields better visible. */ form label[required] { font-weight: bold; } </style> <style> /** DIV table layout. */ form .table { display: table; width: 100%; /* if parent is 100%, go with it */ } form .table > div { display: table-row; line-height: 2em; /* needed for vertical line-alignment */ } </style> <style> /** Layout 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 .table input, form .table textarea, form .table select { box-sizing: border-box; } /* Let the user resize big fields vertically. */ form .table select[multiple], form .table textarea { resize: vertical; } </style> <style> /** Layout for left-label variant */ form .left-label > div > label, form .left-label > div > input, form .left-label > div > select, form .left-label > div > textarea, form .left-label > div > div, form .left-label > div > fieldset { display: table-cell; /* display elements horizontally */ vertical-align: middle; /* avoid vertical alignment problems */ margin-top: 0.2em; margin-bottom: 0.2em; /* vertical line distance */ } form .table.left-label > div label { 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: 1px 1px 1px red; } </style> <style> /* Button and toolbar styles. */ form input[type = 'submit'], form input[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 */ } form input[type = 'submit'] { color: green; } form input[type = 'reset'] { color: red; } /* Variant label-top: put "Ok" to very right, "Cancel" will be at very left. */ form :not(.left-label) + .button-container input[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; } /** Round border for fieldset. */ fieldset { border-radius: 0.5em; } </style> <!-- Script to copy required and ids --> <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) return true; var children = element.children; for (var i = 0; i < children.length; i++) if (containsRequired(children[i])) return true; return false; } /* 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> <div class="responsive"> <fieldset> <legend>HTML Form Layout Example</legend> <div class="table"> <!-- <div class="table left-label"> --> <div> <label>Single Line</label> <input name="single-line-text" type="text" required value="Default value in text-field"> </div> <div> <label>Password</label> <input name="hidden-password" type="password" required> </div> <div> <label>Number</label> <input name="numeric" type="number" required> </div> <div> <label>More Text</label> <fieldset> <div class="table left-label"> <div> <label>E-Mail</label> <input name="e-mail" type="email" required value="a@b.c"> </div> <div> <label>Telephone</label> <input name="phone-number" type="tel" /> </div> <div> <label>URL</label> <input name="url-address" type="url" value="protocol://host"> </div> <div> <label>Auto-Suggest</label> <input list="browsers" name="auto-suggest"/> <datalist id="browsers"> <option value="Firefox"> <option value="Chrome"> <option value="Opera"> <option value="Safari"> <option value="Edge"> </datalist> </div> <div> <label>Search</label> <input name="search-pattern" type="search"> </div> <div> <label>Multiple Lines</label> <textarea name="multiline-text" required>Default value in text area</textarea> </div> </div> <!-- end table --> </fieldset> <!-- end cell --> </div> <!-- end row --> <div> <label>Date</label> <div class="horizontal-bar"> <script> function calculateYearsSince(date) { var millis = Date.now() - new Date(date).getTime(); var years = millis / 1000 / 60 / 60 / 24 / 365; var age = (years > 0) ? Math.floor(years) : Math.ceil(years); document.getElementById("yearsSince").value = age; } </script> <input class="horizontal-bar-stretch" name="day-only" type="date" required oninput="calculateYearsSince(this.value);"> <label class="horizontal-bar-natural" for="yearsSince">Years Since</label> <output name="yearsSince" id="yearsSince"></output> </div> </div> <div> <label>Date / Time</label> <fieldset> <div class="table left-label"> <div> <label>Time</label> <input name="time-only" type="time"> </div> <div> <label>Date / Time Local</label> <input name="date-and-time-local" type="datetime-local"> </div> <div> <label>Month</label> <input name="month-only" type="month"> </div> <div> <label>Week</label> <input name="week-only" type="week"> </div> </div> <!-- end table --> </fieldset> <!-- end cell --> </div> <!-- end row --> <div> <label>Other Inputs</label> <fieldset> <div class="table left-label"> <div> <label>Radiobuttons</label> <div> <!-- span.sameline: don't let the label and the button separate to different layout rows --> <span class="sameline"><input name="gender" type="radio" value="male" required><label>Male</label></span> <span class="sameline"><input name="gender" type="radio" value="female" required><label>Female</label></span> <span class="sameline"><input name="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><input name="RGB" type="radio" value="red" ><label>Red</label></div> <div><input name="RGB" type="radio" value="green"><label>Green</label></div> <div><input name="RGB" type="radio" value="blue" ><label>Blue</label></div> </div> </div> <div> <label>Single Select</label> <select name="single-select" required> <optgroup label="Compiled"> <option value="java">Java</option> <option value="c++">C++</option> </optgroup> <optgroup label="Interpreted"> <option value="es6">EcmaScript</option> <option value="python">Python</option> </optgroup> </select> </div> <div> <label>Multiple Select</label> <select name="multi-select" multiple required> <option value="one">Use</option> <option value="two">Shift</option> <option value="three">and</option> <option value="four">Control</option> <option value="five">Keys</option> <option value="six">with</option> <option value="seven">Mouse</option> </select> </div> <div> <label>Range</label> <input name="slider" type="range"> </div> <div> <label>Color</label> <input name="colour" type="color"> </div> <div> <label>File</label> <input name="file-upload" type="file"> </div> </div> <!-- end table --> </fieldset> <!-- end cell --> </div> <!-- end row --> <div> <label>License Read</label> <input name="boolean" type="checkbox" required> </div> </div> <!-- end table --> <div class="button-container"> <input title="Reset" type="reset" value="✘"> <input title="Submit" type="submit" value="✔"> </div> </fieldset> <!-- end top fieldset --> </div> <!-- end div.responsive --> </form> </body> </html> |
Keine Kommentare:
Kommentar veröffentlichen