Overview
Most pages in the application have a repeatable pattern of components. This documentation provides the general best practice for what can be added to a header, where in it, and when to use it. This pattern is used in conjunction with our Page component.
Principles
-
Consistency in structure and page relation. On any given page, how the header arranges descriptions, actions, and navigation should be similar across the application.
-
Prioritize small vertical height. Many pages in the ServiceTitan app are dense. Headers should help users complete their tasks on a page, but should get out of the way for the page’s main content.
-
Consistency in foundational styles. There are many ways to subtly visualize the same header. Standardizing the foundational choices, from text sizes to button variations, improves familiarity to users.
Page Headers are broken up into three general functions: The description of the page, actions on the page, and navigation within the page.
const Example = () => {
const breadcrumbs = (
<Breadcrumb className="m-b-1">
<Breadcrumb.Link label="Root page" />
<Breadcrumb.Link label="Subpage" />
<Breadcrumb.Link label="Current page" />
</Breadcrumb>
);
const title = (text = 'Page Title') => (
<Stack alignItems="center" spacing={1}>
<Headline size="large" className="m-b-0">{text}</Headline>
<Tag>Tag</Tag>
</Stack>
);
const actions = (
<ButtonGroup>
<Button small>Action</Button>
<Button primary small>Primary Action</Button>
</ButtonGroup>
);
const description = (
<BodyText subdued className="m-t-1">A standard description for the entire page.</BodyText>
);
return (
<Frame
header={
<div style={{ height: 56 }}>
<div style={{ backgroundColor: "#141414", height: 56, position: "fixed", top: 0, width: "100%", zIndex: 1 }} />
</div>
}
>
<Page
header={
<>
{breadcrumbs}
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
{title('Page Title')}
</Stack.Item>
{actions}
</Stack>
{description}
<TabGroup className="m-t-3">
<Tab active>Active Tab</Tab>
<Tab>Action Required</Tab>
<Tab>Insights</Tab>
<Tab>Settings</Tab>
</TabGroup>
</>
}
>
<Banner
icon
title="You are viewing an inactive item."
className="m-b-4"
/>
Content of the page.
</Page>
</Frame>
);
}
render (<Example />)
Most pages will provide a basic amount of information in the header, typically just a page title.
Page title
Most pages should have a title. The title should represent the main content of the page itself, including user-generated titles.
<Page header={
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
<Headline size="large" className="m-b-0">Page Title</Headline>
</Stack.Item>
</Stack>
}>
<Divider spacing="4" />
Content of the page.
</Page>
Description
Descriptions provide a more detailed overview of what the page is about. In general this will be a single line of text, but can be customized to fit the specific needs of a page. A title should always be used with a description.
const Example = () => {
const title = (text = 'Page Title') => (
<Stack alignItems="center" spacing={1}>
<Headline size="large" className="m-b-0">{text}</Headline>
</Stack>
);
const description = (
<BodyText subdued className="m-t-1">Edit requisitions in the table before you confirm and save.</BodyText>
);
return (
<Page
header={
<>
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
{title('Requisition #1234567')}
</Stack.Item>
</Stack>
{description}
</>
}
>
<Divider spacing="4" />
Content of the page.
</Page>
);
}
render (<Example />)
const Example = () => {
const title = (text = 'Page Title') => (
<Stack alignItems="center" spacing={1}>
<Headline size="large" className="m-b-0">{text}</Headline>
</Stack>
);
const description = (
<Grid columns={3} className="m-t-3">
<Grid.Column>
<Eyebrow >Category</Eyebrow>
<Headline className='m-t-half m-b-0 t-truncate' size='small'>Referrals – Widgets</Headline>
</Grid.Column>
<Grid.Column>
<Eyebrow>Business Unit</Eyebrow>
<Headline className='m-t-half m-b-0 t-truncate' size='small'>Change-Out</Headline>
</Grid.Column>
<Grid.Column>
<Eyebrow>Advertised Number</Eyebrow>
<Headline className='m-t-half m-b-0 t-truncate' size='small'>(123) 456–7890</Headline>
</Grid.Column>
</Grid>
);
return (
<Page header={
<>
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
{title('Campaign Promo')}
</Stack.Item>
</Stack>
{description}
</>
}>
<Divider spacing="4" />
Content of the page.
</Page>
);
}
render (<Example />)
Tags provide useful metadata about the page. The relative importance of this data varies from page to page: the visual variations provided by Tags can help prioritize them in the header.
<Page header={
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
<Stack alignItems="center" spacing={1}>
<Headline size="large" className="m-b-0">
Batch #33: Plumbing Small Parts
</Headline>
<Tag color='info' compact>Scheduled</Tag>
</Stack>
</Stack.Item>
</Stack>
}>
<Divider spacing="4" />
Content of the page.
</Page>
<Page header={
<>
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
<Stack alignItems="center" spacing={1}>
<Headline size="large" className="m-b-0">
Old Equipment
</Headline>
</Stack>
</Stack.Item>
</Stack>
<div>
<TagGroup className="m-t-1 m-b-2">
<Tag color='success'>Active</Tag>
<Tag subtle>Unsold Estimates</Tag>
</TagGroup>
<BodyText subdued>A standard description for the entire page.</BodyText>
</div>
</>
}>
<Divider spacing="4" />
Content of the page.
</Page>
Actions
Actions can refer to many different activities a user can perform at the page level.
Types of Actions
The majority of page-level actions should be located within the header. Some actions are more appropriate for the page footer. Actions that only apply to a section of the page, or tied to a Table, should
- Add / Create
- Starting a Flow
- Edit page contents
- Switch to
- Secondary page actions, such as download, share, print, import, etc.
- A generic action that brings the user to a new page, Takeover, or Modal.
- Search and filtering
The majority of these actions are located at the bottom of the page.
- Save
- Canceling a process
- Previous / Next functionality
- Ending / Finalizing a flow
- Actions that are explicitly tied to a Form
Most actions can be represented through Buttons. These appear on the right side of the page. Except for the overflow menu, the most important actions are on the right-most side of the header. There should only be between 0-1 primary actions (visually a solid blue button) in the page header. Button actions should not be explicitly tied to a Table.
const Example = () => {
const title = (text = 'Page Title') => (
<Stack alignItems="center" spacing={1}>
<Headline size="large" className="m-b-0">{text}</Headline>
</Stack>
);
const actions = (
<ButtonGroup>
<Button small>View on Mobile</Button>
<Button small primary>Create Rule</Button>
</ButtonGroup>
);
return (
<Page header={
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
{title('Markups')}
</Stack.Item>
{actions}
</Stack>
}>
<Divider spacing="4" />
Content of the page.
</Page>
);
}
render (<Example />)
const Example = () => {
const title = (text = 'Page Title') => (
<Stack alignItems="center" spacing={1}>
<Headline size="large" className="m-b-0">{text}</Headline>
</Stack>
);
const actions = (
<ButtonGroup>
<Tooltip text='Print invoice'><Button small iconName='print' /></Tooltip>
<Tooltip text='Email invoice'><Button small iconName='email' /></Tooltip>
<Tooltip text='Edit invoice' direction='tl'><Button small iconName='edit' /></Tooltip>
</ButtonGroup>
);
return (
<Page header={
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
{title('Invoice #123456')}
</Stack.Item>
{actions}
</Stack>
}>
<Divider spacing="4" />
Content of the page.
</Page>
);
}
render (<Example />)
An overflow menu is placed on the right-most side of the header. This menu is useful when there are many page-level actions, the number of potential overflow actions varies by context, and there is a need to purposefully deemphasize actions (e.g. delete). The disadvantage of an overflow menu is the lack of discoverability of the actions and the extra clicks it takes to complete an action.
const Example = () => {
const title = (text = 'Page Title') => (
<Stack alignItems="center" spacing={1}>
<Headline size="large" className="m-b-0">{text}</Headline>
</Stack>
);
const [open, setOpen] = React.useState(false);
const actions = (
<ButtonGroup>
<Button small>Clone</Button>
<ActionMenu
open={open}
trigger={<Button small iconName='more_vert' onClick={() => setOpen(!open)} selected={open} />}
direction="bl"
>
<ActionMenu.Item>Edit</ActionMenu.Item>
<ActionMenu.Item>Delete</ActionMenu.Item>
</ActionMenu>
</ButtonGroup>
);
return (
<Page header={
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
{title('Campaign: New Style HVAC')}
</Stack.Item>
{actions}
</Stack>
}>
<Divider spacing="4" />
Content of the page.
</Page>
);
}
render (<Example />)
Search and Filtering
Page-level search and filtering are separated from Button-based actions. When they manipulate data on the page, they appear below the description area, starting on the left. When they manipulate data across many pages, they are to the left of other persistent Button-based actions.
const Example = () => {
const title = (text = 'Page Title') => (
<Stack alignItems="center" spacing={1}>
<Headline size="large" className="m-b-0">{text}</Headline>
</Stack>
);
const actions = <Button small>Clone</Button>;
return (
<Page header={
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
{title('Page Title')}
</Stack.Item>
{actions}
</Stack>
}>
<Divider spacing="4" />
<Form className="m-b-4">
<Form.Input placeholder='Search Content' icon="search" iconPosition="left" small style={{ width: '300px' }} />
</Form>
<BodyText>Rest of Page Content</BodyText>
</Page>
);
}
render (<Example />)
const Example = () => {
const title = (text = 'Page Title') => (
<Stack alignItems="center" spacing={1}>
<Headline size="large" className="m-b-0">{text}</Headline>
</Stack>
);
const actions = <Button small>Clone</Button>;
return (
<Page header={
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
{title('Page Title')}
</Stack.Item>
{actions}
</Stack>
}>
<Divider spacing="4" />
<Form className="m-b-4">
<Form.Group>
<Form.AnvilSelect label="Date Type" icon="search" iconPosition="left" trigger={{ size: 'small', placeholder: 'Completion Date' }} options={[]} />
<Form.AnvilSelect label="Date Range" icon="search" iconPosition="left" trigger={{ size: 'small', placeholder: 'Last 30 Days' }} options={[]} />
<Form.AnvilSelect label="Business Unit" shortLabel="funnel" trigger={{ size: 'small', placeholder: 'Business Unit' }} options={[]} />
</Form.Group>
</Form>
<BodyText>Rest of Page Content</BodyText>
</Page>
);
}
render (<Example />)
Navigation
Breadcrumbs
Breadcrumbs are an optional, supplemental navigational element that helps users understand where they are in the app. Breadcrumbs don’t show where a user has been, but shows the underlying site structure to a user. If a page can be accessed through multiple parent pages (i.e. poly-hierarchical), a single pathway should be designated. Assuming a page title exists, the page you are on should not be represented in the breadcrumbs.
const Example = () => {
const breadcrumbs = (
<Breadcrumb className="m-b-1">
<Breadcrumb.Link label="Root page" />
<Breadcrumb.Link label="Subpage" />
<Breadcrumb.Link label="Current page" />
</Breadcrumb>
);
const title = (text = 'Page Title') => <Headline size="large" className="m-b-0">{text}</Headline>;
const actions = <Button small>Action</Button>;
return (
<Page header={
<>
{breadcrumbs}
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
{title('Page Title')}
</Stack.Item>
{actions}
</Stack>
</>
}>
<Divider spacing="4" />
Content of the page.
</Page>
);
}
render (<Example />)
When to use Breadcrumbs
- If the page has a parent page (i.e. a page represented in the sidebar or global header)
- When a user has likely entered the page from outside the hierarchy
When not to use Breadcrumbs
- When there is no parent page to return to, such as if the page appears in a Sidebar.
- Within a flow or wizard. The Back Link can be used instead.
- To replace any other navigational structure.
Tertiary navigation via Tabs
A tertiary navigation (primary navigation being the global header, secondary being the sidebar) can be added to the Page header. This is represented through the Tab component. Tabs in the header can be divided into two purposes: to navigate to different pages related to the title, and to filter content on the page, such as a table.
Page-level Tabs
Use page-level Tabs when the header content above the Tabs does not change between tab switches. Types of content below the page typically varies between tabs.
const Example = () => {
const title = (text = 'Page Title') => <Headline size="large" className="m-b-0">{text}</Headline>;
return (
<Page header={
<>
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
{title('Sync Log')}
</Stack.Item>
</Stack>
<TabGroup className="m-t-3">
<Tab active>Services</Tab>
<Tab>Material</Tab>
<Tab>Equipment</Tab>
</TabGroup>
</>
}>
Content of the page.
</Page>
);
}
render (<Example />)
Notifications
Banner notifications should appear at the bottom of the page header, above the start of the page content.
const Example = () => {
const breadcrumbs = (
<Breadcrumb className="m-b-1">
<Breadcrumb.Link label="Root page" />
<Breadcrumb.Link label="Subpage" />
<Breadcrumb.Link label="Current page" />
</Breadcrumb>
);
const title = (text = 'Page Title') => (
<Stack alignItems="center" spacing={1}>
<Headline size="large" className="m-b-0">{text}</Headline>
<Tag>Tag</Tag>
</Stack>
);
const actions = (
<ButtonGroup>
<Button small>Action</Button>
<Button primary small>Primary Action</Button>
</ButtonGroup>
);
const description = (
<BodyText subdued className="m-t-1">A standard description for the entire page.</BodyText>
);
return (
<Page header={
<>
{breadcrumbs}
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
{title('Page Title')}
</Stack.Item>
{actions}
</Stack>
{description}
</>
}>
<Divider spacing="4" />
<Banner
icon
title="You are viewing an inactive item."
className="m-b-4"
/>
Content of the page.
</Page>
);
}
render (<Example />)
The header can be customized to be "boxed" from the content below it. It applies a full-width white background. This styling can be used when incorporating a custom fixed header style, or when the page is very dense in content. When using, it is recommended to consistently use it across the same section of the app. Caution should be used when using this style, as it is visually heavier, and is a little more difficult to technically maintain.
const Example = () => {
const breadcrumbs = (
<Breadcrumb className="m-b-1">
<Breadcrumb.Link label="Root page" />
<Breadcrumb.Link label="Subpage" />
<Breadcrumb.Link label="Current page" />
</Breadcrumb>
);
const title = (text = 'Page Title') => <Headline size="large" className="m-b-0">{text}</Headline>;
const actions = <Button primary small>Primary Action</Button>;
const description = <BodyText subdued className="m-t-1 m-b-3">A standard description for the entire page.</BodyText>;
return (
<Frame
header={<div style={{ height: 56 }}><div style={{backgroundColor: "#141414",height: 56,position: "fixed",top: 0,width: "100%",zIndex: 1}}/></div>}
>
<Page
sidebar={<Sidebar localStorageKey="page-headers__boxed-style-headers" />}
spacing='none'
maxWidth='wide'
header={
<div className="bg-white p-t-3" style={{ borderBottom: '1px solid #dfe0e1' }}>
<div className="m-x-auto p-x-5" style={{ maxWidth: '1280px' }}>
{breadcrumbs}
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
{title('Page Title')}
</Stack.Item>
{actions}
</Stack>
{description}
</div>
</div>
}
>
<div className="m-x-auto p-b-3 p-x-5 p-y-2" style={{ maxWidth: '1280px' }}>
Content of the page.
</div>
</Page>
</Frame>
);
}
render (<Example />)
Example with Tabs
The box styling can be paired well with Tabs. This use case works well when Tabs act as navigation between different types of page content.
<Page
spacing='none'
maxWidth='wide'
header={
<div className="bg-white p-t-3" style={{ borderBottom: '1px solid #dfe0e1' }}>
<div className="m-x-auto p-x-5" style={{ maxWidth: '1280px' }}>
<Stack alignItems="center" spacing={2} wrap='wrap'>
<Stack.Item fill>
<Headline size="large" className="m-b-0">Page Title</Headline>
</Stack.Item>
</Stack>
<TabGroup className="m-t-3" divider={false}>
<Tab active>Ready To Bill <Tag compact badge>127</Tag></Tab>
<Tab>Action Required <Tag compact badge color="critical">25</Tag></Tab>
<Tab>Insights</Tab>
<Tab>Settings</Tab>
</TabGroup>
</div>
</div>
}
>
<div className="m-x-auto p-b-3 p-x-5" style={{ maxWidth: '1280px' }}>
Content of the page.
</div>
</Page>
Best Practices
- Almost all pages should at least have a title.
- Page Headers should be paired with Page Layout.