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

# Foundations

> Illustration language for Anvil2: style, color, form, perspective, line work, texture, and character.

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

Anvil2 illustrations use a shared illustration language: modern, clean, and friendly 2D art with consistent line weight and restrained detail. Apply these rules when creating or reviewing illustration work. Import published SVGs from the [Illustrations kit overview](/docs/kits/illustrations).

<Card title="Illustration language in Figma" icon="figma" href="https://www.figma.com/design/9AdQ0X9Wgx54HHFPM1xkOT/NEW-Illustration-Library?m=auto&node-id=2885-4404&t=95hr7d0TQUiSvJeG-1">
  Design source for style, color, form, perspective, line work, texture, and
  character.
</Card>

## Style

<div style={{ marginTop: "-15px" }}>*Modern, Clean, and Friendly*</div>

Illustrations are designed with clean lines, minimal details, and a balanced mix of muted and vibrant colors. The combination of squared and rounded shapes creates an approachable aesthetic. Consistent line weights enhance clarity, while subtle shadows add depth without disrupting the clean, cohesive style. The foundation is flat, with no skewing or angling of shapes, adhering to a polished 2D style.

## Color palette

<div style={{ marginTop: "-15px" }}>*Professional Tones*</div>

Each illustration employs a well-thought-out color palette specific to its application, ensuring a functional and visually engaging hierarchy. Primary tones are complemented by a secondary accent color to highlight details:

| App               | Color tones                      | Meaning                    |
| ----------------- | -------------------------------- | -------------------------- |
| Desktop and Field | Blue tones with red accents      | Professionalism and energy |
| Office            | Cool greens and neutral tones    | Calmness and balance       |
| Inventory         | Warm oranges with subtle accents | Vibrancy and focus         |

## Form and shapes

<div style={{ marginTop: "-15px" }}>
  *Geometric, Structured, and Flat 2D Foundation*
</div>

Elements are simplified and geometric. Circular shapes add softness, while rectangular forms provide stability.

* **Buildings and trucks:** Rectangular forms with rounded edges convey structure and approachability.
* **Details like tools:** Smooth, rounded shapes ensure recognizability without excessive detail.

The balance of shapes ensures versatility across themes, with forms deliberately designed to be intuitive and relatable. By avoiding overly complex contours, the illustrations remain clean and universally accessible.

###### Corner radiuses

<div style={{ marginTop: "6px" }}>
  To maintain consistency, corner radiuses should adhere to even numbers such as
  **2, 4, 6, or 8** pixels. This standardization ensures a cohesive appearance
  across all illustrations, balancing sharp and rounded edges for a professional
  yet approachable design.
</div>

These same published illustrations, reused across contexts, show how the form and shape rules hold up regardless of the scene.

<LiveCode example="illustrations-kit-form-language" screenshot fullWidth>
  ```tsx lines theme={null}
  import {
    AnvilProvider,
    Flex,
    usePrefersColorScheme,
  } from "@servicetitan/anvil2";
  import FirstUseLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-first-use-light.svg";
  import FirstUseDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-first-use-dark.svg";
  import ErrorStateLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-error-light.svg";
  import ErrorStateDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-error-dark.svg";
  import SuccessLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-success-light.svg";
  import SuccessDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-success-dark.svg";
  import UserClearedLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-user-cleared-light.svg";
  import UserClearedDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-user-cleared-dark.svg";
  import NoSearchResultsLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-no-search-results-light.svg";
  import NoSearchResultsDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-no-search-results-dark.svg";

  function App() {
    const { mode } = usePrefersColorScheme();

    const FirstUse = mode === "dark" ? FirstUseDark : FirstUseLight;
    const ErrorState = mode === "dark" ? ErrorStateDark : ErrorStateLight;
    const Success = mode === "dark" ? SuccessDark : SuccessLight;
    const UserCleared = mode === "dark" ? UserClearedDark : UserClearedLight;
    const NoSearchResults =
      mode === "dark" ? NoSearchResultsDark : NoSearchResultsLight;

    return (
      <AnvilProvider themeData={{ mode }}>
        <Flex gap={4} wrap="wrap">
          <FirstUse aria-hidden="true" width={167} height={140} />
          <ErrorState aria-hidden="true" width={167} height={140} />
          <Success aria-hidden="true" width={167} height={140} />
          <UserCleared aria-hidden="true" width={167} height={140} />
          <NoSearchResults aria-hidden="true" width={167} height={140} />
        </Flex>
      </AnvilProvider>
    );
  }

  export default App;
  ```
</LiveCode>

## Perspective

<div style={{ marginTop: "-15px" }}>
  *Flat with Subtle Depth*
</div>

Illustrations maintain a primarily flat appearance, with shadows and layering techniques that suggest depth. This subtle approach enhances scalability and ensures legibility across sizes without leaning into realism.

### Implying depth

<div style={{ marginTop: "-10px" }}>
  *Layering and Shadows*
</div>

Although rooted in 2D, the illustrations create depth by:

* **Layering and overlapping:** Objects in the foreground overlap smaller or farther elements in the background.
* **Color adjustments:** Layers with varying opacities (e.g., 50%) or slightly darker shades imply dimension.
* **Subtle shadows:** Strategically placed shadows enhance the perception of depth without adding clutter.

For example, the side of a building may use a slightly darker tone to suggest perspective while maintaining the 2D foundation. This consistent method of implying depth ensures the illustrations remain polished and visually engaging.

<LiveCode example="illustrations-kit-depth-language" screenshot fullWidth>
  ```tsx lines theme={null}
  import {
    AnvilProvider,
    Flex,
    usePrefersColorScheme,
  } from "@servicetitan/anvil2";
  import FirstUseLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-first-use-light.svg";
  import FirstUseDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-first-use-dark.svg";
  import SuccessLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-success-light.svg";
  import SuccessDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-success-dark.svg";
  import NoSearchResultsLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-no-search-results-light.svg";
  import NoSearchResultsDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-no-search-results-dark.svg";

  function App() {
    const { mode } = usePrefersColorScheme();

    const FirstUse = mode === "dark" ? FirstUseDark : FirstUseLight;
    const Success = mode === "dark" ? SuccessDark : SuccessLight;
    const NoSearchResults =
      mode === "dark" ? NoSearchResultsDark : NoSearchResultsLight;

    return (
      <AnvilProvider themeData={{ mode }}>
        <Flex gap={4} wrap="wrap">
          <FirstUse aria-hidden="true" width={167} height={140} />
          <Success aria-hidden="true" width={167} height={140} />
          <NoSearchResults aria-hidden="true" width={167} height={140} />
        </Flex>
      </AnvilProvider>
    );
  }

  export default App;
  ```
</LiveCode>

## Line work

<div style={{ marginTop: "-15px" }}>*Consistent 2pt Strokes*</div>

All strokes are uniformly set to **2pt**, providing subtle detail and a sense of familiarity. This consistent line weight ensures:

* **Clarity and polish:** Maintains visual legibility and refinement at both large and small scales.
* **Consistency:** Applied uniformly across all illustration themes and applications, creating a cohesive visual language.
* **Intentional weight variation:** If a stroke needs to be heavier than 2pt, it should be noticeably thicker, leaning towards a shape rather than a line to maintain visual balance and hierarchy.

This ensures that any variation in stroke weight feels deliberate and reinforces the overall style rather than creating unintended inconsistencies.

<LiveCode example="illustrations-kit-line-work" screenshot fullWidth>
  ```tsx lines theme={null}
  import type { ComponentType, SVGProps } from "react";
  import {
    AnvilProvider,
    Flex,
    usePrefersColorScheme,
  } from "@servicetitan/anvil2";
  import FirstUseLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-first-use-light.svg";
  import FirstUseDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-first-use-dark.svg";
  import UserClearedLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-user-cleared-light.svg";
  import UserClearedDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-user-cleared-dark.svg";
  import ErrorStateLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-error-light.svg";
  import ErrorStateDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-error-dark.svg";

  const ILLUSTRATION_WIDTH = 215;
  const ILLUSTRATION_HEIGHT = 180;
  const CALLOUT_COLOR = "var(--a2-color-green-500)";

  type CalloutPosition = { top: number; left: number };

  function Callout({ top, left }: CalloutPosition) {
    return (
      <Flex
        alignItems="center"
        gap={1}
        style={{ position: "absolute", top, left }}
      >
        <div
          style={{
            width: 6,
            height: 6,
            borderRadius: "50%",
            background: CALLOUT_COLOR,
          }}
        />
        <div style={{ width: 56, borderTop: `2px dashed ${CALLOUT_COLOR}` }} />
        <span style={{ color: CALLOUT_COLOR, fontStyle: "italic", fontSize: 12 }}>
          2pt
        </span>
      </Flex>
    );
  }

  function IllustrationWithCallouts({
    Illustration,
    callouts,
  }: {
    Illustration: ComponentType<SVGProps<SVGSVGElement>>;
    callouts: CalloutPosition[];
  }) {
    return (
      <div
        style={{
          position: "relative",
          width: ILLUSTRATION_WIDTH + 100,
          height: ILLUSTRATION_HEIGHT,
        }}
      >
        <Illustration
          aria-hidden="true"
          width={ILLUSTRATION_WIDTH}
          height={ILLUSTRATION_HEIGHT}
        />
        {callouts.map((callout) => (
          <Callout key={callout.top} {...callout} />
        ))}
      </div>
    );
  }

  function App() {
    const { mode } = usePrefersColorScheme();

    const FirstUse = mode === "dark" ? FirstUseDark : FirstUseLight;
    const UserCleared = mode === "dark" ? UserClearedDark : UserClearedLight;
    const ErrorState = mode === "dark" ? ErrorStateDark : ErrorStateLight;

    return (
      <AnvilProvider themeData={{ mode }}>
        <Flex gap={6} wrap="wrap">
          <IllustrationWithCallouts
            Illustration={FirstUse}
            callouts={[
              { top: 67, left: 181 },
              { top: 110, left: 172 },
            ]}
          />
          <IllustrationWithCallouts
            Illustration={UserCleared}
            callouts={[
              { top: 41, left: 147 },
              { top: 97, left: 94 },
              { top: 116, left: 177 },
            ]}
          />
          <IllustrationWithCallouts
            Illustration={ErrorState}
            callouts={[
              { top: 95, left: 93 },
              { top: 122, left: 149 },
            ]}
          />
        </Flex>
      </AnvilProvider>
    );
  }

  export default App;
  ```
</LiveCode>

## Details and textures

<div style={{ marginTop: "-15px" }}>
  *Minimal with Subtle Interest*
</div>

Textures and patterns are rarely used, appearing only as light shading or small dashes to suggest depth or surface material. This is achieved by layering colors and adjusting opacities (**50% opacity**), adding just enough visual interest without overcrowding. This minimalism keeps elements clean and highly visible at smaller scales.

* **Light shading:** Adding a sense of material depth.
* **Simple line work or strokes:** Used sparingly to imply otherwise complex details in a subtle and approachable manner.

This approach ensures elements remain clean, highly visible, and engaging without overwhelming the viewer. The minimal texture application is balanced with functional clarity, enhancing the illustration's overall effectiveness.

<LiveCode example="illustrations-kit-opacity-layering" screenshot fullWidth>
  ```tsx lines theme={null}
  import {
    AnvilProvider,
    Text,
    usePrefersColorScheme,
  } from "@servicetitan/anvil2";
  import SuccessLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-success-light.svg";
  import SuccessDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-success-dark.svg";

  const ILLUSTRATION_WIDTH = 167;
  const ILLUSTRATION_HEIGHT = 140;
  const SWATCH_WIDTH = 90;
  const SWATCH_HEIGHT = 34;
  const SWATCH_OFFSET = 24;
  const CALLOUT_COLOR = "var(--a2-color-green-500)";

  const SWATCH_COLORS = {
    light: { base: "#113064", overlay: "#B5DEFF" },
    dark: { base: "#D6E5FF", overlay: "#0265DC" },
  };

  function Dot({ top, left }: { top: number; left: number }) {
    return (
      <div
        style={{
          position: "absolute",
          top: top - 3,
          left: left - 3,
          width: 6,
          height: 6,
          borderRadius: "50%",
          background: CALLOUT_COLOR,
        }}
      />
    );
  }

  function App() {
    const { mode } = usePrefersColorScheme();

    const Success = mode === "dark" ? SuccessDark : SuccessLight;
    const swatchColors = SWATCH_COLORS[mode];

    const illustrationDot = { top: 66, left: 68 };
    const swatchDot = { top: 17, left: 57 };
    const bridgeTop = 0;

    return (
      <AnvilProvider themeData={{ mode }}>
        <div style={{ position: "relative", width: 460, height: 160 }}>
          <Success
            aria-hidden="true"
            width={ILLUSTRATION_WIDTH}
            height={ILLUSTRATION_HEIGHT}
            style={{ position: "absolute", top: 20, left: 0 }}
          />
          <div
            style={{
              position: "absolute",
              top: 20 + swatchDot.top - SWATCH_HEIGHT / 2,
              left: 175,
              width: SWATCH_WIDTH + SWATCH_OFFSET,
              height: SWATCH_HEIGHT,
            }}
          >
            <div
              style={{
                position: "absolute",
                top: 0,
                left: SWATCH_OFFSET,
                width: SWATCH_WIDTH,
                height: SWATCH_HEIGHT,
                background: swatchColors.base,
              }}
            />
            <div
              style={{
                position: "absolute",
                top: 0,
                left: 0,
                width: SWATCH_WIDTH,
                height: SWATCH_HEIGHT,
                background: swatchColors.overlay,
                opacity: 0.5,
              }}
            />
          </div>
          <Text
            subdued
            style={{
              position: "absolute",
              top: 20 + swatchDot.top + SWATCH_HEIGHT / 2 + 8,
              left: 175,
              width: 210,
              fontStyle: "italic",
              fontSize: 12,
            }}
          >
            Layering two colors with top layer at 50% opacity to create
            depth/shading
          </Text>
          <div
            style={{
              position: "absolute",
              top: 20 + bridgeTop,
              left: 20 + illustrationDot.left,
              width: 1,
              height: 20 + illustrationDot.top - (20 + bridgeTop),
              borderLeft: `1px dashed ${CALLOUT_COLOR}`,
            }}
          />
          <div
            style={{
              position: "absolute",
              top: 20 + bridgeTop,
              left: 20 + illustrationDot.left,
              width: 175 + swatchDot.left - (20 + illustrationDot.left),
              height: 1,
              borderTop: `1px dashed ${CALLOUT_COLOR}`,
            }}
          />
          <div
            style={{
              position: "absolute",
              top: 20 + bridgeTop,
              left: 175 + swatchDot.left,
              width: 1,
              height: 20 + swatchDot.top - (20 + bridgeTop),
              borderLeft: `1px dashed ${CALLOUT_COLOR}`,
            }}
          />
          <Dot top={20 + illustrationDot.top} left={20 + illustrationDot.left} />
          <Dot top={20 + swatchDot.top} left={175 + swatchDot.left} />
        </div>
      </AnvilProvider>
    );
  }

  export default App;
  ```
</LiveCode>

## Character and emotion

<div style={{ marginTop: "-15px" }}>*Personable and Professional*</div>

Characters and animals (when included) are designed with:

* **Gentle expressions:** Reflecting friendliness and trust.
* **Minimal facial details:** Simple eyes and neutral expressions align with the professional tone.

For example, the dog in the illustration is shown laying down with a calm posture and no distinct expression, adding a relatable and approachable touch while avoiding any unintended implications.

<LiveCode example="illustrations-kit-character-dog" screenshot fullWidth>
  ```tsx lines theme={null}
  import { AnvilProvider, usePrefersColorScheme } from "@servicetitan/anvil2";
  import NoSearchResultsLight from "@servicetitan/anvil2-illustrations/illustrations/empty-state-no-search-results-light.svg";
  import NoSearchResultsDark from "@servicetitan/anvil2-illustrations/illustrations/empty-state-no-search-results-dark.svg";

  function App() {
    const { mode } = usePrefersColorScheme();

    const NoSearchResults =
      mode === "dark" ? NoSearchResultsDark : NoSearchResultsLight;

    return (
      <AnvilProvider themeData={{ mode }}>
        <NoSearchResults aria-hidden="true" width={167} height={140} />
      </AnvilProvider>
    );
  }

  export default App;
  ```
</LiveCode>
