Color was the only thing telling the three message types apart, and it was not doing it.

Measured

Against each other rather than against the page: status against error 1.03:1, status against warning 1.31:1, warning against error 1.28:1. The same darkness. As ink at 0.875rem the only difference was hue carried by letter strokes, which is the weakest place to put it, and a reader with ordinary color vision could not see it either.

What shipped

A mark per type, punched out of the text color with a mask, so it takes whatever the palette does and needs no second file per color. The shapes are Claro's: a check for status, a bar and a dot for warning, a circle with a diagonal stroke for error. A booker who also meets a Drupal admin screen reads the same three. Drawn once, in the stepper's stylesheet, because the seat map's library already depends on it. Each mask is written prefixed as well as plain: core's browser list reaches back two Safari majors, and a mask that does not apply leaves no mark at all rather than a plainer one, which puts a booker back where this started.

Filled blocks on the price list, white on the color, which is what the seat map already did. The color becomes an area instead of letter strokes, and both surfaces say the same thing the same way. Every fill clears 4.5:1 against white text.

The map's messages stop centering their text: a line that starts with a mark and then centers its words leaves the mark hanging away from them. The sentence starts at the mark, the dismiss button holds the far end.

One rule for the three fills

The fills were picked one at a time and were not a set: 6.36:1, 5.92:1 and 10.71:1 against white text, so the error block sat visibly heavier than the other two, and the warning read as dirt rather than as a warning. Amber cannot be amber while white text sits on it. White needs 4.5:1, which holds the fill under 0.183 luminance, and a yellow-orange that dark is brown: darkening or saturating it further only made it browner. The hue is the way out. Walking from yellow toward red, the same 4.5:1 allows a steadily lighter fill, so at 24 degrees the block can be a burnt orange and still carry white text, where at 38 it could only be brown.

So the three are set by one rule now: the hue belongs to the type, the saturation is the same for all three, and the lightness is tuned until white text lands on 4.8:1. Status is #0d843e at 4.79:1, warning #b95512 at 4.80:1, error #df2316 at 4.78:1. Holding them to one contrast holds them to one weight, so no type reads as heavier or muddier than the one beside it, and sitting that contrast just above the 4.5:1 floor leaves every hue as light as 1.4.3 allows, which is what the orange needed.

Equal contrast against white means equal luminance, so the three are 1.00:1 against each other in grayscale now rather than 1.03 and 1.31. That is the design rather than a cost: the color was never what told the three apart, and pretending it might be is what pushed the warning into brown in the first place.

The shape of a message

A message block is as wide as the sentence it carries rather than as wide as the card. Run full width it reads as a banner about the whole offer, when it is one sentence about one thing that happened; fit-content still wraps a long sentence at the card's edge, so nothing overflows. A filled block also draws its own outline, so it needs less room above and below than colored ink did, and it takes rounder corners. The room to the left and right stays: that padding is what holds the fill clear of the first and last letter.

What it deliberately does not do

Make the color carry the meaning. Three fills of one darkness stay one shade in grayscale, with colors disabled, or to a booker who cannot separate red from green, and no palette fixes that: the light-tinted alternative in the Claro house style measures 1.04:1, which separates nothing either. The mark is what tells them apart, and the sentence says it in words.

Naming the type for a reader

The mark is visual, so a screen reader learns nothing from it. Each line now carries its type in the words core uses on its own admin screens, wearing core's .visually-hidden: clipped rather than hidden, because display:none and visibility:hidden would take the words out of the accessibility tree, which is the one place they are for. The stepper's library declares system/base, which owns that rule; it reaches a page through the theme, and a component that needs it and does not say so prints "Warning message" in front of every sentence on a theme that does not carry it.

Not aria-label, which this summary first proposed. That replaces what a line says rather than labelling it, so the sentence itself would be lost. Core names a message's type with a heading nobody sees, and this is the same idea at the size of one line.

The words are one function, on Drupal.yoyaku.Message, because the seat map draws its own lines and needs the same three. Three French entries were added; the catalog's two typography checks both read zero.

What putting text inside a line costs

The stepper's reconciler decided whether a line had changed by comparing element.innerHTML against the sentence, which stops being an answer once the element also holds the type. innerText would not have helped: clipped text is still rendered text, so it is included, and reading it forces a layout on every read in a path that runs on every answer about every offer. The line now carries the sentence it was given, so the question is asked of our own record and the DOM is not read at all, which is one read fewer than before rather than one more.

The same property caught a test. getText() returns the clipped words too, so the line as a whole no longer answers "what does this message say". The sentence is its own element on both surfaces now, and the one helper every assertion goes through asks for it.

Checked

The map's stylesheet figure is measured on CI rather than nudged; the price list's own tolerance covered its share. stylelint reports nothing new on either file, and eslint none at all under the configuration CI uses, which needs core's .prettierrc.json in the module root or it reports hundreds of quote-style errors that are not real. The three inline SVGs parse. OfferStepperTest exercises the reconciler hard and is what caught the one thing this got wrong.

AI-Generated: Yes (Claude Code took the measurements, made these changes and drafted this summary. I reviewed them before posting.)

Issue fork yoyaku-3619803

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mably created an issue. See original summary.

mably’s picture

Issue summary: View changes

Dropped the role proposal: it is the announcement half of carrying a type rather than a way of telling the types apart without color, so it went into the branch on #3619635: Carry a message type rather than an error boolean instead. What is left here is the visible mark and the lightness split, plus naming the type to assistive technology.

mably’s picture

Status: Active » Needs review
mably’s picture

Issue summary: View changes
mably’s picture

Issue summary: View changes
mably’s picture

Issue summary: View changes

  • mably committed ae432d8f on 1.x
    task: #3619803 Tell the three message types apart without relying on...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.