Roosevelt

Semantic Forms — Layout

🔝 Scroll to top

← Usage

Responsive columns

This feature makes it so the form fields will grow, shrink, expand the number of columns, or reduce the number of columns based on how much space exists in the container. You can customize it by applying various colspan-# classes to various elements in the form markup structure.

Set number of columns for a group of forms

You can limit the number of columns on your forms using the colspan-# class on a <dl> element:

<form class="semanticForms">
  <dl class="colspan-2">
    <div>
      <dt><label for="input-a">Input A</label></dt>
      <dd><input type="text" id="input-a" name="input-a"></dd>
    </div>

    <div>
      <dt><label for="input-b">Input B</label></dt>
      <dd><input type="text" id="input-b" name="input-b"></dd>
    </div>

    <!-- wraps to the next row -->
    <div>
      <dt><label for="input-c">Input C</label></dt>
      <dd><input type="text" id="input-c" name="input-c"></dd>
    </div>
  </dl>
</form>

You can also apply the class to a <fieldset> which will apply that styling to all nested <dl> elements:

<form class="semanticForms">
  <fieldset class="colspan-3">
    <!-- inherits 3 column class -->
    <dl>
      <div>
        <dt><label for="f-ex-input-a">Input A</label></dt>
        <dd><input type="text" id="f-ex-input-a" name="f-ex-input-a"></dd>
      </div>

      <div>
        <dt><label for="f-ex-input-b">Input B</label></dt>
        <dd><input type="text" id="f-ex-input-b" name="f-ex-input-b"></dd>
      </div>

      <div>
        <dt><label for="f-ex-input-c">Input C</label></dt>
        <dd><input type="text" id="f-ex-input-c" name="f-ex-input-c"></dd>
      </div>
    </dl>

    <!-- has 4 columns -->
    <dl class="colspan-4">
      <div>
        <dt><label for="f-ex-input-d">Input D</label></dt>
        <dd><input type="text" id="f-ex-input-d" name="f-ex-input-d"></dd>
      </div>

      <div>
        <dt><label for="f-ex-input-e">Input E</label></dt>
        <dd><input type="text" id="f-ex-input-e" name="f-ex-input-e"></dd>
      </div>

      <div>
        <dt><label for="f-ex-input-f">Input F</label></dt>
        <dd><input type="text" id="f-ex-input-f" name="f-ex-input-f"></dd>
      </div>
    </dl>
  </fieldset>
</form>

Set number of columns a single input field takes up

You can specify the number of columns an input will span using the colspan-# class on the <dd> element. The inputs are responsive and will shrink according to available space. Available numbered classes are colspan-1 through colspan-5:

<form class="semanticForms">
  <fieldset>
    <dl>
      <div>
        <dt><label for="cspan-ex-input-a">Input A</label></dt>
        <!-- 1 column -->
        <dd class="colspan-1"><input type="text" id="cspan-ex-input-a" name="cspan-ex-input-a"></dd>
      </div>

      <div>
        <dt><label for="cspan-ex-input-b">Input B</label></dt>
        <!-- 2 columns  -->
        <dd class="colspan-2"><input type="text" id="cspan-ex-input-b" name="cspan-ex-input-b"></dd>
      </div>

      <div>
        <dt><label for="cspan-ex-input-c">Input C</label></dt>
        <!-- 5 columns (max) -->
        <dd class="colspan-5"><input type="text" id="cspan-ex-input-c" name="cspan-ex-input-c"></dd>
      </div>
    </dl>
  </fieldset>
</form>

colspan-full can also be used to force an input to take up the entire width of a form:

<form class="semanticForms">
  <fieldset>
    <dl>
      <div>
        <dt><label for="cspanfull-ex-input-a">Input A</label></dt>
        <!-- this spans 2 columns on row 1 -->
        <dd class="colspan-2"><input type="text" id="cspanfull-ex-input-a" name="cspanfull-ex-input-a"></dd>
      </div>

      <div>
        <dt><label for="cspanfull-ex-input-b">Input B</label></dt>
        <!-- this spans the full width of row 2 -->
        <dd class="colspan-full"><input type="text" id="cspanfull-ex-input-b" name="cspanfull-ex-input-b"></dd>
      </div>

      <div>
        <!-- this spans 1 column on row 3 -->
        <dt><label for="cspanfull-ex-input-c">Input C</label></dt>
        <dd><input type="text" id="cspanfull-ex-input-c" name="cspanfull-ex-input-c"></dd>
      </div>
    </dl>
  </fieldset>
</form>

Align elements within the responsive grid

Use classes to align an element at the start, center, or end of its available space. Available classes are align-start, align-center, and align-end.

<form class="semanticForms" method="post">
  <dl class="colspan-3">
    <div>
      <dt><label for="align-start">align-start</label></dt>
      <dd class="align-start"><input id="align-start" name="align-start" type="text"></dd>
    </div>

    <div>
      <dt><label for="align-center">align-center</label></dt>
      <dd class="align-center"><input id="align-center" name="align-center" type="text"></dd>
    </div>

    <div>
      <dt><label for="align-end">align-end</label></dt>
      <dd class="align-end"><input id="align-end" name="align-end" type="text"></dd>
    </div>
  </dl>
</form>

Set which column a single input field goes in

You can place a field in a particular column using the col-# class on the <dd> element. Available numbered classes are col-1 through col-5. It can be combined with colspan-#, in which case col-# decides where the field starts and colspan-# decides how wide it is.

Positions are responsive in the same way spans are: when the form narrows to fewer columns than the class asks for, the field falls back to flowing into the next free space rather than forcing a column that is no longer there.

<form class="semanticForms">
  <fieldset>
    <dl class="colspan-3">
      <div>
        <dt><label for="col-ex-input-a">Input A</label></dt>
        <!-- third column -->
        <dd class="col-3"><input type="text" id="col-ex-input-a" name="col-ex-input-a"></dd>
      </div>

      <div>
        <dt><label for="col-ex-input-b">Input B</label></dt>
        <!-- first column -->
        <dd class="col-1"><input type="text" id="col-ex-input-b" name="col-ex-input-b"></dd>
      </div>

      <div>
        <dt><label for="col-ex-input-c">Input C</label></dt>
        <!-- starts in the second column and spans two -->
        <dd class="col-2 colspan-2"><input type="text" id="col-ex-input-c" name="col-ex-input-c"></dd>
      </div>
    </dl>
  </fieldset>
</form>

Tables

Applying the semanticForms class to a <table> styles the table itself and any fields inside its cells. A <tr> with a class of title is styled as a title row spanning the table.

<table class="semanticForms">
  <thead>
    <tr class="title"><th colspan="2">Table title</th></tr>
    <tr><th>Column 1</th><th>Column 2</th></tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td><input type="text" id="table-ex-input" name="table-ex-input" placeholder="A text input in a table"></td>
    </tr>
  </tbody>
</table>
Table title
Column 1Column 2
1

A whole <form class="semanticForms"> can also go inside a single cell of a table that has no semanticForms class of its own, if you would rather leave the table unstyled.

Fieldsets and details

A <fieldset> groups fields under its <legend>. Nesting one inside another darkens it, so the levels stay distinguishable, and <details> elements are styled and animated the same way as a nested fieldset.

<form class="semanticForms">
  <fieldset>
    <legend>Outer group</legend>
    <fieldset>
      <legend>Inner group</legend>
      <dl>
        <div>
          <dt><label for="nested-ex-input">Input</label></dt>
          <dd><input type="text" id="nested-ex-input" name="nested-ex-input"></dd>
        </div>
      </dl>
    </fieldset>

    <details>
      <summary>Show more</summary>
      <dl>
        <div>
          <dt><label for="details-ex-input">Hidden until opened</label></dt>
          <dd><input type="text" id="details-ex-input" name="details-ex-input"></dd>
        </div>
      </dl>
    </details>
  </fieldset>
</form>
Outer group
Inner group
Show more

Fields in p tags

Where a <dl> is more structure than you need, a field and its label can go in a <p> instead. These do not get the float label treatment, so the label stays beside the field.

<form class="semanticForms">
  <p>
    <label for="p-ex-input">Text input</label>
    <input type="text" id="p-ex-input" name="p-ex-input">
  </p>
  <p>
    <input type="checkbox" id="p-ex-checkbox" name="p-ex-checkbox">
    <label for="p-ex-checkbox">Checkbox</label>
  </p>
</form>

Your own layout

To lay a form out yourself while still using the recommended markup and the way Semantic Forms styles the controls, add the customLayout class to the <form>, a <fieldset> or a <dl>. On a fieldset it covers every <dl> inside it, the same way the colspan-# classes do.

Inside it, none of the library's layout of the <dl> applies: no responsive grid, no colspan-#, col-# or align-* classes, no sizing of the <div> and <dd> elements, no float labels, and no room kept beside a field for a clear button. The JavaScript adds, moves and hides nothing around the fields there: no float labels, no clear or show password buttons, no wrapping <div>, no extra label beside a single checkbox or switch, no required asterisk or help icon, and no value appended to the label of a data-display-value range. A <dt> label beside a <dd> that only holds a button stays visible too.

What still applies is everything that styles the controls themselves: text and number fields, selects and their arrow, buttons and button menus, checkboxes, switches, range sliders and color pickers, along with their focus, disabled, invalid and warning styles, the fieldsets and legends, and the theming variables. Switches can still be dragged, textareas still grow to fit their text, and data-focus-key keyboard shortcuts still work, though the shortcut is put in the field's title rather than drawn over the field.

This example puts each field's label and a description in its <dt> and the control in its <dd>, and lays each <div> out as a row:

<form class="semanticForms customLayout settingsExample">
  <fieldset>
    <legend>Playback</legend>
    <dl>
      <div>
        <dt>
          <label for="custom-ex-crossfade">Crossfade</label>
          <p>Milliseconds to blend the end of one track into the next.</p>
        </dt>
        <dd>
          <input type="number" id="custom-ex-crossfade" name="custom-ex-crossfade" min="0" max="10000" value="1000" required>
          <p data-invalid-text>Enter a number from 0 to 10000.</p>
        </dd>
      </div>

      <div>
        <dt>
          <label for="custom-ex-output">Output device</label>
          <p>Where the sound goes.</p>
        </dt>
        <dd>
          <select id="custom-ex-output" name="custom-ex-output">
            <option>System default</option>
            <option>Headphones</option>
          </select>
        </dd>
      </div>

      <div>
        <dt>
          <label for="custom-ex-gapless">Gapless playback</label>
          <p>Play albums without a pause between tracks.</p>
        </dt>
        <dd><input type="checkbox" switch id="custom-ex-gapless" name="custom-ex-gapless" checked></dd>
      </div>

      <div>
        <dt>
          <label for="custom-ex-volume">Interface volume</label>
          <p>How loud clicks and alerts are.</p>
        </dt>
        <dd>
          <input type="range" id="custom-ex-volume" name="custom-ex-volume" value="80">
          <output for="custom-ex-volume">80</output>
        </dd>
      </div>
    </dl>
  </fieldset>

  <fieldset>
    <legend>Appearance</legend>
    <dl>
      <div>
        <dt>
          <label for="custom-ex-accent">Accent color</label>
          <p>Used for highlights and switches.</p>
        </dt>
        <dd><input type="color" id="custom-ex-accent" name="custom-ex-accent" value="#3366cc"></dd>
      </div>

      <div>
        <dt>
          <label id="custom-ex-columns-label">Visible columns</label>
          <p>Which columns the track list shows.</p>
        </dt>
        <dd>
          <ul aria-labelledby="custom-ex-columns-label">
            <li><input type="checkbox" id="custom-ex-artist" name="custom-ex-columns" value="artist" checked> <label for="custom-ex-artist">Artist</label></li>
            <li><input type="checkbox" id="custom-ex-album" name="custom-ex-columns" value="album" checked> <label for="custom-ex-album">Album</label></li>
            <li><input type="checkbox" id="custom-ex-year" name="custom-ex-columns" value="year"> <label for="custom-ex-year">Year</label></li>
          </ul>
        </dd>
      </div>

      <div>
        <dt>
          <label for="custom-ex-reset">Reset settings</label>
          <p>Put everything back how it was.</p>
        </dt>
        <dd><button type="button" id="custom-ex-reset">Reset</button></dd>
      </div>
    </dl>
  </fieldset>
</form>

With CSS of your own for the rows, along these lines:

form.settingsExample dl {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

/* each row: the label and description on the left, the control on the right */
form.settingsExample dl > div {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 20px;
}

form.settingsExample dt {
  flex: 1 1 250px;
}

form.settingsExample dt p {
  margin: 2px 0 0;
  font-size: small;
  color: var(--semanticFormsPlaceholderColor);
}

form.settingsExample dd {
  flex: 0 0 200px;
  margin-left: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 5px;
}

form.settingsExample dd:has(output) {
  flex-direction: row;
  align-items: center;
}

/* outside the grid a list of checkboxes is a wrapping row, so stack it instead */
form.settingsExample dd ul {
  margin: 0;
  padding: 0;
  flex-direction: column;
  align-items: flex-start;
  gap: 5px;
}

form.settingsExample dd li {
  gap: 5px;
}

form.settingsExample input[type=number] {
  width: 110px;
}

form.settingsExample input[type=color] {
  width: 80px;
}

And a line of JavaScript to keep the range's <output> up to date, since data-display-value writes into the label, which is left alone here:

const volume = document.getElementById('custom-ex-volume')
volume.addEventListener('input', () => { volume.nextElementSibling.value = volume.value })
Playback

Milliseconds to blend the end of one track into the next.

Enter a number from 0 to 10000.

Where the sound goes.

Play albums without a pause between tracks.

How loud clicks and alerts are.

80
Appearance

Used for highlights and switches.

Which columns the track list shows.

Put everything back how it was.

To show a message when a field is invalid, put an element with the data-invalid-text attribute after the control in the same <dd>, as the crossfade field above does. It is hidden until the field is invalid, which is when the browser's own validation or an invalid class you add says so, and it is shown in the invalid color. A warn class on the control shows it in the same way. It has to come after the control and share its parent, since it is found with a sibling selector.

The controls fill the width of whatever holds them, so give them a width of your own where that is not what you want. A list of checkboxes or radios outside the grid is laid out as a wrapping row with the browser's own list spacing, which the example above stacks into a column instead. The library's control styles weigh no more than the form's class and the element, so an ordinary rule such as form.settingsExample input[type=number] overrides them without needing an id or !important.

Leaving the <dl> out altogether works too. Everything described above as still applying is styled wherever it sits in a semanticForms form, and the grid, float labels and clear buttons only ever apply inside a <dl>, so markup of your own without one needs no extra class.

Tabs

Add the tabs class to an element holding one <fieldset> per tab. Each fieldset's <legend> names its tab.

Semantic Forms builds the tab strip, wires up the tab and tabpanel roles, and moves between tabs with the left and right arrow keys, Home, and End. Left alone the markup is a stack of labelled field groups, which is what a visitor without JavaScript gets, so nothing is hidden from them.

<form class="semanticForms">
  <div class="tabs">
    <fieldset>
      <legend>Account</legend>
      <dl>
        <div>
          <dt><label for="tabs-ex-username">Username</label></dt>
          <dd><input type="text" id="tabs-ex-username" name="tabs-ex-username"></dd>
        </div>
      </dl>
    </fieldset>

    <fieldset>
      <legend>Preferences</legend>
      <dl>
        <div>
          <dt><label for="tabs-ex-newsletter">Newsletter</label></dt>
          <dd><input type="checkbox" switch id="tabs-ex-newsletter" name="tabs-ex-newsletter"></dd>
        </div>
      </dl>
    </fieldset>
  </div>
</form>
Account
Preferences

The first tab opens by default. To open a different one, put data-selected on its fieldset. Without JavaScript every panel is on the page at once, so the attribute does nothing there. Marking more than one fieldset uses the first and logs an error.

<div class="tabs">
  <fieldset>
    <legend>Account</legend>
    ...
  </fieldset>

  <fieldset data-selected>
    <legend>Preferences</legend>
    ...
  </fieldset>
</div>

The tab strip takes its colors from --semanticFormsButtonGradientLight and --semanticFormsButtonGradientDark for the unselected tabs, and --semanticFormsFormSubBgColor for the selected one, so it matches the buttons and fieldsets around it.