Forms
Forms and the pieces that go in them, from text boxes and choices to matching, sorting and an ID tap.
On This Page
Page Setup and Forms App
With cmu-app on <body>, or cmu-framed on
a page inside another tool such as a Canvas LTI frame, forms mostly take care of
themselves:
-
Labels go in small gray capitals over each text box or menu, and
the box fills its column. Add
inlineto keep a box or menu at its natural width. -
Checkboxes and radio buttons get a legend in the same style, no
bullets, each label next to its box. Group them in a
fieldsetso the legend names them for screen readers. A legend that asks a question (Choice Rows, Matching, Sorting) stays bold and in sentence case. -
Hints: a
p.cmu-hintright after a field (or after a fieldset's list) sits just under it; point the field'saria-describedbyat it. - CMU red for checkboxes, radio buttons, sliders and progress bars, on any page with the kit.
body.cmu-app, body.cmu-framed
Fields in a Row App
Short fields that belong together on one line, each with its label above: a width and
a height, a date range, a first and last name. Wrap each label and its field in a
div inside cmu-field-row; a separator such as × or
"to" goes between them in a span with aria-hidden="true",
since the labels already say what each field is. A hint under the row works as under a
single field; a hint for one field goes in that field’s div, under
it, and the row lines up along the top instead of the bottom. When the row is too
narrow, the fields wrap onto the next line. For short values, such as numbers or
codes, add cmu-field-row--short so the boxes keep a short width instead
of sharing the whole row. To name the pair, such as Size for a width and a height, put
the row in a fieldset with a <legend>: the legend
takes the small capitals, and the fields’ own labels turn quieter, in ordinary
case.
cmu-field-row, cmu-field-row--short, cmu-hint
Field With a Button App
One text box and the button that acts on it, side by side: adding someone by Andrew
ID, looking up a code or running a search that waits for a button. Put the box and a
btn in cmu-field-button, with the label right before it as
for any text box. The box takes the spare width and the button matches its height. To
filter a list as people type, with no button, use a
search bar instead; to show text for people to copy,
with a Copy button beside it, use a copy row.
cmu-field-button
Search Suggestions AppScript
A text box that suggests what to enter as people type: a place, a person, a course.
The page fills the list from its own data or a search service; the kit shows the list
under the box, bolds the typed text in each suggestion and handles the keyboard and
screen readers. Unlike a datalist, which each browser draws its own way,
the list looks the same everywhere.
-
The markup: the box and an empty list in
cmu-suggest. Give the boxrole="combobox",aria-autocomplete="list",aria-expanded="false"andaria-controlswith the list's id, and the listrole="listbox"andhidden. -
The page's part: on each
inputevent, replace the list's contents with an<li>for each suggestion, or empty it. The list shows while the box has focus and has suggestions. Put a value to enter that differs from what's shown indata-value. -
Picking: Up and Down move through the list, Enter or a click picks
and Escape closes it. Picking puts the suggestion in the box and fires
input,changeandcmu-suggest-pick, whosedetailis the chosen<li>. -
With a button: put
cmu-suggestinside a field with a button in place of the box.
cmu-suggest, cmu-suggest-list
Options Under a Choice App
Options that only apply to one choice go inside that choice's list item, lined up with
its label, in a cmu-sub-options block. The kit shows them only while that
choice is picked, with no script: put the radio button or checkbox right inside the
list item, before its label. Menus and text boxes inside fill the block, as they fill
a column elsewhere. Try it: pick A Logo.
cmu-sub-options
Choice Rows App
A question with a few answers, each a large row that's one target: the radio button or
checkbox and its words, with the box around it, so it's easy to pick on a phone. Put
each <input> inside its <label>, and the
question in the fieldset's <legend>. The picked row turns red. Use
checkboxes the same way when more than one answer can be picked. A question in the
legend is bold; when the legend only names the choices, such as Pages under a heading
that already asks, add cmu-label for the small capitals of other labels.
cmu-choices, cmu-label on a legend that names
Choice Grid App
Many short choices that read best as pictures, such as icons or colors: choice rows
laid out as tiles. Add cmu-choices--grid to the fieldset, and put a
picture and a few words in each <label> after its
<input>. The radio button is hidden but still takes focus, and the
arrow keys move between tiles. The picked tile turns red. In a
dialog, the grid scrolls, so a search field above
it can narrow a long set.
cmu-choices cmu-choices--grid, a bi icon sized for a tile
Question Card App
One question at a time, such as in a training, a quiz or a poll: a white card on a
colored band, with the question over a rule and its choice rows. The card is
cmu-question on the form; the band is the template's
content wide with a color, here red. Checking the answer,
and what happens next, is up to the app.
-
Under the choices,
cmu-question-buttonsputs a forward arrow ( ▶ ) on the right and, when people can go back, a back arrow ( ◀ ) on the left (cmu-question-back). - On the last question, the forward arrow gives way to Submit, centered, so someone clicking forward doesn't submit by accident.
-
Give each arrow an
aria-labeland a matchingtitle, "Previous question" and "Next question," as with the arrows in Sorting.
cmu-question, cmu-question-buttons,
cmu-question-back, cmu-choices
Matching App
Items to pair with answers, such as terms and what they mean, drawn as puzzle pieces:
each item has a socket, and the menu of answers beside it a tab. Once an answer is
picked, the two pieces snap together. It's a menu, not dragging, so it works with a
keyboard, a screen reader and a phone. Each menu starts on "Choose," with an empty
value, and has a <label> naming its item. Under 600px
the menu goes under its item, without the puzzle shapes.
cmu-match
Sorting AppScript
Items to put in order, such as the steps of a task: a numbered list with up and down
arrow buttons on each item, which cmu-app.js adds. Each button names its
item for screen readers, focus stays on the item as it moves and its new place is read
out. Put a hidden <input> in each item; it moves with the item, and
the form sends the new order. Without the script, the list keeps its first order. In
React, Svelte or Vue, which own the list, see the Framework tab: render the buttons
yourself and handle the cmu-sort-move event.
cmu-sort, data-cmu-sort, cmu-sort-moves,
data-cmu-move
In React
The list is React's, so render the buttons yourself and handle
cmu-sort-move: cancel it and reorder your state. The kit adds no
buttons and moves nothing; it still says where the item went and keeps focus on it.
import { useEffect, useRef, useState } from "react";
export function Steps() {
const [steps, setSteps] = useState(["Delete the email", "Don't click its links", "Report it"]);
const list = useRef(null);
useEffect(() => {
const move = (e) => {
e.preventDefault();
const { from, to } = e.detail;
setSteps((s) => {
const next = [...s];
next.splice(to, 0, next.splice(from, 1)[0]);
return next;
});
};
list.current.addEventListener("cmu-sort-move", move);
return () => list.current?.removeEventListener("cmu-sort-move", move);
}, []);
return (
<ol className="cmu-sort" data-cmu-sort ref={list}>
{steps.map((step, i) => (
<li key={step}>
{step}
<input type="hidden" name="order" value={step} />
<span className="cmu-sort-moves">
<button type="button" className="btn ghost small" data-cmu-move="-1"
aria-label={`Move ${step} up`} disabled={i === 0}>▲</button>
<button type="button" className="btn ghost small" data-cmu-move="1"
aria-label={`Move ${step} down`} disabled={i === steps.length - 1}>▼</button>
</span>
</li>
))}
</ol>
);
}
In Svelte
The list is Svelte's, so render the buttons yourself and handle
cmu-sort-move: cancel it and reorder your state. The kit adds no
buttons and moves nothing; it still says where the item went and keeps focus on it.
<script>
let steps = $state(["Delete the email", "Don't click its links", "Report it"]);
let list;
$effect(() => {
const move = (e) => {
e.preventDefault();
const { from, to } = e.detail;
steps.splice(to, 0, steps.splice(from, 1)[0]);
};
list.addEventListener("cmu-sort-move", move);
return () => list.removeEventListener("cmu-sort-move", move);
});
</script>
<ol class="cmu-sort" data-cmu-sort bind:this={list}>
{#each steps as step, i (step)}
<li>
{step}
<input type="hidden" name="order" value={step} />
<span class="cmu-sort-moves">
<button type="button" class="btn ghost small" data-cmu-move="-1"
aria-label="Move {step} up" disabled={i === 0}>▲</button>
<button type="button" class="btn ghost small" data-cmu-move="1"
aria-label="Move {step} down" disabled={i === steps.length - 1}>▼</button>
</span>
</li>
{/each}
</ol>
In Vue
The list is Vue's, so render the buttons yourself and handle
cmu-sort-move: cancel it and reorder your state. The kit adds no
buttons and moves nothing; it still says where the item went and keeps focus on it.
<script setup>
import { ref } from "vue";
const steps = ref(["Delete the email", "Don't click its links", "Report it"]);
function move(e) {
e.preventDefault();
const { from, to } = e.detail;
steps.value.splice(to, 0, steps.value.splice(from, 1)[0]);
}
</script>
<template>
<ol class="cmu-sort" data-cmu-sort @cmu-sort-move="move">
<li v-for="(step, i) in steps" :key="step">
{{ step }}
<input type="hidden" name="order" :value="step">
<span class="cmu-sort-moves">
<button type="button" class="btn ghost small" data-cmu-move="-1"
:aria-label="`Move ${step} up`" :disabled="i === 0">▲</button>
<button type="button" class="btn ghost small" data-cmu-move="1"
:aria-label="`Move ${step} down`" :disabled="i === steps.length - 1">▼</button>
</span>
</li>
</ol>
</template>
In Rails
Rails renders the list, and the kit adds the buttons and moves the items; the hidden
fields send the new order as order[]. With Turbo, a page restored from
its cache already has the buttons, and the kit doesn't add them twice.
<ol class="cmu-sort" data-cmu-sort>
<% @steps.each do |step| %>
<li><%= step.title %><%= hidden_field_tag "order[]", step.id %></li>
<% end %>
</ol>
Card Sort App
Things to sort into groups, such as uses of AI into public and protected tools: each
thing is a white card, with a large tile for each group at its bottom. Cards sit side
by side, as many as fit, and one above another on phones. Picking a tile turns it red.
Each card is a <fieldset> whose <legend> holds
its words, with a radio button in each tile, so keyboards and screen readers work as
with any radio buttons. Put the cards on a colored band, as with the
question card.
cmu-card-sort
File Drop Zone / Picker AppScript
For uploading a file, such as a syllabus or a roster. It's a label wrapping a file input, so it works by click and keyboard as well as by dragging a file onto it, and the file arrives as an ordinary form post.
- Dragging: the zone turns solid red while a file is over it, and dropping chooses the file. Without the script, people choose by clicking.
-
Say what was chosen: the script puts the file's name in
cmu-drop-fileand marks the zonecmu-drop-has-file, so people can catch the wrong file before they send it. Keep the emptycmu-drop-filein the markup: its line holds its place, so the zone doesn't move when a file is chosen. - Say what it takes in a hint under it: the kinds of file and the size limit.
cmu-drop, cmu-drop-pick, cmu-drop-file,
cmu-drop-over, cmu-drop-has-file
Use this markup in a {framework} component, with cmu-drop-file empty:
cmu-app.js writes the chosen file's name there and adds
cmu-drop-over while a file is over the zone. Keep the zone's
class fixed rather than set from state, or a re-render takes those
away. Read the file from the input's change event, as with any file
input.
Try it: choose a file, or drag one onto the zone. Its three looks: waiting, a file over it and a file chosen.
Use this markup in a {framework} component, with cmu-drop-file empty:
cmu-app.js writes the chosen file's name there and adds
cmu-drop-over while a file is over the zone. Keep the zone's
class fixed rather than set from state, or a re-render takes those
away. Read the file from the input's change event, as with any file
input.
In a Dialog
When uploading is one action on a busy page, such as a row in a table, put the zone in a dialog that a small button opens, with Cancel as a ghost button that closes it.
Use this markup in a {framework} component, with cmu-drop-file empty:
cmu-app.js writes the chosen file's name there and adds
cmu-drop-over while a file is over the zone. Keep the zone's
class fixed rather than set from state, or a re-render takes those
away. Read the file from the input's change event, as with any file
input.
ID Tap / NFC AppScript
For checking people in or looking them up with their CMU ID card, such as at a desk, a workshop or an exam room. An NFC card reader acts as a keyboard: it types the card's number into whatever has focus, and is assumed to end it with a carriage return (Enter), which submits the form. So the target holds a hidden text field, and the number arrives as an ordinary form post.
- Ready on load: when it's the only ID tap on the page, the field takes focus when the page loads, so the reader works without a click. The target says Ready while the field has focus, and to select it when it doesn't, so a tap never goes nowhere. Selecting anywhere on the target focuses the field, and so does sending the form.
-
A tap-only page: add
data-cmu-tap="keep"on a kiosk or check-in page, and the field also takes focus back whenever nothing else has it. On a page where something else should have focus first, usedata-cmu-tap="click", and the field waits to be selected. -
Show the card: add
<img class="cmu-tap-card">with PlaceMellon's sample CMU ID card,https://placemellon.andrew.cmu.edu/images/cmu-id-card.svg, and it takes the icon's place. Leave itsaltempty, since the label says what to do. - Its layout follows its room: where the form is at least 46rem wide, the card or icon sits beside the words; anywhere narrower, such as on a phone, in a sidebar or in a dialog, they stack, with the card large on top.
- No card: put a search for an Andrew ID, name or email under it (a field with a button), folded behind "No ID card?" and a Find a Person toggle (a folded list), for people who forgot theirs or for staff entering someone by hand.
-
Say who it found: after each tap, put the person's name in
cmu-tap-result, so the person at the desk can catch a wrong card. It takes Ready's place, so the target doesn't move. - Keep the number private: the field is never shown, and the app should clear it after each tap and never log card numbers.
cmu-tap-target, data-cmu-tap, cmu-tap-card,
cmu-tap-ready, cmu-tap-paused, cmu-tap-result,
cmu-tap-other, cmu-field-button
Try it: type a few numbers in the target and press Enter, or search for a name.
In a Dialog
When tapping is one action on a busy page, such as an attendance list, put the target
in a dialog that a small button opens, with Done as
a ghost button that closes it. Give the field autofocus, and the dialog
opens ready. (The demo here leaves it off, since this page shows the dialog already
open, and the browser would jump down to it.) Since the reader's carriage return
submits the form the field is in, put Done in a method="dialog" form of
its own; otherwise every tap would close the dialog. A dialog is narrow, so the target
stacks there.
Try it: select Open the Dialog, then type a few numbers and press Enter, or search for a name.