The panel that opens on the right of the map when a rider picks a vehicle or a stop: a Sheet headed by a DetailHeader, which names the line's vehicle or the stop, with the close button at the end of its header row and the caller's details below, and the bottom sheet it becomes on a phone.
When to use
Use DetailPanel for the panel that opens beside the map when a rider picks a vehicle or a stop, headed by what it shows and holding its details.
| Use instead | When |
|---|---|
| Sheet | The panel is not about one vehicle or stop, such as the overview sheet |
| DetailHeader | Only the header is wanted, at the top of a panel the screen draws itself |
| MapTooltip | A few words on a vehicle or a stop show while the pointer rests on it, with no panel opened |
| Dialog | The panel must hold focus over the map until it closes, such as the help with the keys |
Variants and states
| Rest | |
|---|---|
| Line | |
| Stop |
- Rest
Props
| Name | Type | Default | Description |
|---|---|---|---|
variant | 'line' | 'stop' | 'line' | What the panel shows, as DetailHeader's variant: line for a vehicle or a line, headed by its line badge and destination; stop for a stop or station, headed by its marker and name. Set on the root as data-variant. |
line | 'm1' | 'm2' | 'm3' | 'm4' | 'a' | 'b' | 'c' | 'e' | 'f' | 'h' | 'bus' | 'regional' | — | The line whose colours the header's mark takes, as DetailHeader's line: in line, the badge's colours, needed with lineName; in stop, the marker's colour, left out for a stop several lines serve. |
lineName | ReactNode | — | The line's short name as riders know it, "5C", "M2" or "A", as DetailHeader's lineName: the badge's label in line, also read first in the title and so in the panel's name. Not used in stop. |
title* | ReactNode | — | The panel's title, as DetailHeader's title: the destination in line ("Husum Torv"), the stop's name in stop ("Nørreport St."). Required. It names the panel's dialog, after the line's name in line. Replaces the HTML title attribute. |
subheader | ReactNode | — | One supporting line under the title, as DetailHeader's subheader, such as "Next stop: Nørreport St." or the lines a stop has. |
action | ReactNode | — | The panel's own controls, in a row of their own under the title and the subheader, as DetailHeader's action, such as a "Show on map" button. The close button is the panel's and needs no place here. |
children | ReactNode | — | The panel's details, below the header row, set space.300 apart in a column: for a vehicle its StopTimeline and FactList, for a stop its Sections of departures. It scrolls when it is taller than the panel. The body comes as free children, not as data such as a vehicle or a stop that the panel lays out itself: a vehicle's panel and a stop's hold different patterns, StopTimeline, FactList, DisruptionCard and Sections, which the screen composes, and the panel is their frame, as Sheet is. |
anchor | 'right' | 'bottom' | 'right' | The edge the panel slides from, as the Sheet's anchor: right beside the map on a wide screen, bottom on a phone, under breakpoint.phone, which the screen picks. The panel takes only right and bottom, with right the default, and not the Sheet's left: the foundations put the detail panel on the right and the overview sheet on the left, so a left detail panel would sit over the overview sheet. |
open | boolean | — | Whether the panel is open, for a controlled panel, as the Sheet's open: the screen opens it when the rider picks a vehicle or a stop. |
defaultOpen | boolean | false | Whether the panel starts open, for an uncontrolled panel, as the Sheet's defaultOpen. |
onOpen | (event, details) => void | — | Called when the panel asks to open, from its trigger, as the Sheet's onOpen. |
onClose | (event, reason, details) => void | — | Called when the panel asks to close, as the Sheet's onClose, with Base UI's reason: escape-key, close-press, swipe, trigger-press and the rest the Sheet lists. details.cancel() keeps it open. A controlled panel sets open to false here, and the screen clears its selection. |
trigger | ReactElement<unknown, JSXElementConstructor<any> | string> | — | The button that opens the panel, as the Sheet's trigger; it gets the trigger's ARIA and focus back on close. Without one, the panel opens through open, as it does from the map. |
snapPoints | DrawerSnapPoint[] | undefined | With anchor 'bottom': size.sheet-peek and 1 | The heights a bottom panel snaps to, as the Sheet's snapPoints. Ignored with anchor right. |
snapPoint | null | number | string | undefined | — | The snap point a bottom panel is at, for a controlled snap point, as the Sheet's snapPoint. |
defaultSnapPoint | null | number | string | undefined | The first snap point | The snap point a bottom panel opens at, for an uncontrolled snap point, as the Sheet's defaultSnapPoint. |
onSnapPointChange | (event, snapPoint, details) => void | — | Called when a bottom panel asks to move to another snap point, by a swipe, as the Sheet's onSnapPointChange. |
closeLabel | string | The locale's | Overrides "Close", the close button's label; given to the Sheet |
className | string | — | Joined after pn-Sheet and pn-DetailPanel on the root, the panel's popup |
style | CSSProperties | — | Reaches the root, the panel's popup |
ref | Ref<HTMLDivElement> | — | Reaches the root element, the panel's popup |
slotProps | { header?: Omit<DetailHeaderProps, "action" | "line" | "lineName" | "subheader" | "title" | "variant"> | undefined; trigger?: Props<unknown> | undefined; portal?: DrawerPortalProps | undefined; viewport?: DrawerViewportProps | undefined; close?: DrawerCloseProps | undefined; content?: DrawerContentProps | undefined; } | — | Props for each part: header, trigger, portal, viewport, close, content. |
variant'line' | 'stop', default'line'- What the panel shows, as DetailHeader's
variant:linefor a vehicle or a line, headed by its line badge and destination;stopfor a stop or station, headed by its marker and name. Set on the root asdata-variant. line'm1' | 'm2' | 'm3' | 'm4' | 'a' | 'b' | 'c' | 'e' | 'f' | 'h' | 'bus' | 'regional', default —- The line whose colours the header's mark takes, as DetailHeader's
line: inline, the badge's colours, needed withlineName; instop, the marker's colour, left out for a stop several lines serve. lineNameReactNode, default —- The line's short name as riders know it, "5C", "M2" or "A", as DetailHeader's
lineName: the badge's label inline, also read first in the title and so in the panel's name. Not used instop. title (required)ReactNode, default —- The panel's title, as DetailHeader's
title: the destination inline("Husum Torv"), the stop's name instop("Nørreport St."). Required. It names the panel's dialog, after the line's name inline. Replaces the HTMLtitleattribute. subheaderReactNode, default —- One supporting line under the title, as DetailHeader's
subheader, such as "Next stop: Nørreport St." or the lines a stop has. actionReactNode, default —- The panel's own controls, in a row of their own under the title and the subheader, as DetailHeader's
action, such as a "Show on map" button. The close button is the panel's and needs no place here. childrenReactNode, default —- The panel's details, below the header row, set
space.300apart in a column: for a vehicle its StopTimeline and FactList, for a stop its Sections of departures. It scrolls when it is taller than the panel. The body comes as freechildren, not as data such as a vehicle or a stop that the panel lays out itself: a vehicle's panel and a stop's hold different patterns, StopTimeline, FactList, DisruptionCard and Sections, which the screen composes, and the panel is their frame, as Sheet is. anchor'right' | 'bottom', default'right'- The edge the panel slides from, as the Sheet's
anchor:rightbeside the map on a wide screen,bottomon a phone, underbreakpoint.phone, which the screen picks. The panel takes onlyrightandbottom, withrightthe default, and not the Sheet'sleft: the foundations put the detail panel on the right and the overview sheet on the left, so aleftdetail panel would sit over the overview sheet. openboolean, default —- Whether the panel is open, for a controlled panel, as the Sheet's
open: the screen opens it when the rider picks a vehicle or a stop. defaultOpenboolean, defaultfalse- Whether the panel starts open, for an uncontrolled panel, as the Sheet's
defaultOpen. onOpen(event, details) => void, default —- Called when the panel asks to open, from its trigger, as the Sheet's
onOpen. onClose(event, reason, details) => void, default —- Called when the panel asks to close, as the Sheet's
onClose, with Base UI's reason:escape-key,close-press,swipe,trigger-pressand the rest the Sheet lists.details.cancel()keeps it open. A controlled panel setsopento false here, and the screen clears its selection. triggerReactElement<unknown, JSXElementConstructor<any> | string>, default —- The button that opens the panel, as the Sheet's
trigger; it gets the trigger's ARIA and focus back on close. Without one, the panel opens throughopen, as it does from the map. snapPointsDrawerSnapPoint[] | undefined, default With anchor 'bottom': size.sheet-peek and 1- The heights a bottom panel snaps to, as the Sheet's
snapPoints. Ignored withanchorright. snapPointnull | number | string | undefined, default —- The snap point a bottom panel is at, for a controlled snap point, as the Sheet's
snapPoint. defaultSnapPointnull | number | string | undefined, default The first snap point- The snap point a bottom panel opens at, for an uncontrolled snap point, as the Sheet's
defaultSnapPoint. onSnapPointChange(event, snapPoint, details) => void, default —- Called when a bottom panel asks to move to another snap point, by a swipe, as the Sheet's
onSnapPointChange. closeLabelstring, default The locale's- Overrides "Close", the close button's label; given to the Sheet
classNamestring, default —- Joined after
pn-Sheetandpn-DetailPanelon the root, the panel's popup styleCSSProperties, default —- Reaches the root, the panel's popup
refRef<HTMLDivElement>, default —- Reaches the root element, the panel's popup
slotProps{ header?: Omit<DetailHeaderProps, "action" | "line" | "lineName" | "subheader" | "title" | "variant"> | undefined; trigger?: Props<unknown> | undefined; portal?: DrawerPortalProps | undefined; viewport?: DrawerViewportProps | undefined; close?: DrawerCloseProps | undefined; content?: DrawerContentProps | undefined; }, default —- Props for each part:
header,trigger,portal,viewport,close,content.
Use it
import { DetailPanel } from '@mylder/perron/patterns'Ask your agent
search_docs({ query: "DetailPanel" })Keyboard and ARIA
From the Sheet and its Base UI Drawer. "From" says who does it: Base UI, the Sheet, or the pattern.
| Key | Does | From |
|---|---|---|
Enter, Space | On the trigger, opens the panel and moves focus to the popup | Base UI |
Tab | From the popup, moves to the controls in action, then to the close button, then to the body while it scrolls, in reading order | The browser, in the DOM order the pattern sets (CR-2) |
Tab, Shift+Tab | Moves on out of the panel into the page; the panel stays open, since it is not modal | Base UI, with the Sheet's disablePointerDismissal |
Enter, Space | On the close button, closes the panel with reason close-press, and focus returns to the trigger or to the element focused before | Base UI |
Esc | Closes the panel with reason escape-key when it is the topmost open sheet, after any dialog above it, and focus returns as above | Base UI |
Arrows, PageUp, PageDown, Home, End | Scroll the body while it has focus | The Sheet, with tabIndex 0 while the body overflows |
| Element | Role | Accessible name from | States announced |
|---|---|---|---|
trigger | button | The caller's element | aria-expanded, aria-haspopup="dialog", with aria-controls naming the popup |
root | dialog | The DetailHeader's h2, through the panel's aria-labelledby: "5C Husum Torv" in line, "Nørreport St." in stop | None: it is not modal, so the page stays in the accessibility tree |
header | None (generic div, in the Sheet's title div) | None | None |
| title | heading, level 2, DetailHeader's | Its text: in line the hidden lineName then title, in stop title | None |
close | button | The owned close string, or closeLabel | None |
content | None (generic div) | None | 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 header's badge or marker is
aria-hiddenand its line is read in the title, which names the panel; the close button's icon isaria-hiddenand theCloseButtonpart gives it the owned label; the body scrolls by keys and screen reader at every snap point, as the Sheet makes it. - Every landmark and named region has an accessible name: the dialog is named by DetailHeader's
h2througharia-labelledby(CR-3), never by the whole header row, so the subheader and the action's names stay out of it. - Every default label can be overridden:
closeLabel, given to the Sheet. - Every shape the type accepts renders, in every variant:
title,lineName,subheader,actionandchildrenshow a string, an element and a list alike, inlineandstop; theLineandStopstories'playtests open the panel with each butaction, and theWithActionstory's withaction. - A hidden copy kept for animation is
aria-hidden, and a collapsed region isinert: none here; a closed panel is unmounted, orhiddenwhenslotProps.portal.keepMountedkeeps it, as the Sheet does. - Native semantics first: the
h2from DetailHeader,<button>for the close button and the trigger, and Base UI'sdialogrole on the root; the panel adds onlyaria-labelledby. - Focus moves in reading order and returns on close: the popup, then the header's action, under the title it acts on, then the close button, the last control of the header as the Sheet places it in every sheet, then the body; Base UI returns focus to the trigger or to the element focused before.
- Every target is at least
size.target-minsquare: the close button,size.control; the action's controls keep their own sizes. - Loops stop when the system asks for less motion: none here; the slide reads
duration.sheet, which the tokens shorten to 0.01 ms.
Owned strings
Every word the component shows or announces on its own. Reuse a key packages/perron/src/locale/en.ts already has; a new key gets its English here, and agents write the Danish, Swedish and Norwegian in the locale files.
| Key | English | Override | Seen or heard where |
|---|---|---|---|
close | Close | closeLabel | The Sheet's close button at the end of the header row, announced as its name |
Every other word comes from the caller, through lineName, title, subheader, action and children; the panel adds no key.
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 | must | Open one detail panel at a time, controlled by open, for the vehicle or stop the rider picked, and clear the selection in onClose | Stack a second detail panel over the first; pick the new vehicle into the open panel, so Esc still closes the topmost thing first | review |
| DD-2 | should | Give the destination as title and the line's short name as lineName: "Husum Torv" with "5C" | Put the line in the title as well, "5C Husum Torv": the panel already reads the line first, so its name would say it twice | review |
| DD-3 | must | Put at most one control of your own in action, such as "Show on map", and let the panel draw its close button | Put a close button of your own in action: the panel has one, at the end of the header row, which Esc and focus return go with | review |
| DD-4 | should | Pick anchor bottom under breakpoint.phone, in the screen's code | Expect the panel to turn into a bottom sheet on its own; it holds no media query | review |
| DD-5 | must | Compose the body from Perron's patterns and components, StopTimeline, FactList, Section and DisruptionCard, with their own h3 headings | Put a heading of level 2 in the body: the panel's title is its h2, and a section inside it is an h3 | review |
| DD-6 | must | Keep one primary action per panel | Put a brand button in action and another in the body; a screen has one brand action at most | review |