Radios and checkboxes

The two boolean groups in every layout they get asked for: stacked and on one row, with a description under every option, under some of them, and under none. Both elements produce the same markup -- a fieldset around one .form-item per option -- so they are on one page and meant to be read against each other. The interesting half is the descriptions: they are legal on every option and core renders them without complaint, but they arrive as a third child of a .form-item that is usually laid out expecting two, and they are wider than the label they explain. Formtips is installed on this site and most of the others, and it takes every one of those descriptions out of the flow and leaves a tooltip trigger in its place, so each described set is shown twice: once as the site renders it, and once inside a .no-formtips container -- a selector added to formtips.settings for exactly this -- where the descriptions stay where core put them. The pairs are the point. With the descriptions visible the theme's two-column grid used to come apart -- the description sized the max-content column and pushed the label of its option the width of the sentence away from its input -- so the grid now puts the input in the first column and the label, the description and any error in the second. The last two sections cover the states that change the markup, and the three properties that look as though they would put a set on one row -- one of which never arrives anywhere the CSS can see it. Nothing is saved. One set is required, so submitting the form empty shows the real error state rather than a copy of it.

Stacked

What both elements do when nothing is asked of them: a <fieldset> carrying the group title as its <legend>, and inside it one .form-item per option holding an input and a label. Nothing here sets a class; everything below is a variation on this shape, so it is worth reading first.

Radios

One value out of the set. Every input shares the name of the group, which is what makes the browser treat them as one control, and each gets an id of its own so its label can point at it.

Which workshop are you booking?

Checkboxes

The same markup with a different control. The names are indexed rather than shared -- stacked_checkboxes[ceramics] -- because the value is an array, and #default_value is a list of the keys that start ticked.

Which materials will you need?

A description on the group

A #description on the element itself rather than on any option. It lands after the last option, inside the fieldset, and the fieldset is pointed at it with aria-describedby. This is the description a boolean group is laid out for first; the ones in the next section had to be fitted in afterwards. Worth knowing while reading the markup of the rest of the page: template_preprocess_fieldset() writes that aria-describedby onto every fieldset whether or not there is a description to point at, so every other set here carries a reference to an id that does not exist. Formtips reaches this one too -- a fieldset carries the form-item class, so the description inside it matches -- which is why the question mark is against the legend rather than against any option.

Kiln firing
Bisque is fired first and takes a day. Glaze firings are run twice a week, so a piece booked for one may wait.

The same set, Formtips off

The shape a group description was meant to take: a paragraph after the last option, inside the fieldset and measured to it. This one is worth having on the page even though it is not the awkward case, because it is the description every other description on the page gets compared against.

Kiln firing
Bisque is fired first and takes a day. Glaze firings are run twice a week, so a piece booked for one may wait.

A single checkbox, for comparison

Not a group and not the same markup: one option is #type checkbox, which has no fieldset, no legend and no wrapping div. It is here because a form usually mixes the two, and the vertical rhythm has to hold across the join.

Three emails a year. A single checkbox puts its description under the label rather than under the group, because there is no group.

Stacked, with a description per option

An option is a child element, so it takes every property an element takes -- including #description. Setting it is a matter of writing to the child key before the process callback runs: $element['bisque']['#description'] = …, which works because processRadios() and processCheckboxes() merge their defaults in with += and leave anything already there alone.

Every set here is shown twice, because on this site the descriptions are not on the page. Formtips is installed, and it takes them out of the flow: it matches .form-item .description globally, hides the div and puts a small round ? after the label in its place. So the first of each pair is what these sites render today, and the second is the same element inside a .no-formtips container -- a selector added to formtips.settings for exactly this, since the module skips any description matching it. Read the pairs together. The plain one is the layout the theme actually has to hold, and the one it would be handed the moment Formtips were turned off anywhere.

Radios, every option described

The description is a third child of the .form-item, after the input and the label, which is one child more than most layouts of that .form-item expect. Here it is a question mark against each option and nothing else.

How should we contact you about the booking?
Sent the same day. The address on your account, which you can change at any time.
Only if something changes on the day itself; we do not ring to confirm.
A card in the second post. Allow a week, and tell us if you move.
You will still get the receipt, because that is a legal requirement rather than a message.

The same set, Formtips off

The same element with the descriptions left where core put them. This theme lays a .form-item out as a grid of two columns, max-content minmax(0, 1fr): an input and a label. The description is a third child, and left to auto-placement it went to the next row of the first column -- a column sized to its widest content, which was then a sentence rather than a radio button, so it blew out to the width of the description and stranded each label five hundred pixels from its radio. The grid now places the input in the first column and every other child in the second, so the description hangs under the label it explains and the first column stays the width of the control.

How should we contact you about the booking?
Sent the same day. The address on your account, which you can change at any time.
Only if something changes on the day itself; we do not ring to confirm.
A card in the second post. Allow a week, and tell us if you move.
You will still get the receipt, because that is a legal requirement rather than a message.

Checkboxes, every option described

The same, one control along. Worth putting beside the radios above rather than trusting the two match: the inputs are not the same width, so anything aligned to the edge of one is not aligned to the edge of the other.

What should the studio have ready?
Ten kilos of stoneware, wedged. Say so below if you want something else.
The house glazes only. Anything mixed to order has to be asked for a fortnight ahead.
Offcuts from the joinery bench, so sizes vary and nothing is planed.
Annealed steel, 1.2mm. Heavier gauges are kept in the workshop and issued on the day.

The same set, Formtips off

The same layout with a checkbox in front of it. Worth having beside the radios rather than assumed to match: the first column is the width of the control, and a checkbox is not the width of a radio, so the descriptions start at a slightly different edge. Nothing about the fix is specific to either control; every measurement is.

What should the studio have ready?
Ten kilos of stoneware, wedged. Say so below if you want something else.
The house glazes only. Anything mixed to order has to be asked for a fortnight ahead.
Offcuts from the joinery bench, so sizes vary and nothing is planed.
Annealed steel, 1.2mm. Heavier gauges are kept in the workshop and issued on the day.

Only some options described

The mixed case, which is the common one: two options carry a description and the third does not. As tooltips that shows only as a question mark on some rows and not others, which is the version that reads best of anything in this section -- worth admitting, since it is the reason the module is installed.

Access
The kiln room is down three steps and cannot be reached any other way; everything else in the building is level.
Fitted in the studio and the library. Switch it on at the wall.

The same set, Formtips off

The hardest of the three, because the set is uneven. The two described options are two rows tall and the third is one, so an even spacing between options stops looking even. Each .form-item is a grid of its own, so the option without one is laid out exactly as it would be in a set with no descriptions at all, and its label lines up with the labels above it because every first column is the width of the same control. Whatever separates the options of a set has to be measured against this rather than against a set where every option is one line.

Access
The kiln room is down three steps and cannot be reached any other way; everything else in the building is level.
Fitted in the studio and the library. Switch it on at the wall.

Description before the option

#description_display accepts before, after and invisible on an option as it does anywhere else. Set to before it puts the text ahead of the input in the source rather than after the label. Core leaves the description class off the div in that branch -- form-element.html.twig adds it only to a description after the element -- and so did this theme, which meant a description moved above its option matched none of the rules written for one below it, Formtips included. The theme adds the class in both branches now, so this set takes question marks like any other.

Membership
Open to anyone who has finished one course.
Two years of associate membership, or a proposal from two full members.

The same set, Formtips off

The description is the first child of the .form-item, before the input. It takes the second column like everything but the input does, and auto-placement fills a row from the left, so it sits in the second column of the first row and the input and label move down to the row below: the text lines up over the label it belongs to, and the first column stays the width of the radio.

Membership
Open to anyone who has finished one course.
Two years of associate membership, or a proposal from two full members.

Inline

The options on one row instead of one per line. There is no element property for this: it is a class on an ancestor and a rule in the theme, so what follows is the class this theme actually styles -- .form--inline .form-radios -- put on the container each item is wrapped in. The next section but one is about the other ways of asking, and what they do instead.

Radios

Short labels, which is what inline is for: a set of two or three one-word options reads as a row and wastes four lines as a column. The row wraps, so the same set has to be looked at narrow as well as wide.

Session

Checkboxes

The same row of a different control. Baseline alignment rather than center: the labels are the thing being lined up, and an input taller than its text pushes the row apart if it is centered on it.

Days

Labels too long for a row

Inline is a request rather than a promise. These are the labels from the first section, which are long enough that the row breaks into something that is neither a row nor a column as soon as the column narrows, and long enough for an option to wrap inside itself as well. The point of having it here is that a set is not always laid out by whoever wrote its labels: narrow the window until this one gives way, and that is the width the rest of the section has to be judged at too.

Which workshops are you booking?

Inline, with a description per option

The awkward corner, and the reason the two variations are crossed rather than shown one after the other. An inline set is laid out on the assumption that an option is one line of text; a described option is at least two, and it is the description that decides how wide the option wants to be. Nothing here is a mistake in the form -- both properties are ordinary -- so the layout has to hold or the theme has to say it will not. Paired the same way as the section above: the tooltip version first, then the same element with Formtips kept off it.

Radios

Three described options on one row, which with the descriptions hidden is simply the inline set from the section above with a question mark added to each option. This is the easy half.

Session
09:00 to 12:30.
13:30 to 17:00.
18:00 to 21:00, term time only.

The same set, Formtips off

The hard half. Whether this is readable depends on how wide an option is allowed to get and whether the description is measured to the label or to the row. The flex rule that puts the options on the row says nothing about either -- it sets a direction and a gap and stops there -- so it is the grid of each option that answers: the description goes in the column of the label, and an option is as wide as the longer of the two.

Session
09:00 to 12:30.
13:30 to 17:00.
18:00 to 21:00, term time only.

Checkboxes, uneven descriptions

Four options whose descriptions are of very different lengths, which as tooltips makes no difference at all: the row is the same row it would be with no descriptions on it.

Days
Studio only.
Kiln room open from noon.
Closed for firing.
Kiln room open all day; the print room is given over to the letterpress course until four.

The same set, Formtips off

Where the alignment shows. The options are aligned on their baseline, so the inputs stay level and the descriptions hang to different depths under them; stretch the options instead and the inputs stop lining up at all. The last description is long enough to take a row to itself, which is the thing to watch: the row wraps rather than breaking, but one option written by an editor still decides how the whole set reads.

Days
Studio only.
Kiln room open from noon.
Closed for firing.
Kiln room open all day; the print room is given over to the letterpress course until four.

How a set is asked to go inline

Three properties look as though they would do it and two of them work, which is worth knowing because the one that does not fails quietly: the form renders, the class is simply not where the CSS is looking. The sets below are identical apart from where the class was put.

#wrapper_attributes: nothing at all

The property that normally reaches the div around a form element, and here it reaches nothing. A boolean group is a composite: preRenderCompositeFormElement() replaces the form_element wrapper with a fieldset, and the fieldset template reads #attributes. Nothing ever looks at #wrapper_attributes, so it is dropped without a word.

Session

#attributes: the fieldset, and every input

What core and contrib do -- the "on days" set of Smart Date Recur is one -- and it lands twice: on the fieldset, and again on every individual input, because processRadios() copies the group #attributes onto each option it builds. Inspect one of the radios below and it is carrying container-inline itself. A flex container made of the fieldset alone would do nothing, because the fieldset has one child in flow -- the div wrapping the options -- so the theme has a rule of its own for this shape, fieldset.container-inline: it floats the legend out of the border and into the row, and lays out the options a level further down. The inputs are left out of the generic container-inline rule, which would otherwise make a flex container of every radio.

Session

A class on an ancestor: the one that works

form--inline on the container around the element, which is how every set in the two sections above was done. It works because the rule is written for a descendant -- the theme reaches past the fieldset to the .form-radios div inside it -- and it is the shape core uses for exposed filters, so a set inside one is inline already without being asked.

Session

States

Required, disabled and unlabelled, which are the states that change the markup rather than only the color of it. Submit the form with the first set empty to see the error: it is the real validation, so the message, the classes and the focus are whatever core and the theme actually produce.

Required

The marker goes on the legend rather than on any option, because the options are one control: the fieldset takes required and aria-required, and the legend span takes form-required. Submit and the invalid state lands in two places -- error and aria-invalid on the fieldset, and error on every input inside it. The message is reported once rather than once per radio, because each option carries #error_no_message; where it is shown depends on the site, since without Inline Form Errors installed it goes to the messages region and the place fieldset.html.twig keeps for it stays empty.

Session

One option disabled

#disabled on a child rather than on the group. Only that option gets the attribute; the rest of the set stays usable, so the disabled one has to be legible enough to read and plainly not something to click. The ticked one is ticked on purpose: a disabled input is not submitted at all, and processCheckboxes() puts its default back on the server rather than reading it as unticked, so the state shown is the state kept.

Rooms
Booked for a glaze firing until Thursday.

The whole set disabled

#disabled on the group, which core pushes down onto every option: the fieldset takes the disabled attribute and so does each input, and it is the .form-item around each option -- not the fieldset -- that picks up the form-disabled class. So a rule hung on form-disabled reaches the options and never the legend.

Session

Title hidden

#title_display invisible, for a set whose context is already on the page -- a row of a table, or a column heading. The legend is still there and still read out; it is only taken off the screen, so the set loses its heading without losing the space above it.

Session