Skip to the content

Props

NameTypeDefaultDescription
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.
brandHrefstring—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—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 —".
pausedbooleanfalseThe map takes no updates for now: the status line starts with "Paused", and the time stays the time of the data the map shows.
outOfDatebooleanfalseThe 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—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—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.
updatedLabelstringThe locale'sOverrides "Updated {time}", the status line's phrase; {time} is filled with time, or "—" when it is left out
updatedOutOfDateLabelstringThe locale'sOverrides "Updated {time}, may be out of date", the phrase while outOfDate is set; {time} is filled as updatedLabel's is
sourcePausedLabelstringThe locale'sOverrides "Paused", the word that starts the status line while paused is set
filtersLabelstringThe locale'sOverrides "Filters", the filters group's name, heard as the group is entered, such as "Modes" where every filter is a mode
brandNameLabelstringThe locale'sOverrides "Mylder", the brand name; given to the Brand
classNamestring—Joined after pn-TopBar on the root, such as the screen's class that places the bar
styleCSSProperties—Reaches the root
refRef<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: 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.
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', 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 —".
pausedboolean, default 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.
outOfDateboolean, default 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.
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 with time, or "—" when it is left out
updatedOutOfDateLabelstring, default The locale's
Overrides "Updated {time}, may be out of date", the phrase while outOfDate is set; {time} is filled as updatedLabel's is
sourcePausedLabelstring, default The locale's
Overrides "Paused", the word that starts the status line while paused is 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-TopBar on 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.

KeyDoesFrom
TabMoves 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 filtersThe browser
Shift+TabMoves back through the same elements, and out of the bar to whatever came before itThe browser
TabWith 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 orderThe browser
ElementRoleAccessible name fromStates announced
rootNone (generic div)NoneNone
brandLink with brandHref; otherwise none (Brand's span)Its text: "Mylder", from the wordmark or Brand's hidden labelNone
statusNone (Text's span)None; its text is read in placeNone
timeTime, the role ARIA gives the elementIts text, "Updated 08:14:05"None
filtersGrouparia-label: the owned "Filters", or filtersLabelNone
toolsNone (generic div); the caller's controls carry theirsThe caller's controls' own namesTheirs

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 header has its banner; the filters group is named "Filters", or by filtersLabel.
  • Every default label can be overridden: updatedLabel, updatedOutOfDateLabel, sourcePausedLabel, filtersLabel and brandNameLabel.
  • Every shape the type accepts renders, in every variant: filters and tools render a string, an element, a fragment and a list alike, and render no group for null, false or an empty list; time given as HH:MM:SS, as HH:MM and left out; paused and outOfDate alone and together; every size, responsive at both widths. TopBar.test.tsx renders each shape of filters and tools.
  • A hidden copy kept for animation is aria-hidden, and a collapsed region is inert: none here; nothing animates or collapses.
  • Native semantics first: a fieldset for the filters, a time for the time, an <a href> for the brand's link, plain div and span for the rest, and no ARIA of the pattern's own but the group's aria-label.
  • Focus moves in reading order and returns on close: the DOM order is the order on screen with size medium and small (CR-3), and with responsive above breakpoint.phone; under it, reading-flow brings Tab to the order on screen in a browser that follows it. A browser without reading-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-min square: the brand's link keeps Brand's size.target-min height; the chips and buttons the stories put in the bar are size.control and size.button-sm tall.
  • 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.

KeyEnglishOverrideSeen or heard where
topBarUpdated"Updated {time}"updatedLabelThe time part: the status line's phrase, read in place
topBarUpdatedOutOfDate"Updated {time}, may be out of date"updatedOutOfDateLabelThe time part while outOfDate is set, in warning ink, read in place
topBarFilters"Filters"filtersLabelThe filters part: the group's name, heard as the group is entered; never seen
sourcePaused"Paused"sourcePausedLabelThe paused part, first in the status line while paused is set; reused from SourceLine
brandName"Mylder"brandNameLabelThe 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.

RuleLevelDoDon'tChecked by
DD-1shouldSet size to small under breakpoint.phone, in the screen's code, or to responsive where the page renders before it knows its widthRender 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 elementsreview
DD-2mustPlace the bar from the screen, size.panel-gap from the edges at z-index.top-bar, and wrap it in the page's headerGive it a position or a z-index through slotProps, or expect it to be a landmark on its ownreview
DD-3mustGive time in Copenhagen's time as HH:MM:SS, the time the data is from, and give it again when the data changesPass 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 hasreview
DD-4mustSet outOfDate when the app judges the data may be stale, and paused when the map takes no updatesLeave a stale time showing as if fresh, or show "Paused" by changing updatedLabel: the states have their own words and inksreview
DD-5shouldPut a FilterChip in filters for each filter, with its count, and keep its state in the screen, such as in the addressPut buttons, switches or a segmented control in filters: a filter people turn on and off is a FilterChip, and the group is named for filtersreview
DD-6shouldPut the language link, a small outline LinkButton, and the About button, a large IconButton with an 18 px-grid icon, in toolsFill tools until the first row wraps on a phone: the brand, the status line and the tools share itreview
DD-7shouldGive filtersLabel the word that names what the filters hold, such as "Modes"Leave a group of modes named "Filters" where every chip is a modereview
DD-8mustPick slotProps.brand.variant 'mark' under breakpoint.compact-phoneHide the brand or its parts with className, which can leave a mark no screen reader hearsreview
DD-9mustAnnounce a change of state, such as the data feed failing, in the screen's own polite live regionMake the status line a live region: its time changes on every refresh and would flood a screen readerreview