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 comes apart: the description sizes the max-content column, and the label of every option is pushed the width of the longest description away from the input it belongs to. 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 -- only one of which 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 one description a boolean group is laid out for; the ones in the next section are not. 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, and the problem is visible at once. 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, so it goes to the next row of the first column -- and that column is sized to its widest content, which is now a sentence rather than a radio button. The column blows out to the width of the description and takes the label with it, so each option ends up with its radio at the left, its label stranded five hundred pixels away on the right, and its description underneath spanning the gap between them.

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 collapse with a checkbox in front of it. Worth having beside the radios rather than assumed to match, because the width the first column blows out to is set by the longest description in the set and nothing else -- these are different sentences, so the label lands in a different place again. 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 worst of the three, because the set is uneven twice over. The two described options are two rows tall and the third is one, so an even spacing between options stops looking even -- and since the first column is sized to the longest description in the whole set, the undescribed option has its label pushed just as far right as the others for no reason at all. 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 above the input rather than below it, and the div it comes out in has no class on it at all, because form-element.html.twig adds the description class in the after branch and forgets to in the before one. So this is the one set in the section with no pair: a description moved above its option stops matching every rule written for one below it, and that includes the rule Formtips looks for, so it renders plainly without being asked to. What it renders is the same collapse the other pairs show and a little worse -- the description takes the whole of the first grid row, which strands the input at the end of it and drops the label onto the next line alone.

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 centre: the labels are the thing being lined up, and an input taller than its text pushes the row apart if it is centred 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 entirely on how wide an option is allowed to get and whether the description is measured to the label or to the row -- neither of which the flex rule that puts the options on the row says anything about, since it sets a direction and a gap and stops there.

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 decide the width of the whole row on its own, which is the thing to watch: one option written by an editor can take the set apart.

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 only one of them arrives anywhere useful, which is worth knowing because the two that do not fail 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 does, 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. The class does reach a rule this theme has -- .form-wrapper .container-inline, which the item container and the fieldset satisfy between them -- and still nothing happens, because the rule makes a flex container of the fieldset and the fieldset has one child in flow: the div wrapping the options. The legend is laid out by the fieldset rather than as a flex item, the options are a level further down, and the set below is in a column like the last one.

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 colour 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