Skip to content
lab components / Structure

Base Layout (New)

The Base Layout component establishes a standardized structure at the top of the UI, aiding user navigation, providing context, and offering quick access to essential actions.

This is a Lab component!

That means it doesn't satisfy our definition of done and may be changed or even deleted. For an exact status, please reach out to the Fancy team through the dev_fancy or ux_fancy channels.

import { BaseLayoutNew } from "@siteimprove/fancylab";

#Composition

The Base Layout is ideal for composing the following elements:


  1. #Horizontal Navigation

    Reserved for global actions and branding elements, maintaining consistent placement across the platform.


  2. #Side navigation

    The primary means for navigating the platform's core features and sections.


  3. Contains the following elements:

    • Page Title: Clearly identifies the current page or section, using the same label as the corresponding side navigation item for consistency. Keep titles concise and informative.
    • Breadcrumbs (Optional): Display the user's navigation path within the platform, enhancing context and discoverability. Recommended for most pages unless the navigation is shallow.
    • Utilities (Optional): Contextual actions or tools specific to the current page or section. Positioned based on their scope, with global utilities on the right and page-specific utilities on the left.
    • Filters (Required): Enable users to refine and explore data on the page, improving discoverability. Prioritize the most frequently used filters and ensure they adapt to different screen sizes.
    • Dashboard Picker (Optional): Allows users to personalize their view by selecting a preferred dashboard upon entering the platform.
    • Alerts (Optional, Use Sparingly): Display critical system-wide messages that persist above the content area. Use sparingly to avoid overwhelming users.

#Examples

#Header base

Features a persistent page title, serving as a constant reference point for users throughout their navigation. Ensure consistency by using the same label from the side navigation as the page title.

Page title

Page content 🦔
<BaseLayoutNew id="content" pageHeader={<PageHeader title="Page title" />}> <>Page content 🦔</> </BaseLayoutNew>

#Header with navigation

Includes breadcrumbs to provide users with a clear understanding of their navigation path and context. This is especially useful in more complex modules or sections of the platform.

Page title

Page content 🦔
<BaseLayoutNew id="content" pageHeader={ <PageHeader title="Page title" breadcrumbs={{ "aria-label": "Breadcrumbs", items: [ { title: "Level 1", url: "https://fancylib.netlify.app" }, { title: "Level 2", url: null }, ], }} /> } > <>Page content 🦔</> </BaseLayoutNew>

#Header with utilities

Presents utilities and actions for user interaction, positioned based on their scope: product-specific utilities on the left and global utilities on the right. Prioritize the most important actions for the current context.

  • Global utilities: Located on the far left, these are universal functions, such as export, search, settings, and similar features.
  • Product-specific utilities: Located on the far right, these are functionalities relevant to the current page or product users are interacting with.

Page title

Page content 🦔
<BaseLayoutNew id="content" pageHeader={ <PageHeader title="Page title" utilities={{ global: ( <> <Button variant="borderless" aria-label="Icon only button"> <Icon> <IconSearch /> </Icon> </Button> <Button variant="borderless" aria-label="Icon only button"> <Icon> <IconDownload /> </Icon> </Button> <Button variant="borderless" aria-label="Icon only button"> <Icon> <IconSettings /> </Icon> </Button> <Button variant="borderless" aria-label="Icon only button"> <Icon> <IconOptions /> </Icon> </Button> </> ), productSpecific: ( <> <Button variant="primary">Primary action</Button> <Button variant="secondary">Secondary action</Button> </> ), }} /> } > <>Page content 🦔</> </BaseLayoutNew>

#Header with filters

Features a filter bar to enable users to refine and explore data displayed on the page. This improves data discoverability and empowers users to focus on the most relevant information. Arrange filters by frequency of use, with the most common filters appearing first. Examples include:

  • Site picker: allow users to select a site
  • Group picker: allow users to filter based on pre-defined groups of pages.
  • Period picker: enable filtering by specific timeframes (e.g., date range, month, quarter).

Page title

Page content 🦔
const locale = useFormattingLanguage(); type SitesRequest = { query: string; pageNumber: number; pageSize: number; sortField: SortField<Site>; }; const [sitesRequest, setSitesRequest] = useState<SitesRequest>({ query: "", pageNumber: 1, pageSize: 20, sortField: { property: "isFavorite", direction: "asc" }, }); const { api: sitesApi } = useSitesDataAPI(); const sitesFetchDataFn = async (request: SitesRequest, signal?: AbortSignal) => { // Simulate a delay to display the loading state. await new Promise((resolve) => setTimeout(resolve, 2000)); return await sitesApi.getSites(request, signal); }; const { data: sitesData, loading: loadingSites, triggerRender: sitesTriggerRender, } = useData(sitesFetchDataFn, sitesRequest); const [selectedSite, setSelectedSite] = useState<Site>(); const loading = loadingSites && sitesData === null; // Period Picker const [periodPickerValue, setPeriodPickerValue] = useState<PeriodPickerValue | null>(null); // Activity Plans type ActivityPlan = { isFavorite: boolean; name: string; searchEngineVisibility: number; keywords: number; competitors: number; }; type ActivityPlansRequest = { query: string; page: number; pageSize: number; sortField: SortField<ActivityPlan>; }; const [activityPlansRequest, setActivityPlansRequest] = useState<ActivityPlansRequest>({ query: "", page: 1, pageSize: 20, sortField: { property: "isFavorite", direction: "asc" }, }); const { api: activityPlansApi } = useActivityPlansDataAPI(); const activityPlansFetchDataFn = async (request: ActivityPlansRequest, signal?: AbortSignal) => await activityPlansApi.getActivityPlans(request, signal); const { data: activityPlansData, loading: loadingActivityPlans, triggerRender: activityPlansTriggerRender, } = useData(activityPlansFetchDataFn, activityPlansRequest); const [selectedActivityPlan, setSelectedActivityPlan] = useState<ActivityPlan>(); return ( <BaseLayoutNew id="content" pageHeader={ <PageHeader title="Page title" utilities={{ global: ( <> <Button variant="borderless" aria-label="Icon only button"> <Icon> <IconSearch /> </Icon> </Button> <Button variant="borderless" aria-label="Icon only button"> <Icon> <IconDownload /> </Icon> </Button> <Button variant="borderless" aria-label="Icon only button"> <Icon> <IconSettings /> </Icon> </Button> <Button variant="borderless" aria-label="Icon only button"> <Icon> <IconOptions /> </Icon> </Button> </> ), productSpecific: ( <> <Button variant="primary">Primary action</Button> <Button variant="secondary">Secondary action</Button> </> ), }} pickers={{ loading: loading, items: [ sitesData && ( <SitePicker items={sitesData.items} sort={sitesRequest.sortField} setSort={(property, direction) => setSitesRequest((prev) => ({ ...prev, sortField: { property, direction: property === sitesRequest.sortField.property ? invertDirection(sitesRequest.sortField.direction) : direction, }, })) } search={{ query: sitesRequest.query, onSearch: (query) => setSitesRequest((prev) => ({ ...prev, query })), }} onLoadMore={() => setSitesRequest((prev) => ({ ...prev, pageSize: prev.pageSize + 20 })) } totalItems={sitesData.totalItems} selectedSite={selectedSite} onSelectedSite={setSelectedSite} onFavorite={async (site, isFavorite) => { await sitesApi.updateFavoriteSite(site, isFavorite); sitesTriggerRender(); }} loading={loadingSites} editSitesUrl="https://my2.siteimprove.com/Settings/Sites/v2" extraColumns={[colPages(), colVisits()]} /> ), <PeriodPicker key="period-picker" translations={{ nextMonthAriaLabel: "Next month", nextYearAriaLabel: "Next year", nextCenturyAriaLabel: "Next century", nextDecadeAriaLabel: "Next decade", prevMonthAriaLabel: "Previous month", prevYearAriaLabel: "Previous year", prevCenturyAriaLabel: "Previous century", prevDecadeAriaLabel: "Previous decade", }} periodButtonVariant="borderless" periodButtonSize="large" periodButtonStyle={{ minHeight: "initial" }} value={periodPickerValue} onChange={setPeriodPickerValue} />, activityPlansData && ( <PageHeaderPicker search={{ query: activityPlansRequest.query, onSearch: (query) => setActivityPlansRequest((prev) => ({ ...prev, query })), }} onLoadMore={() => setActivityPlansRequest((prev) => ({ ...prev, pageSize: prev.pageSize + 20 })) } itemsCount={activityPlansData.items.length} totalItems={activityPlansData.totalItems} selectedItem={selectedActivityPlan} popoverButtonIcon={<IconGoalTarget />} selectedItemStringify={(item) => item.name} loading={loadingActivityPlans} texts={{ buttonContentNoItemSelected: "Select an Activity Plan", searchPlaceholder: "Search for Activity Plans", showingXOfYItems: (showing, total) => `Showing ${toFormattedNumberString({ number: showing, locale, })} of ${toFormattedNumberString({ number: total, locale })} Activity Plans`, }} toolbarActions={ <Button variant="primary" href="https://fancy.siteimprove.com/"> Edit Activity Plans </Button> } contentItems={( firstFocusableRef, close, tableClassName, selectButtonClassName ) => ( <Table items={activityPlansData.items} columns={[ { header: { content: "Favourite", property: "isFavorite" }, render: (item) => ( <Starred isStarred={item.isFavorite} onChange={async (starred) => { await activityPlansApi.updateFavoriteActivityPlan(item, starred); activityPlansTriggerRender(); }} aria-label="Favorite item" /> ), }, { header: { content: "Activity Plan", property: "name" }, render: (item, cellPosition) => ( <Button variant="borderless" ref={cellPosition.rowNum === 0 ? firstFocusableRef : undefined} onClick={() => { setSelectedActivityPlan(item); close(); }} className={selectButtonClassName} > {item.name} </Button> ), options: { isKeyColumn: true, cellPadding: "none" }, }, { header: { content: "Search engine visibility", property: "searchEngineVisibility", }, render: (item) => ( <FormattedNumber number={item.searchEngineVisibility} format="number" /> ), options: { align: "right" }, }, { header: { content: "Keywords", property: "keywords" }, render: (item) => ( <FormattedNumber number={item.keywords} format="number" /> ), options: { align: "right" }, }, { header: { content: "Competitors", property: "competitors" }, render: (item) => ( <FormattedNumber number={item.competitors} format="number" /> ), options: { align: "right" }, }, ]} sort={activityPlansRequest.sortField} setSort={(property, direction) => setActivityPlansRequest((prev) => ({ ...prev, sortField: { property, direction: property === activityPlansRequest.sortField.property ? invertDirection(activityPlansRequest.sortField.direction) : direction, }, })) } loading={false} highlightRow={(item) => item.name === selectedActivityPlan?.name} className={tableClassName} /> )} /> ), ], }} /> } > <>Page content 🦔</> </BaseLayoutNew> );

#Header with alert

Displays critical, system-wide messages that persist over the content area. Use header alerts sparingly and only for truly urgent messages to avoid information overload.

Some error message with a Link

Page title

Page content 🦔
const locale = useFormattingLanguage(); // Period Picker const [periodPickerValue, setPeriodPickerValue] = useState<PeriodPickerValue | null>(null); const { ColorWhite } = useDesignToken(); return ( <BaseLayoutNew id="content" topHeaderArea={ <Message type="negative" style={{ borderRadius: "0" }}> <Paragraph> Some error message with a <Link href="https://fancylib.netlify.app">Link</Link> </Paragraph> </Message> } pageHeader={ <PageHeader title="Page title" breadcrumbs={{ "aria-label": "Breadcrumbs", items: [ { title: "Level 1", url: "https://fancylib.netlify.app" }, { title: "Level 2", url: null }, ], }} utilities={{ global: ( <> <Button variant="borderless" aria-label="Icon only button"> <Icon> <IconSearch /> </Icon> </Button> <Button variant="borderless" aria-label="Icon only button"> <Icon> <IconDownload /> </Icon> </Button> <Button variant="borderless" aria-label="Icon only button"> <Icon> <IconSettings /> </Icon> </Button> <Button variant="borderless" aria-label="Icon only button"> <Icon> <IconOptions /> </Icon> </Button> </> ), productSpecific: ( <> <Button variant="primary">Primary action</Button> <Button variant="secondary">Secondary action</Button> </> ), }} pickers={{ loading: false, items: [ <SitePicker key="site-picker" items={[]} sort={{ property: "isFavorite", direction: "asc" }} setSort={() => {}} search={{ query: "", onSearch: () => {} }} onLoadMore={() => {}} totalItems={0} selectedSite={undefined} onSelectedSite={() => {}} onFavorite={() => {}} editSitesUrl="https://my2.siteimprove.com/Settings/Sites/v2" />, <PageHeaderPicker key="group-picker" search={{ query: "", onSearch: () => {} }} onLoadMore={() => {}} itemsCount={0} totalItems={0} selectedItem={undefined} popoverButtonIcon={<IconGroup />} selectedItemStringify={() => "-"} texts={{ buttonContentNoItemSelected: "Select a group", searchPlaceholder: "Search for groups", showingXOfYItems: (showing, total) => `Showing ${toFormattedNumberString({ number: showing, locale, })} of ${toFormattedNumberString({ number: total, locale })} groups`, }} toolbarActions={ <Button variant="primary" href="https://fancy.siteimprove.com/"> Edit groups </Button> } contentItems={() => ( <EmptyState type="reassure" heading="No groups available" style={{ backgroundColor: ColorWhite }} /> )} />, <PeriodPicker key="period-picker" translations={{ nextMonthAriaLabel: "Next month", nextYearAriaLabel: "Next year", nextCenturyAriaLabel: "Next century", nextDecadeAriaLabel: "Next decade", prevMonthAriaLabel: "Previous month", prevYearAriaLabel: "Previous year", prevCenturyAriaLabel: "Previous century", prevDecadeAriaLabel: "Previous decade", }} periodButtonVariant="borderless" periodButtonSize="large" periodButtonStyle={{ minHeight: "initial" }} value={periodPickerValue} onChange={setPeriodPickerValue} />, <PageHeaderPicker key="filter-picker" search={{ query: "", onSearch: () => {} }} onLoadMore={() => {}} itemsCount={0} totalItems={0} selectedItem={undefined} popoverButtonIcon={<IconFunnel />} selectedItemStringify={() => "-"} texts={{ buttonContentNoItemSelected: "Select a filter", searchPlaceholder: "Search for filters", showingXOfYItems: (showing, total) => `Showing ${toFormattedNumberString({ number: showing, locale, })} of ${toFormattedNumberString({ number: total, locale })} filters`, }} toolbarActions={ <Button variant="primary" href="https://fancy.siteimprove.com/"> Edit filters </Button> } contentItems={() => ( <EmptyState type="reassure" heading="No filters available" style={{ backgroundColor: ColorWhite }} /> )} />, ], }} /> } > <>Page content 🦔</> </BaseLayoutNew> );

#Properties

Page title

Social Media that have referred visitors to your website. Expand the table for referring social media pages, trend graphs, and entry pages.
PropertyDescriptionDefinedValue
idRequired
stringID for the main section of base layout
childrenOptional
elementElements to populate the base layout
topHeaderAreaOptional
elementOptional top header area
pageHeaderOptional
elementOptional page header element
contentPaddingOptional
"medium" | "none"Padding for the content section

#Guidelines

#Do's

  • Focus on the most important actions for the current context.
  • Strictly adhere to Siteimprove's brand guidelines for colors, typography, and iconography.
  • Use ample white space to maintain a clean and uncluttered appearance.
  • Ensure a balanced distribution of elements throughout the header.
  • Reveal additional options or actions as needed, rather than displaying everything at once.

#Don'ts

  • Avoid overloading the header with too many features, which can distract users from core tasks.
  • Reserve alerts for truly critical, time-sensitive messages requiring immediate user attention.
  • Prioritize essential actions and consider moving less-used ones to dropdown menus or other locations.
  • Avoid hiding crucial actions behind unclear icons or gestures.

#Accessibility

For developers, ensure the header is accessible by adhering to the following best practices:
  • Breadcrumbs: Placed inside the <main> element. Make the breadcrumb container a <nav aria-label="Breadcrumbs">...</a>
  • Heading: Placed inside the <main> element. Make the heading an <h1> element.
  • Main content section: Use the <main> element for this (Note: A page must only have one <main> element). It must have id="content" and tabindex="-1" for the page's="Skip to main content" link to work.
  • Page toolbar: Placed inside the <main> element, This container should have role="group" and aria-label="Page toolbar"
  • Page filter: Placed inside the <main> element. This container should have role="group" and aria-label="Page filter"

Explore detailed guidelines for this component: Accessibility Specifications

#Writing

Page Titles:

  • Use the same label from the side navigation for consistency.
  • Keep them short, informative, and in sentence case (e.g., "Account Settings").

Button Labels:

  • Employ action verbs (e.g., "Create," "Edit," "Delete").
  • Consult the Word list for approved terminology.

Alert Messages:

  • Prioritize clarity and conciseness.
  • Focus on the essential information users need to know.