The bar along the top of Mylder's moving map, on every screen size: the Mylder brand, the status line that says when the map's data is from, the screen's filters and its tools, on one glass panel over the map.
When to use
Use TopBar for the bar along the top of Mylder's moving map: the brand, the status line that says when the map's data is from, the screen's filters and its tools.
| Use instead | When |
|---|---|
| KioskBar | The screen is in kiosk mode, read from across a room and never touched |
| SourceLine | A panel names where its own data comes from and the time of that data |
| Toolbar | A few actions sit together in one bar, with no brand or status line |
Props
| Name | Type | Default | Description |
|---|---|---|---|
size | 'medium' | 'small' | 'responsive' | 'medium' | The bar's layout: medium is one row at least size.top-bar tall, for a wide screen; small is the phone's, at least size.top-bar-phone tall, with the status line under the brand and the filters on a second row of their own that scrolls sideways; responsive is small under breakpoint.phone and medium above it, switched in CSS from one render, for a page rendered before it knows the viewport's width. A screen that knows it picks small under breakpoint.phone. |
brandHref | string | — | Where the brand leads, such as the start screen, '/en'. Given, the brand is a link there; left out, it is the brand alone. |
time | string | — | The time of the data the map shows, 24-hour HH:MM:SS, such as '08:14:05', or HH:MM, in Copenhagen's time, shown as "Updated 08:14:05" as it is given, and given again when the data changes; the bar never counts up from it. Left out, before any data has come, the line reads "Updated —". |
paused | boolean | false | The map takes no updates for now: the status line starts with "Paused", and the time stays the time of the data the map shows. |
outOfDate | boolean | false | The app judges the data may be out of date, such as when its last refresh failed or the time is past the app's limit: the line reads "Updated 08:09:40, may be out of date", in warning ink. The bar never judges it from the time. |
filters | ReactNode | — | The screen's filters, in the order they show: a FilterChip for each, with its count, its state and its handler kept by the caller. Left out, the bar has no filters group. |
tools | ReactNode | — | The screen's tools at the end of the bar, in the order they show, such as the language link, a small outline LinkButton, and the About button, a large IconButton. Left out, the bar has no tools group. |
updatedLabel | string | The locale's | Overrides "Updated {time}", the status line's phrase; {time} is filled with time, or "—" when it is left out |
updatedOutOfDateLabel | string | The locale's | Overrides "Updated {time}, may be out of date", the phrase while outOfDate is set; {time} is filled as updatedLabel's is |
sourcePausedLabel | string | The locale's | Overrides "Paused", the word that starts the status line while paused is set |
filtersLabel | string | The locale's | Overrides "Filters", the filters group's name, heard as the group is entered, such as "Modes" where every filter is a mode |
brandNameLabel | string | The locale's | Overrides "Mylder", the brand name; given to the Brand |
className | string | — | Joined after pn-TopBar on the root, such as the screen's class that places the bar |
style | CSSProperties | — | Reaches the root |
ref | Ref<HTMLDivElement> | — | Reaches the root element |
slotProps | { identity?: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> | undefined; brand?: Omit<BrandProps, "brandNameLabel" | "render"> | undefined; status?: Omit<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "children" | "color"> | undefined; paused?: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> | undefined; separator?: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> | undefined; time?: DetailedHTMLProps<TimeHTMLAttributes<HTMLTimeElement>, HTMLTimeElement> | undefined; filters?: DetailedHTMLProps<FieldsetHTMLAttributes<HTMLFieldSetElement>, HTMLFieldSetElement> | undefined; tools?: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> | undefined; } | — | Props for each part: identity, brand, status, paused, separator, time, filters, tools. |
size'medium' | 'small' | 'responsive', default'medium'- The bar's layout:
mediumis one row at leastsize.top-bartall, for a wide screen;smallis the phone's, at leastsize.top-bar-phonetall, with the status line under the brand and the filters on a second row of their own that scrolls sideways;responsiveissmallunderbreakpoint.phoneandmediumabove it, switched in CSS from one render, for a page rendered before it knows the viewport's width. A screen that knows it pickssmallunderbreakpoint.phone. brandHrefstring, default —- Where the brand leads, such as the start screen,
'/en'. Given, the brand is a link there; left out, it is the brand alone. timestring, default —- The time of the data the map shows, 24-hour
HH:MM:SS, such as'08:14:05', orHH:MM, in Copenhagen's time, shown as "Updated 08:14:05" as it is given, and given again when the data changes; the bar never counts up from it. Left out, before any data has come, the line reads "Updated —". pausedboolean, defaultfalse- The map takes no updates for now: the status line starts with "Paused", and the time stays the time of the data the map shows.
outOfDateboolean, defaultfalse- The app judges the data may be out of date, such as when its last refresh failed or the time is past the app's limit: the line reads "Updated 08:09:40, may be out of date", in warning ink. The bar never judges it from the time.
filtersReactNode, default —- The screen's filters, in the order they show: a FilterChip for each, with its count, its state and its handler kept by the caller. Left out, the bar has no filters group.
toolsReactNode, default —- The screen's tools at the end of the bar, in the order they show, such as the language link, a small outline LinkButton, and the About button, a large IconButton. Left out, the bar has no tools group.
updatedLabelstring, default The locale's- Overrides "Updated {time}", the status line's phrase;
{time}is filled withtime, or "—" when it is left out updatedOutOfDateLabelstring, default The locale's- Overrides "Updated {time}, may be out of date", the phrase while
outOfDateis set;{time}is filled asupdatedLabel's is sourcePausedLabelstring, default The locale's- Overrides "Paused", the word that starts the status line while
pausedis set filtersLabelstring, default The locale's- Overrides "Filters", the filters group's name, heard as the group is entered, such as "Modes" where every filter is a mode
brandNameLabelstring, default The locale's- Overrides "Mylder", the brand name; given to the Brand
classNamestring, default —- Joined after
pn-TopBaron the root, such as the screen's class that places the bar styleCSSProperties, default —- Reaches the root
refRef<HTMLDivElement>, default —- Reaches the root element
slotProps{ identity?: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> | undefined; brand?: Omit<BrandProps, "brandNameLabel" | "render"> | undefined; status?: Omit<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "children" | "color"> | undefined; paused?: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> | undefined; separator?: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> | undefined; time?: DetailedHTMLProps<TimeHTMLAttributes<HTMLTimeElement>, HTMLTimeElement> | undefined; filters?: DetailedHTMLProps<FieldsetHTMLAttributes<HTMLFieldSetElement>, HTMLFieldSetElement> | undefined; tools?: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> | undefined; }, default —- Props for each part:
identity,brand,status,paused,separator,time,filters,tools.
Use it
import { TopBar } from '@mylder/perron/patterns'Ask your agent
search_docs({ query: "TopBar" })Keyboard and ARIA
TopBar has no keys of its own. Tab moves through what the bar holds in the order it shows, which is the DOM order (CR-3): the brand's link when brandHref is given, then each chip in filters and then each control in tools in a medium bar, or the tools and then the filters in a small one. A responsive bar keeps the medium order in the DOM at both widths; under breakpoint.phone, a browser that follows reading-flow tabs in the order on screen, the tools and then the filters, as a small bar does, and one without it, iOS Safari among them, follows the DOM, the filters on the second row before the tools on the first, away from the order on screen; the groups don't depend on each other, and one DOM order must differ from the screen at one width (CR-16). The chips, buttons and links answer their own keys, Enter and Space, as their specs say.
| Key | Does | From |
|---|---|---|
Tab | Moves to the next focusable element in the bar, in the order on screen: the brand's link, then the filters and the tools, or with size='small' the tools and the filters | The browser |
Shift+Tab | Moves back through the same elements, and out of the bar to whatever came before it | The browser |
Tab | With size='responsive' under breakpoint.phone, moves in the order on screen, the brand's link, the tools, then the filters, in a browser that follows reading-flow, and in the DOM's order, the filters before the tools, in one that doesn't. Untested: user-event's Tab walks the DOM order itself and can't follow reading-flow as the browser does; ResponsivePhone checks reading-flow: flex-visual and the order on screen, and ResponsiveWide presses Tab through the wide order | The browser |
| Element | Role | Accessible name from | States announced |
|---|---|---|---|
root | None (generic div) | None | None |
brand | Link with brandHref; otherwise none (Brand's span) | Its text: "Mylder", from the wordmark or Brand's hidden label | None |
status | None (Text's span) | None; its text is read in place | None |
time | Time, the role ARIA gives the element | Its text, "Updated 08:14:05" | None |
filters | Group | aria-label: the owned "Filters", or filtersLabel | None |
tools | None (generic div); the caller's controls carry theirs | The caller's controls' own names | Theirs |
The filters' name is an aria-label on the fieldset, not a legend, as Mylder's composition names it: a legend renders on the fieldset's edge outside its flex row, and the VisuallyHidden part that would hide one is a span.
The status line is no live region: its time changes on every refresh, which would flood a screen reader, and Accessibility asks that only changes of state be announced, in the screen's own polite live region, as SourceLine's line is none.
What axe won't catch, from the contract, each with how this component meets it:
- Everything the eye is told, the ear is told, for every shape each prop accepts: the brand is heard as "Mylder" in both of Brand's variants and as a link with
brandHref; the status line is its own text, "Paused · Updated 08:14:05" or "Updated 08:09:40, may be out of date", so the paused and out-of-date states are words, not only inks; the filters group is named, and each chip says its count and its pressed state itself. The unknown time's "—" is text, read as the screen reader names it. - Every landmark and named region has an accessible name: the root is not a landmark, and the screen that wraps it in a
headerhas its banner; the filters group is named "Filters", or byfiltersLabel. - Every default label can be overridden:
updatedLabel,updatedOutOfDateLabel,sourcePausedLabel,filtersLabelandbrandNameLabel. - Every shape the type accepts renders, in every variant:
filtersandtoolsrender a string, an element, a fragment and a list alike, and render no group fornull,falseor an empty list;timegiven asHH:MM:SS, asHH:MMand left out;pausedandoutOfDatealone and together; every size,responsiveat both widths.TopBar.test.tsxrenders each shape offiltersandtools. - A hidden copy kept for animation is
aria-hidden, and a collapsed region isinert: none here; nothing animates or collapses. - Native semantics first: a
fieldsetfor the filters, atimefor the time, an<a href>for the brand's link, plaindivandspanfor the rest, and no ARIA of the pattern's own but the group'saria-label. - Focus moves in reading order and returns on close: the DOM order is the order on screen with
sizemediumandsmall(CR-3), and withresponsiveabovebreakpoint.phone; under it,reading-flowbrings Tab to the order on screen in a browser that follows it. A browser withoutreading-flow, iOS Safari among them, moves focus on a phone from the brand to the filters on the second row and then up to the tools on the first, away from the order on screen. That is accepted: one DOM order must differ from the screen at one of the two widths, and the groups don't depend on each other, so meeting the filters first changes no meaning and loses no control (CR-16). Nothing in the bar opens or closes. - Every target is at least
size.target-minsquare: the brand's link keeps Brand'ssize.target-minheight; the chips and buttons the stories put in the bar aresize.controlandsize.button-smtall. - Loops stop when the system asks for less motion: none here; nothing moves, and the time changes its text without a transition.
Owned strings
Three new keys, in packages/perron/src/locale/en.ts and the Danish, Swedish and Norwegian dictionaries, which agents write, and three reused. The two phrases wrap the caller's time, so each is one string with a {time} placeholder, filled by interpolate, and its override takes the same placeholder (the contract's Words). They start in sentence case, "Updated", since each starts the line, where SourceLine's lower-case "updated" sits after a source's name.
| Key | English | Override | Seen or heard where |
|---|---|---|---|
topBarUpdated | "Updated {time}" | updatedLabel | The time part: the status line's phrase, read in place |
topBarUpdatedOutOfDate | "Updated {time}, may be out of date" | updatedOutOfDateLabel | The time part while outOfDate is set, in warning ink, read in place |
topBarFilters | "Filters" | filtersLabel | The filters part: the group's name, heard as the group is entered; never seen |
sourcePaused | "Paused" | sourcePausedLabel | The paused part, first in the status line while paused is set; reused from SourceLine |
brandName | "Mylder" | brandNameLabel | The brand part: Brand's wordmark, shown lowercase, or its hidden label with variant='mark' |
The new keys are topBarUpdated "Updated {time}", topBarUpdatedOutOfDate "Updated {time}, may be out of date" and topBarFilters "Filters", scoped to the pattern as SourceLine scopes its own, with SourceLine's sourcePaused reused for "Paused" and Brand's brandName for the brand. The words are the ticket's ("Updated 08:14:05") and SourceLine's, in sentence case for the start of a line; a key the dictionary has is reused, as the contract asks; "Filters" names the group by what it holds on any screen, and Mylder, whose filters are its modes, gives "Modes" through filtersLabel.
Do and don't
For a caller using the pattern, with the reason. Each row is a rule: a must row's Don't is never right, a should row's needs a reason, and Checked by says what catches the Don't in a caller's code.
| Rule | Level | Do | Don't | Checked by |
|---|---|---|---|---|
| DD-1 | should | Set size to small under breakpoint.phone, in the screen's code, or to responsive where the page renders before it knows its width | Render a medium and a small bar and hide one with a media query: responsive is both from one render, with one set of the caller's elements | review |
| DD-2 | must | Place the bar from the screen, size.panel-gap from the edges at z-index.top-bar, and wrap it in the page's header | Give it a position or a z-index through slotProps, or expect it to be a landmark on its own | review |
| DD-3 | must | Give time in Copenhagen's time as HH:MM:SS, the time the data is from, and give it again when the data changes | Pass the time now, or a time counting up: the line says when the data is from, and the brand never claims fresher data than it has | review |
| DD-4 | must | Set outOfDate when the app judges the data may be stale, and paused when the map takes no updates | Leave a stale time showing as if fresh, or show "Paused" by changing updatedLabel: the states have their own words and inks | review |
| DD-5 | should | Put a FilterChip in filters for each filter, with its count, and keep its state in the screen, such as in the address | Put buttons, switches or a segmented control in filters: a filter people turn on and off is a FilterChip, and the group is named for filters | review |
| DD-6 | should | Put the language link, a small outline LinkButton, and the About button, a large IconButton with an 18 px-grid icon, in tools | Fill tools until the first row wraps on a phone: the brand, the status line and the tools share it | review |
| DD-7 | should | Give filtersLabel the word that names what the filters hold, such as "Modes" | Leave a group of modes named "Filters" where every chip is a mode | review |
| DD-8 | must | Pick slotProps.brand.variant 'mark' under breakpoint.compact-phone | Hide the brand or its parts with className, which can leave a mark no screen reader hears | review |
| DD-9 | must | Announce a change of state, such as the data feed failing, in the screen's own polite live region | Make the status line a live region: its time changes on every refresh and would flood a screen reader | review |