> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Page – Code

> The foundation for creating page layouts in Anvil2.

export const LiveCode = ({children, customHeight, clickToLoad, example, fullWidth, fullHeight, hideCodeInLiveCode, screenshot, screenshotOnly, showCode: showCodeProp}) => {
  const SCREENSHOTS_BASE = "https://servicetitan.github.io/anvil2-docs-live-code/screenshots";
  const STACKBLITZ_BASE = "https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples";
  const [showCodeBlock, setShowCodeBlock] = useState(showCodeProp ?? false);
  const [isLocalOverride, setIsLocalOverride] = useState(false);
  useEffect(() => {
    const examplePath = `/images/live-code-screenshots-tmp/${example}.png`;
    fetch(examplePath, {
      method: "HEAD"
    }).then(r => {
      if (r.ok) setIsLocalOverride(true);
    }).catch(() => {});
  }, [example]);
  const screenshotBase = isLocalOverride ? "/images/live-code-screenshots-tmp" : SCREENSHOTS_BASE;
  if (screenshotOnly) {
    return <Frame className="flex flex-col">
        <div className="flex dark:hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#FFFFFF"
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>
        <div className="hidden dark:flex" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#141414"
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>
      </Frame>;
  }
  if (screenshot) {
    return <Frame className="flex flex-col -mb-2">
        <div className="flex dark:hidden bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>

        <div className="hidden dark:flex bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      background: "#141414",
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>

        <div className="flex justify-end items-center text-xs py-2 px-1 gap-4">
          {!showCodeProp ? <button className="inline-flex justify-end items-center text-gray-700 dark:text-gray-50 hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1 cursor-pointer" onClick={() => setShowCodeBlock(!showCodeBlock)} style={{
      appearance: "none"
    }}>
              <Icon icon="code" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
              <span>{showCodeBlock ? "Hide code" : "Show code"}</span>
            </button> : null}

          <a className="inline-flex justify-end items-center hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1" href={`${STACKBLITZ_BASE}/${example}?file=src/App.tsx`} target="_blank" rel="noreferrer">
            <Icon icon="bolt" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
            <span>StackBlitz demo</span>
          </a>
        </div>

        <div className="grid transition-[grid-template-rows] duration-300 ease-in-out overflow-auto overflow-y-hidden overflow-x-auto" style={showCodeBlock ? {
      gridTemplateRows: "1fr"
    } : {
      gridTemplateRows: "0fr"
    }}>
          <div style={{
      minHeight: 0,
      overflowX: "auto",
      overflowY: "hidden",
      marginBlockStart: "-1.25rem",
      marginBlockEnd: "-1.5rem"
    }}>
            {children}
          </div>
        </div>
      </Frame>;
  } else {
    return <div style={{
      display: "flex",
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px",
      resize: "vertical",
      overflow: "auto"
    }}>
        <iframe title={example} style={{
      flex: 1,
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px"
    }} src={`${STACKBLITZ_BASE}/${example}?embed=1&hideNavigation=1&hideExplorer=1&terminalHeight=0&file=src/App.tsx${clickToLoad ? "&ctl=1" : ""}${hideCodeInLiveCode ? "&view=preview" : ""}`} allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />
      </div>;
  }
};

<Tabs>
  <Tab title="Implementation">
    <LiveCode showCode example="page-playground-full-page" fullWidth screenshot>
      ```tsx lines expandable theme={null}
      import {
        Page,
        Text,
        SideNav,
        Link,
        Layout,
        Flex,
        Button,
      } from "@servicetitan/anvil2";
      import { useState } from "react";

      function App() {
        const [panelOpen, setPanelOpen] = useState(false);

        const breadcrumbs = [
          { href: "/", children: "Home" },
          { href: "/section", children: "Section" },
          { children: "Current Page" },
        ];

        const chips = [{ label: "Status" }, { label: "Draft" }];

        const actions = {
          primary: {
            label: "Primary Action",
            onClick: () => console.log("Primary clicked"),
          },
          secondary: [
            {
              label: "Secondary Action",
              onClick: () => console.log("Secondary clicked"),
            },
          ],
        };

        return (
          <div style={{ width: "100vw" }}>
            <Page>
              <Page.Sidebar>
                <Page.SidebarHeader>
                  <Text variant="headline" el="h2">
                    Sidebar Title
                  </Text>
                </Page.SidebarHeader>
                <SideNav>
                  <SideNav.Link id="01" href="#" active>
                    First Link
                  </SideNav.Link>
                  <SideNav.Link id="02" href="#">
                    Second Link
                  </SideNav.Link>
                  <SideNav.Link id="03" href="#">
                    Third Link
                  </SideNav.Link>
                </SideNav>
              </Page.Sidebar>

              <Page.Header
                title="Page Header"
                breadcrumbs={breadcrumbs}
                chips={chips}
                actions={actions}
                preferenceAction={{
                  "aria-label": "Open settings",
                  onClick: () => console.log("Settings clicked"),
                }}
                description={
                  <Text>
                    This demonstrates all header elements with{" "}
                    <Text inline>
                      <Link href="#" appearance="primary">
                        standard actions
                      </Link>
                    </Text>
                  </Text>
                }
              />

              <Page.Content style={{ height: "15rem" }}>
                <Layout>
                  <Layout.Item span={12}>
                    <Flex direction="column" gap="2">
                      <Text variant="headline" el="h1" size="large">
                        Page Content
                      </Text>
                      <Text>
                        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
                        do eiusmod tempor incididunt ut labore et dolore magna aliqua.
                      </Text>
                      <Button onClick={() => setPanelOpen((p) => !p)}>
                        Toggle Panel Open
                      </Button>
                    </Flex>
                  </Layout.Item>
                </Layout>
              </Page.Content>
              <Page.Panel open={panelOpen}>
                <Flex direction="column" gap="2">
                  <Text variant="headline" el="h2">
                    Page Panel
                  </Text>
                  <Text>Lorem ipsum dolor sit amet.</Text>
                </Flex>
              </Page.Panel>
              <Page.Footer>
                <Text>Page Footer</Text>
              </Page.Footer>
            </Page>
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    ## Common Examples

    ```tsx theme={null}
    import { Page, SideNav, Text } from "@servicetitan/anvil2";

    const ExampleComponent = () => (
      <Page>
        <Page.Sidebar>
          <Page.SidebarHeader>
            <Text variant="headline" el="h2">
              Sidebar Header
            </Text>
          </Page.SidebarHeader>
          <SideNav>
            <SideNav.Link id="01" href="#">
              Nav Link
            </SideNav.Link>
          </SideNav>
        </Page.Sidebar>

        <Page.Header title="Page Title" />

        <Page.Content>
          <Text>Page content.</Text>
        </Page.Content>
      </Page>
    );
    ```

    ### Page components

    In Anvil2, the `Page` component is used to create full-page layouts, along with a few sub-components listed below. When implementing Page, ensure you are using the [method](https://docs.st.dev/docs/frontend/monolith-layout/#anvil2-layout) defined by ST Platform.

    * `Page`: the parent component to configure full-page layouts.
    * `Page.Sidebar`: a collapsible section on the left side of the page.
    * `Page.SidebarHeader`: used as the first child of Page.Sidebar to format a header using a [Text](/docs/web/components/text/code/) component.
    * `Page.Header`: top section of the page where page actions and title go.
    * `Page.Content`: wraps the primary content of the page, and renders as an HTML `section` element. It should be used with a [`Layout`](/docs/web/components/layout/design) component to ensure a consistent and structured design with `Page.Header`.
    * `Page.Panel`: an optional section that takes up the right side of the page and can be expanded or collapsed programmatically. Panels render as HTML `aside` elements.
    * `Page.Footer`: an optional footer section within the page content area. Renders as an HTML `footer` element and supports [layout utility props](/docs/web/utilities/layout-props).

    ### Adding side bars to the page

    The `Page.Sidebar` component is usually the first child of a `Page`, and adds a collapsible section to the left of the rest of the page content.

    The `Page.Sidebar` should generally have a `Page.SidebarHeader` and `SideNav` as its `children` to create a consistent, responsive, and user-friendly in-page navigation experience.

    The sidebar can be omitted if the current page does not require a secondary navigation.

    ```tsx theme={null}
    <Page>
      <Page.Sidebar>
        <Page.SidebarHeader>
          <Text variant="headline" el="h2">
            Sidebar Header
          </Text>
        </Page.SidebarHeader>
        <SideNav>{/* ... side nav links */}</SideNav>
      </Page.Sidebar>
      <Page.Content>{/* ... page content */}</Page.Content>
    </Page>
    ```

    #### Sidebar collapsed state in local storage

    In order to maintain the collapsed state of the page sidebar after refreshing or navigating to another page, a local storage value is saved to the `"sidebar-collapsed"` key. The key name can be overridden using the `localStorageKey` prop.

    ### Adding the header to the page

    The `Page.Header` component should go right above the `Page.Content` section to ensure proper placement. It houses the page title, breadcrumb navigation, page classifications via chips, page actions, page description, and the page preference/setting action trigger.

    The `Page.header` is responsive in nature and has the `Layout` component built into it. It accepts the same properties you would set on your `Page.Content` layout via its own `layout` property, and it is recommended that you keep these values in sync to ensure constraints for the sections remain the same.

    The title in the `Page.Header` will always be an `h1` to adhere to SEO and accessibility best practices.

    While the page description is a `ReactNode`, it is recommended to keep the descriptions simple by using `Text` and `Link` components. If a page action needs to be made for the page, use the actions property and not the description area.

    ```tsx theme={null}
    <Page>
      <Page.Sidebar>{/* ... sidebar content */}</Page.Sidebar>

      <Page.Header
        title="Title"
        breadcrumbs={breadcrumbs}
        chips={chips}
        actions={actions}
        preferenceAction={{
          "aria-label": "Open settings",
          onClick: () => {},
        }}
        description={
          <Text>
            This page demonstrates all header elements with
            <Text inline>
              <Link href="#" appearance="primary">
                standard actions
              </Link>
            </Text>
          </Text>
        }
      />

      <Page.Content>{/* ... page content */}</Page.Content>
    </Page>
    ```

    #### User-editing for the header title text

    To enable editing the page title, assign an object with an `onChange` handler to the `title` prop. This handler will be passed the updated title as a string, which can be used to manage the state for the title.

    For accessibility, it is recommended to announce that the editing of the page title was successful, which can be accomplished by using a `toast`.

    ```tsx theme={null}
    <Page>
      <Page.Header
        title={{
          text: title,
          onChange: (newTitle) => {
            setTitle(newTitle);
            toast.success({ title: "Page title edited" });
          }
        }}
      />
    </Page>
    ```

    ### Adding panels to the page

    The `Page.Panel` component can be used as a child of a `Page`, usually after the sidebar and content.

    Use the `open` prop to determine if the panel is displayed, and the `size` prop to change the width of the panel.

    By default, panels have padding around the inner content. To disable this, use the `noPadding` prop.

    ```tsx theme={null}
    <Page>
      <Page.Sidebar>{/* ... sidebar content  */}</Page.Sidebar>
      <Page.Content>{/* ... page content */}</Page.Content>
      <Page.Panel open size="large">
        {/* ... panel content */}
      </Page.Panel>
    </Page>
    ```

    ### Adding a footer to the page

    The `Page.Footer` component renders a semantic `footer` element within the page content area. It can be placed as a child of `Page` after the content or panel to add supplemental information or secondary actions.

    ```tsx theme={null}
    <Page>
      <Page.Content>{/* ... page content */}</Page.Content>
      <Page.Footer>
        {/* ... footer content */}
      </Page.Footer>
    </Page>
    ```

    ## Best Practices

    ### Use with Layout, Grid, and Flex components

    The `Page` component is the first step in creating consistent and responsive page layouts using Anvil2. Three other component are available to configure dynamic layouts on a page:

    * [Layout](/docs/web/components/layout/code): define content max-width and column structure (usually the direct descendent of `Page.Content`) that matches `Page.Header`.
    * [Grid](/docs/web/components/grid/code): easily use [CSS Grid](https://css-tricks.com/snippets/css/complete-guide-grid/) to create dynamic content areas.
    * [Flex](/docs/web/components/flex/code): create [CSS Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) containers for simple to moderate content layout.

    ### Handling horizontal scrolling

    The Page component does not allow horizontal scrolling at the page level by default. This prevents usability and accessibility issues, as users expect vertical scrolling and horizontal scrolling disrupts this mental model.

    #### Migrating legacy pages

    When migrating legacy pages that aren't responsive to the A2 Page layout, the recommended approach is to make the page responsive so it works well on all screen sizes. However, if making the page fully responsive is not possible, you can add horizontal scroll handling at the component level for wide content. Wrap specific components that need more horizontal space in a container with `overflow: auto`:

    ```tsx theme={null}
    <div style={{ overflowX: "auto" }}>
      {/* Wide content that needs horizontal scrolling */}
    </div>
    ```

    This approach ensures that page headers, body text, and navigation remain accessible without horizontal scrolling, while allowing specific components to scroll independently when needed. Some components like [Data Table](/docs/web/components/data-table/code) have horizontal scrolling built in.
  </Tab>

  <Tab title="Page Props">
    ```tsx theme={null}
    <Page>
      <Page.Sidebar>...</Page.Sidebar>
      <Page.Header title="Page Title" />
      <Page.Content>...</Page.Content>
    </Page>
    ```

    ## `Page` Props

    The `Page` component can accept any valid HTML `div` props.
  </Tab>

  <Tab title="Page.Sidebar Props">
    ```tsx theme={null}
    <Page.Sidebar localStorageKey="sidebar-collapse">
      <Page.SidebarHeader>Header</Page.SidebarHeader>
      <SideNav>...</SideNav>
    </Page.Sidebar>
    ```

    ## `Page.Sidebar` Props

    In addition to the props listed below, the `Page.Sidebar` component can accept any valid HTML `div` props.

    <ParamField path="currentPageTitle" type="string">
      Title to display in the adaptive trigger button on mobile.
    </ParamField>

    <ParamField path="localStorageKey" type="string" default="sidebar-collapsed">
      See [Sidebar collapsed state in local
      storage](#sidebar-collapsed-state-in-local-storage).
    </ParamField>
  </Tab>

  <Tab title="Page.SidebarHeader Props">
    ```tsx theme={null}
    <Page.SidebarHeader>
      <Text variant="headline" el="h2">
        Header
      </Text>
    </Page.SidebarHeader>
    ```

    ## `Page.SidebarHeader` Props

    The `Page.SidebarHeader` component can accept any valid HTML `div` props.
  </Tab>

  <Tab title="Page.Header Props">
    ```tsx theme={null}
    <Page.Header
      title="Page Title"
      breadcrumbs={breadcrumbs}
      chips={chips}
      description="Page description"
      actions={{
        primary: { label: "Primary Action", onClick: () => {} },
        secondary: [{ label: "Secondary Action", onClick: () => {} }],
      }}
      preferenceAction={{
        "aria-label": "Open settings",
        onClick: () => {},
      }}
      layout={{ fluid: false, variant: "default" }}
    />
    ```

    ## `Page.Header` Props

    The `Page.Header` component can accept any valid HTML `header` props, as well as the following:

    <ParamField path="title" type="string | { text: string; onChange?: (title: string) => void | boolean; editButtonLabel?: string; editFieldLabel?: string; confirmButtonLabel?: string; cancelButtonLabel?: string; }" required>
      The page title.

      If an object with an `onChange` handler is passed instead of a string, an edit button will be rendered in the header. This `onChange` function is called when an edit on the title is confirmed, and can be cancelled by returning `false`.
    </ParamField>

    <ParamField path="actions" type="{ primary?: ButtonProps & { label: string; }; secondary?: ButtonProps & { label: string; }[]; }">
      If there are more than 3+ actions of any combination, the secondary actions
      combine into a single menu. Label is required for actions.
    </ParamField>

    <ParamField path="breadcrumbs" type="BreadcrumbsLinkProps[]" />

    <ParamField path="chips" type="ChipProps[]" />

    <ParamField path="description" type="ReactNode" />

    <ParamField path="layout" type="LayoutProps" />

    <ParamField path="preferenceAction" type="(ButtonProps | ButtonLinkProps) & { aria-label: string }">
      Renders a preference/settings button in the header.

      This will only ever be an icon-only button or link, so `aria-label` is required.
    </ParamField>
  </Tab>

  <Tab title="Page.Content Props">
    ```tsx theme={null}
    <Page.Content>
      <Layout>
        <Layout.Item span={12}>Content</Layout.Item>
      </Layout>
    </Page.Content>
    ```

    ## `Page.Content` Props

    The `Page.Content` component can accept any valid HTML `section` props.
  </Tab>

  <Tab title="Page.Panel Props">
    ```tsx theme={null}
    <Page.Panel open={true} size="medium" noPadding={false}>
      Panel content
    </Page.Panel>
    ```

    ## `Page.Panel` Props

    The `Page.Panel` component can accept any valid HTML `aside` props, as well as the following:

    <ParamField path="noPadding" type="boolean" />

    <ParamField path="open" type="boolean" />

    <ParamField path="size" type={`"small" | "medium" | "large"`} default="medium" />
  </Tab>

  <Tab title="Page.Footer Props">
    ```tsx theme={null}
    <Page.Footer>
      Footer content
    </Page.Footer>
    ```

    ## `Page.Footer` Props

    The `Page.Footer` component can accept any valid HTML `footer` props and [layout utility props](/docs/web/utilities/layout-props).
  </Tab>
</Tabs>
