Applications
The last tier, and a different kind of component. Everything before this page is a control: a thing you press, type into or choose from. These three are applications in their own right — a carousel, a calendar and an org chart — each several times the size of anything in the families above it.
They are also where a component library usually stops being honest about accessibility, because the amount of behaviour makes it easy to. The rule the rest of this library follows applies hardest here: what the component is is what the accessibility tree is told, not what it looks like.
One team, three views
All three components on one screen, doing one job between them. Choose somebody in the chart — click them, or Tab in and use the arrows and Enter — and the diary shows that person's week and the strip shows what they are working on. Nothing here is a screenshot and nothing is a log: each component visibly changes what the other two are looking at.
Three components, three different right answers about the same question. The chart is
a tree, because a hierarchy is what its drawing means. The diary's week is a
grid whose columns are days and whose hour rail is not announced at all, because a
row-per-half-hour table has no honest cell for a meeting that runs 9:15 to 10:45. The strip is a
group rather than a tablist, because its dots indicate rather than control.
None of those three is a styling decision, and none of them is guessable from the picture —
which is the argument for deciding a component's shape by what the accessibility tree is told.
None of them owns the data. Choosing a person emits an event; this page reassigns the
calendar's events and rebuilds the strip's slides, and the components redraw. That is the
one-way flow every component in this library uses, and it is the only shape that works from React,
from Blazor and from a plain page like this one at the same time — this page is plain HTML and
about sixty lines of script, with no framework anywhere on it.
A carousel that tells the truth
Six pieces of work. Use it however you like — flick it, drag it, press the arrows, use the keyboard — and watch the panel underneath, which reads what the component is actually doing rather than what this page hoped it would.
The line to watch is Said. While the carousel is moving on its own the live region is
off and the reader is told nothing — a carousel announcing a slide every four seconds
interrupts the rest of the page forever, for something nobody asked to happen. Press an arrow, drag it, or
choose a dot, and the same region becomes polite, because now the change is the answer to
something the reader did. That is the WAI carousel pattern's rule rather than a preference, and it is four
lines of src/core/carousel.ts.
Carousel
Built on the platform's own scroller, which is the decision everything else follows from.
inert — which is the defect
this pattern ships with almost everywhere, and it is invisible to anybody testing with a mouse.
A carousel built on a transform owns the strip and has to imitate everything the reader's device already
does: momentum, the rubber-band at the ends, and — the one that is never quite right — what
happens when a drag is interrupted. This one is a scroll-snap container with the slides
slotted into it, so all of that is the browser's, on the hardware it is running on. The cost is that the
component no longer knows where the strip is and has to ask, which is why every decision it makes comes
from a measured scroll offset rather than from an index it keeps. Two sources of truth disagree the first
time somebody drags.
Calendar
A diary, and the whole month below is one element with a JSON attribute. Move around it with the keyboard and watch the panel: it reports what the grid is actually telling a screen reader, which is not what you can see.
grid at all. Without
it the events in a month are either forty-two extra tab stops or unreachable, and every calendar
component picks one of those two.
The Conference is one bar across three days, not three pieces that line up — and
crossing a week boundary it would draw as two, each open at the end where it continues. That clipping and
the stacking of overlapping bars are weekBars in src/core/diary.ts, which is the
same lane-packing function a day column will use for two meetings that overlap at half past ten. They
look nothing alike on screen and they are the same problem: a set of intervals, and the fewest rows that
hold them without a collision. Doing it as arithmetic is what makes the layout testable at all, and the
layout is where every calendar component goes wrong.
The same diary, on a time axis
Switch it to Week or Day with the buttons in its own header. The events are the same JSON; what changes is that minutes now have a position, and two things at half past nine have to share a column.
The hour rail is decoration, and is not announced. The obvious markup for a time grid — a row per half hour, a cell per day — is a lie the moment two meetings overlap or one runs from 9:15 to 10:45: there is no honest cell for it to be in, and a reader arrowing through that table hears fragments of meetings in slots they do not start in. So each day is one cell, and its name is the day in time order: “Monday 7 September, 4 events, Standup, 9:00 to 9:15…”. One utterance with the shape of the day in it, then Enter for the detail. Position is left to the eyes it was drawn for.
Tuesday's early call starts at half past six and the grid begins at eight. It is not dropped and it is not squashed — it is clipped to the top edge and drawn open, and it is still announced at half past six, because the reader is being told when the meeting is rather than where the box was cut. Wednesday shows the other half of the same rule: an event with no time of day has no position on a time axis, so the Conference is in the strip and never in the column. Drawing it at midnight is the classic way to get this wrong.
Rescheduling, from the keyboard first
The diary below is editable, and this page accepts what it is told: drop a meeting
somewhere else and it stays there, because the page reassigned events. It also refuses
one thing — nothing may be moved onto Friday, the release day — and the
refusal is visible while the block is still in your hand rather than a snap-back a moment after you
thought you had put it down.
readonly and will not move at all.
The keyboard was written first, and that is not politeness. A drag that is only a pointer gesture is not a feature, it is a feature for some readers — and it is the part every calendar component quietly gives up on. So the grab mode is the real one: Space picks up, the arrows move, Shift resizes, and every step is said out loud — “Standup, 9:45 to 10:00” — because a block that moves on screen and says nothing has been given a mode with no output. The pointer is then a second way into the same state machine, not a second implementation of it, so the two cannot disagree.
Nothing changes until you let go, and then the page decides. While the block is moving
it is drawn from a proposal; your events array is untouched. On the drop the component
emits bmxEventChange with the old times and the new ones, and if the page ignores
it the block goes back — which is the honest behaviour for a page that has not agreed to the
change. That is the same one-way flow every other component here uses, and it is the only shape that
works from React, from Blazor and from a plain page at once.
A drag interrupted by a phone call puts the event back. A notification, a scroll
gesture or an incoming call takes the pointer away mid-drag, and a component that only listens for
pointerup leaves the meeting half moved with no way to undo it. It is the case nobody
tests and everybody reports. There is also no animation anywhere in this: a cancelled drag returns to
its position rather than gliding back to it, so there is nothing to switch off under reduced motion and
every reader sees the same thing.
Org chart
A hierarchy drawn as a diagram — and, to a screen reader, not a diagram at all. Tab into it once, then use the arrows: ↓ and ↑ walk the people, → opens a branch and steps into it, ← closes it and goes back up to the manager, and typing a name jumps to it. The readout below says what the accessibility tree is being told at each step.
The connectors are the component, and they are arithmetic. Where every box goes is
Reingold and Tilford's tidy-tree algorithm — a parent centred over its children, identical
subtrees drawn identically wherever they appear, nothing overlapping, and the whole thing as narrow as
those allow — in Buchheim's linear-time form, about a hundred and fifty lines with no
dependencies. Every curve is a cubic between two anchors chosen by the direction of travel. All of it
is src/core/orgchart.ts, pure, with fifty unit tests: a layout that can only be
looked at cannot be checked, which is the same sentence that put the calendar's lane packing in
a file of its own.
To a screen reader this is a tree, not a picture. There is no role for
“boxes joined by curves”, and nobody can follow a line with their ears. What the drawing
means is a hierarchy, and a hierarchy has a role — so the SVG layer is
aria-hidden decoration and the boxes are a real nested tree reporting its
levels, positions and open state. The DOM carries the hierarchy, CSS carries the positions, and both
answer to the same layout. A connector is never the only place a relationship is
stated: something drawn in a hidden layer has to be said somewhere that is not, which is
exactly what the calendar's month view had wrong for two batches.
Width is declared and height is measured. Uniform widths are what make a chart tidy,
so the width is a property the component writes back into its own CSS — the box that is drawn
and the box that was laid out are the same box by construction. Height is the opposite: your font,
your padding, a job title that wraps to two lines all change it, and a layout told the wrong height
draws the boxes overlapping. So the tallest rendered node is measured and fed back in. Asking CSS
instead would not work: getComputedStyle hands a custom property back unresolved, and
parseFloat('4.5rem') is 4.5.
Tier 4 is finished
Three applications rather than controls — a carousel over the platform's own scroller, a calendar with four views and rescheduling from either hand, and an org chart whose layout is an algorithm with a name. What is next is pan and zoom and consumer-drawn node content for the chart, which is where a render callback finally earns its place.