> ## 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.

# AppHeader – Design

> App headers provide top-level navigation and identity for Carto experiences.

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>;
  }
};

export const CodePreviewPlaceholder = ({double, fullWidth}) => {
  const single = <div style={{
    width: fullWidth ? "100%" : "50%",
    borderRadius: "1rem",
    display: "flex",
    padding: "1rem",
    flexDirection: "column",
    gap: "0.5rem",
    height: "10rem",
    marginBlockEnd: "1rem"
  }} className="border-width-default border-color-subdued">
      <div className="bg-strong border-radius-large" style={{
    width: "100%",
    flexGrow: "1"
  }} />
      <div className="bg-strong border-radius-large" style={{
    width: "100%",
    flexGrow: "1"
  }} />
    </div>;
  return double ? <div style={{
    display: "flex",
    gap: "1rem"
  }}>
      {single}
      {single}
    </div> : single;
};

<LiveCode example="carto-app-header" screenshot fullWidth>
  ```tsx lines theme={null}
  import { AppHeader } from "@servicetitan/carto-react-kit";
  import {
    IconMinimize2,
    IconPanelLeft,
    IconPanelRight,
    IconX,
  } from "@servicetitan/carto-react-kit/icons";

  function App() {
    return (
      <div style={{ width: 900 }}>
        <AppHeader
          title="Title of conversation"
          leadingActions={[
            {
              id: "rail",
              icon: <IconPanelLeft />,
              label: "Toggle left panel",
              onPress: () => {},
            },
          ]}
          trailingActions={[
            {
              id: "artifact",
              icon: <IconPanelRight />,
              label: "Toggle right panel",
              onPress: () => {},
            },
            {
              id: "minimize",
              icon: <IconMinimize2 />,
              label: "Minimize",
              onPress: () => {},
            },
            {
              id: "close",
              icon: <IconX />,
              label: "Close",
              onPress: () => {},
            },
          ]}
        />
      </div>
    );
  }

  export default App;
  ```
</LiveCode>

## Options

AppHeader supports an optional centered title and two independent action clusters.

### Title

<LiveCode example="carto-app-header-title" screenshot fullWidth>
  ```tsx lines theme={null}
  import { AppHeader, CartoTheme } from "@servicetitan/carto-react-kit";
  import { IconPanelLeft, IconX } from "@servicetitan/carto-react-kit/icons";
  import "@servicetitan/carto-react-kit/styles.css";

  function App() {
    return (
      <CartoTheme>
        <div
          style={{
            width: 900,
            display: "flex",
            flexDirection: "column",
            gap: 16,
          }}
        >
          {/* With title */}
          <AppHeader
            title="Title of conversation"
            leadingActions={[
              {
                id: "rail",
                icon: <IconPanelLeft />,
                label: "Toggle left panel",
                onPress: () => {},
              },
            ]}
            trailingActions={[
              { id: "close", icon: <IconX />, label: "Close", onPress: () => {} },
            ]}
          />

          {/* Without title — action-only header */}
          <AppHeader
            leadingActions={[
              {
                id: "rail",
                icon: <IconPanelLeft />,
                label: "Toggle left panel",
                onPress: () => {},
              },
            ]}
            trailingActions={[
              { id: "close", icon: <IconX />, label: "Close", onPress: () => {} },
            ]}
          />
        </div>
      </CartoTheme>
    );
  }

  export default App;
  ```
</LiveCode>

`title` renders centered between the two action clusters with `<h1>` semantics — it's the primary heading of the view the shell frames. Omit it for an action-only header; the flexible middle still reserves the centered space, whether or not a title fills it.

### Actions

<LiveCode example="carto-app-header-actions" screenshot fullWidth>
  ```tsx lines theme={null}
  import { AppHeader, CartoTheme } from "@servicetitan/carto-react-kit";
  import {
    IconPanelLeft,
    IconPanelRight,
    IconX,
  } from "@servicetitan/carto-react-kit/icons";
  import "@servicetitan/carto-react-kit/styles.css";

  function App() {
    return (
      <CartoTheme>
        <div style={{ width: 900 }}>
          <AppHeader
            title="Title of conversation"
            leadingActions={[
              {
                id: "rail",
                icon: <IconPanelLeft />,
                label: "Toggle left panel",
                onPress: () => {},
              },
            ]}
            trailingActions={[
              {
                id: "artifact",
                icon: <IconPanelRight />,
                label: "Toggle right panel",
                onPress: () => {},
              },
              { id: "close", icon: <IconX />, label: "Close", onPress: () => {} },
            ]}
          />
        </div>
      </CartoTheme>
    );
  }

  export default App;
  ```
</LiveCode>

`leadingActions` render at the start (left in LTR) and `trailingActions` at the end. Reserve `leadingActions` for entry points into the surrounding layout — panel toggles, navigation — and `trailingActions` for surface-level controls, like minimize and close. Keep each action in the cluster it conventionally belongs to; don't relocate a panel toggle to the trailing side or a window control to the leading side. Consumers rely on this placement staying consistent across every Carto surface, so avoid rearranging it in your implementation.

## Behavior

The header adapts its title to available space and can present any action as disabled.

### Title Truncation

<LiveCode example="carto-app-header-title-truncation" screenshot fullWidth>
  ```tsx lines theme={null}
  import { AppHeader, CartoTheme } from "@servicetitan/carto-react-kit";
  import {
    IconMinimize2,
    IconPanelLeft,
    IconPanelRight,
    IconX,
  } from "@servicetitan/carto-react-kit/icons";
  import "@servicetitan/carto-react-kit/styles.css";

  function App() {
    return (
      <CartoTheme>
        <div style={{ width: 420 }}>
          <AppHeader
            title="A very long conversation title that truncates once it collides with the action clusters"
            leadingActions={[
              {
                id: "rail",
                icon: <IconPanelLeft />,
                label: "Toggle left panel",
                onPress: () => {},
              },
            ]}
            trailingActions={[
              {
                id: "artifact",
                icon: <IconPanelRight />,
                label: "Toggle right panel",
                onPress: () => {},
              },
              {
                id: "minimize",
                icon: <IconMinimize2 />,
                label: "Minimize",
                onPress: () => {},
              },
              { id: "close", icon: <IconX />, label: "Close", onPress: () => {} },
            ]}
          />
        </div>
      </CartoTheme>
    );
  }

  export default App;
  ```
</LiveCode>

The title is single-line and truncates with an ellipsis rather than wrapping or pushing the action clusters. Its center shifts toward the side with fewer actions, since it fills the flexible space between both clusters.

### Disabled Actions

<LiveCode example="carto-app-header-disabled-actions" screenshot fullWidth>
  ```tsx lines theme={null}
  import { AppHeader, CartoTheme } from "@servicetitan/carto-react-kit";
  import {
    IconMinimize2,
    IconPanelLeft,
    IconPanelRight,
    IconX,
  } from "@servicetitan/carto-react-kit/icons";
  import "@servicetitan/carto-react-kit/styles.css";

  function App() {
    return (
      <CartoTheme>
        <div style={{ width: 900 }}>
          <AppHeader
            title="Title of conversation"
            leadingActions={[
              {
                id: "rail",
                icon: <IconPanelLeft />,
                label: "Toggle left panel",
                onPress: () => {},
              },
            ]}
            trailingActions={[
              {
                id: "artifact",
                icon: <IconPanelRight />,
                label: "Toggle right panel",
                onPress: () => {},
                disabled: true,
              },
              {
                id: "minimize",
                icon: <IconMinimize2 />,
                label: "Minimize",
                onPress: () => {},
                disabled: true,
              },
              { id: "close", icon: <IconX />, label: "Close", onPress: () => {} },
            ]}
          />
        </div>
      </CartoTheme>
    );
  }

  export default App;
  ```
</LiveCode>

Set `disabled` on an individual action to dim it and block interaction while keeping it in place — useful when a panel toggle has nothing to reveal yet.

## Keyboard Interaction

Users can navigate and activate header actions using standard keyboard controls.

| Key           | Description                    |
| ------------- | ------------------------------ |
| Tab           | Moves focus to the next action |
| Enter / Space | Activates the focused action   |

## Patterns that use this component

* [Agentic Experience Shell](/docs/kits/ai-kit/patterns/agentic-experience-shell) — Identifies the experience and provides shell-level actions
