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.

Six people, four Tab stops for the whole screen. One for the chart, one for the diary, one for the strip, one for its controls — and inside each, the arrows. A page that gave each of the twenty-odd boxes, cells and slides its own Tab stop would be unusable long before it was inaccessible.

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.

Northwind rebrand
Contoso annual report
Fabrikam launch
Tailspin identity
Adventure Works site
Proseware campaign
Hover it and the clock stops. So does putting the focus inside it, and so does moving to another browser tab — each of those is reported by name, which is why the panel can say what is holding it rather than just that it is still. Press pause and that outranks all of them.

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.

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.

Tab once, then use the arrows. Forty-two cells and one tab stop. Press Enter on a busy day to step into it and reach the events, and Escape to come back out — that two-mode model is what lets a month be a 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.

Tab once, then use the arrows. Left and right walk the days; and move between the all-day strip and the column below it, because that is what is above and below in a week — not seven days. Enter steps into a day and the arrows then walk the meetings in it.

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.

Tab in, Enter into a day, then Space to pick a meeting up. The arrows move it — a quarter hour down the column, a day across — Shift with them makes it longer or shorter, Enter drops it and Esc puts it back. The pointer does the same things: drag the block, or its top and bottom edges. “Payroll” on Thursday is 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.

Sixteen people, one Tab stop. Two generations are open and the rest are behind a counter — a chart that opens with eleven hundred boxes on it has answered no question anybody had. * opens a whole generation at once.

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.