Carnegie Mellon Web Kilt

Tailored to the CMU web template

Forms

Forms and the pieces that go in them, from text boxes and choices to matching, sorting and an ID tap.

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 inline to 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 fieldset so 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-hint right after a field (or after a fieldset's list) sits just under it; point the field's aria-describedby at it.
  • CMU red for checkboxes, radio buttons, sliders and progress bars, on any page with the kit.

body.cmu-app, body.cmu-framed

The department and course number, with a hyphen.

Campus

Cancel

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

Size

In pixels, from 1 to 2048.

Orders over 10 copies take an extra day to print.

Loan Dates

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 box role="combobox", aria-autocomplete="list", aria-expanded="false" and aria-controls with the list's id, and the list role="listbox" and hidden.
  • The page's part: on each input event, 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 in data-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, change and cmu-suggest-pick, whose detail is the chosen <li>.
  • With a button: put cmu-suggest inside a field with a button in place of the box.

cmu-suggest, cmu-suggest-list

Try “hall” or “center.”

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

Image

    A different photo each time the page loads.

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

Which session will you attend?

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

Icon

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-buttons puts 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-label and a matching title, "Previous question" and "Next question," as with the arrows in Sorting.

cmu-question, cmu-question-buttons, cmu-question-back, cmu-choices

Scenario: A text says a package can't be delivered and asks you to tap a link to reschedule. What is it most likely to be?

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

Match each term to what it means.

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

Put these steps in order for an email you think is phishing.
  1. Delete the email
  2. Don't click its links or open its attachments
  3. If you clicked a link, change your Andrew password
  4. Report it with the Report Phishing button

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

Pasting unpublished research code into a free coding assistant on the web
Drafting a public event announcement with a chatbot
Summarizing a student's records with a tool CMU has approved for that data

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-file and marks the zone cmu-drop-has-file, so people can catch the wrong file before they send it. Keep the empty cmu-drop-file in 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

Any document up to 20 MB.

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.

Upload Syllabus

For 15-112 A and 15-112 B.

Any document up to 20 MB.

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, use data-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 its alt empty, 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

Ready

Select here, then tap your card on the reader.

No ID card? Find a PersonHide

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.

Check In

Ready

Select here, then tap your card on the reader.

No ID card? Find a PersonHide

Try it: select Open the Dialog, then type a few numbers and press Enter, or search for a name.