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

# Stepper Base – Code

> Stepper Base shows progress through a sequence when the product owns the current step and each step's outcome.

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="stepper-base-playground" fullWidth screenshot>
      ```tsx lines expandable theme={null}
      import { StepperBase } from "@servicetitan/anvil2";

      function App() {
        return (
          <div style={{ minWidth: "55rem" }}>
            <StepperBase index={2}>
              <StepperBase.List>
                <StepperBase.Step state="complete">Approved</StepperBase.Step>
                <StepperBase.Step state="complete">Reconciled</StepperBase.Step>
                <StepperBase.Step state="complete">
                  Approved & Reconciled
                </StepperBase.Step>
                <StepperBase.Step>Scheduled</StepperBase.Step>
              </StepperBase.List>
            </StepperBase>
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    The `StepperBase` provides a controllable stepper primitive. The caller sets the current step and each step's outcome. Use [`Stepper`](/docs/web/components/stepper/code) for the linear wizard.

    `StepperBase` uses the same visual tokens as [`Stepper`](/docs/web/components/stepper/tokens).

    ## Common Examples

    ```tsx theme={null}
    import { StepperBase } from "@servicetitan/anvil2";

    function ExampleComponent() {
      return (
        <StepperBase index={2}>
          <StepperBase.List>
            <StepperBase.Step state="complete">Approved</StepperBase.Step>
            <StepperBase.Step state="complete">Reconciled</StepperBase.Step>
            <StepperBase.Step state="complete">
              Approved & Reconciled
            </StepperBase.Step>
            <StepperBase.Step>Scheduled</StepperBase.Step>
          </StepperBase.List>
        </StepperBase>
      );
    }
    ```

    ### Current and complete together

    Completeness is the step `state`, not "this index is behind current." A step can be current and `complete` at the same time.

    <LiveCode showCode example="stepper-base-playground" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { StepperBase } from "@servicetitan/anvil2";

      function App() {
        return (
          <div style={{ minWidth: "55rem" }}>
            <StepperBase index={2}>
              <StepperBase.List>
                <StepperBase.Step state="complete">Approved</StepperBase.Step>
                <StepperBase.Step state="complete">Reconciled</StepperBase.Step>
                <StepperBase.Step state="complete">
                  Approved & Reconciled
                </StepperBase.Step>
                <StepperBase.Step>Scheduled</StepperBase.Step>
              </StepperBase.List>
            </StepperBase>
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Step outcomes

    Set `state` on each `StepperBase.Step` to `"not started"`, `"started"`, or `"complete"`. The default is `"not started"`. Outcome does not change when `index` changes.

    <LiveCode showCode example="stepper-base-states" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { Flex, StepperBase } from "@servicetitan/anvil2";

      function App() {
        return (
          <Flex direction="column" gap="6" style={{ minWidth: "55rem" }}>
            <StepperBase index={0}>
              <StepperBase.List>
                <StepperBase.Step>Not started</StepperBase.Step>
                <StepperBase.Step>Not started</StepperBase.Step>
                <StepperBase.Step>Not started</StepperBase.Step>
              </StepperBase.List>
            </StepperBase>
            <StepperBase index={1}>
              <StepperBase.List>
                <StepperBase.Step state="complete">Complete</StepperBase.Step>
                <StepperBase.Step state="started">Started</StepperBase.Step>
                <StepperBase.Step>Not started</StepperBase.Step>
              </StepperBase.List>
            </StepperBase>
            <StepperBase index={1}>
              <StepperBase.List>
                <StepperBase.Step state="complete">Complete</StepperBase.Step>
                <StepperBase.Step state="complete">Complete</StepperBase.Step>
                <StepperBase.Step>Not started</StepperBase.Step>
              </StepperBase.List>
            </StepperBase>
          </Flex>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Adding panel content

    Content can be added with `StepperBase.Panel`. The `step` prop on a panel must match the `id` on its `StepperBase.Step`. This is the inverse of `Stepper`, where the step's `controls` prop names the panel.

    A step `id` is required only when a panel points at that step. Panel `id` is optional and is generated for `aria-controls` when omitted.

    <LiveCode showCode example="stepper-base-panel" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { StepperBase, Text } from "@servicetitan/anvil2";

      function App() {
        return (
          <div style={{ minWidth: "55rem" }}>
            <StepperBase defaultIndex={0}>
              <StepperBase.List>
                <StepperBase.Step id="account" state="started">
                  Account
                </StepperBase.Step>
                <StepperBase.Step id="profile">Profile</StepperBase.Step>
              </StepperBase.List>
              <StepperBase.Panel step="account">
                <Text>Account details</Text>
              </StepperBase.Panel>
              <StepperBase.Panel step="profile">
                <Text>Profile details</Text>
              </StepperBase.Panel>
            </StepperBase>
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Interactive navigation

    The track is display-only by default. Set `interactive` to let click and keyboard change the current step. Pair a controlled `index` with `onIndexChange`. `disabled` on a step blocks activation from the track. Step `onClick` notifies only and does not replace `onIndexChange`.

    <LiveCode showCode example="stepper-base-interactive" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { useState } from "react";
      import { StepperBase } from "@servicetitan/anvil2";

      function App() {
        const [index, setIndex] = useState(0);

        return (
          <div style={{ minWidth: "55rem" }}>
            <StepperBase interactive index={index} onIndexChange={setIndex}>
              <StepperBase.List>
                <StepperBase.Step id="one" state={index > 0 ? "complete" : "started"}>
                  Account
                </StepperBase.Step>
                <StepperBase.Step
                  id="two"
                  state={index === 1 ? "started" : "not started"}
                >
                  Profile
                </StepperBase.Step>
                <StepperBase.Step id="three" disabled>
                  Review
                </StepperBase.Step>
              </StepperBase.List>
              <StepperBase.Panel step="one">Account details</StepperBase.Panel>
              <StepperBase.Panel step="two">Profile details</StepperBase.Panel>
              <StepperBase.Panel step="three">Review details</StepperBase.Panel>
            </StepperBase>
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    <Note>
      Do not mix `Stepper` and `StepperBase` children in the same tree.
    </Note>

    ## React Accessibility

    * The list uses `role="tablist"`. Each step is a `role="tab"`. A visible panel uses `role="tabpanel"`
    * The current step has `aria-selected="true"`
    * When a panel is present, the step sets `aria-controls` to the panel id
    * Disabled steps use `aria-disabled` so `onClick` still fires
    * Steps are `tabIndex={-1}` unless `interactive` is true, the step is current, and the step is not disabled
    * When `interactive` is true, Arrow Left and Arrow Right move among steps that are not disabled. Enter and Space activate the focused step
  </Tab>

  <Tab title="StepperBase Props">
    ```tsx theme={null}
    <StepperBase
      defaultIndex={0}
      index={2}
      interactive={false}
      onIndexChange={(index) => console.log(index)}
    >
      <StepperBase.List>...</StepperBase.List>
      <StepperBase.Panel step="approved">...</StepperBase.Panel>
    </StepperBase>
    ```

    ## `StepperBase` Props

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

    <ParamField path="defaultIndex" type="number" default="0">
      Uncontrolled start. Ignored when `index` is set.
    </ParamField>

    <ParamField path="index" type="number">
      Controlled current step (0-based). Pair with `onIndexChange`.
    </ParamField>

    <ParamField path="interactive" type="boolean" default="false">
      When true, click and keyboard can change the current step.
    </ParamField>

    <ParamField path="onIndexChange" type="(index: number) => void">
      Fired when a step is activated.
    </ParamField>
  </Tab>

  <Tab title="StepperBase.List Props">
    ```tsx theme={null}
    <StepperBase.List>
      <StepperBase.Step>Approved</StepperBase.Step>
    </StepperBase.List>
    ```

    ## `StepperBase.List` Props

    The `StepperBase.List` component can accept any valid HTML `div` props. Each `StepperBase.Step` that renders inside the list receives an index in render order, including steps inside fragments or wrappers.
  </Tab>

  <Tab title="StepperBase.Step Props">
    ```tsx theme={null}
    <StepperBase.Step
      disabled={false}
      id="approved"
      state="complete"
      onClick={(e, index) => console.log(index)}
    >
      Approved
    </StepperBase.Step>
    ```

    ## `StepperBase.Step` Props

    In addition to the props listed below, the `StepperBase.Step` component can accept any valid HTML `button` props except `onClick` and `disabled`, which are replaced by the props below.

    <ParamField path="disabled" type="boolean" default="false">
      When true, this step cannot become current via click or keyboard.
    </ParamField>

    <ParamField path="id" type="string">
      Stable id. Required when a `StepperBase.Panel` points at this step.
    </ParamField>

    <ParamField path="onClick" type="(e: MouseEvent<HTMLButtonElement>, index?: number) => void">
      Notify only. Does not replace `onIndexChange`.
    </ParamField>

    <ParamField path="state" type={`"not started" | "started" | "complete"`} default="not started">
      Outcome of this step. Independent of whether it is current (`index`).
      Current plus any state is valid.
    </ParamField>
  </Tab>

  <Tab title="StepperBase.Panel Props">
    ```tsx theme={null}
    <StepperBase.Panel step="approved">Panel content</StepperBase.Panel>
    ```

    ## `StepperBase.Panel` Props

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

    <ParamField path="step" type="string" required>
      `id` of the controlling `StepperBase.Step`.
    </ParamField>

    <ParamField path="id" type="string">
      Panel element id. Generated when omitted. Used for `aria-controls`.
    </ParamField>
  </Tab>
</Tabs>
