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

# Anvil2 Icons

> Shared Lucide and custom icon allowlist for AI Kit (web) and Anvil2 RN.

export const Anvil2IconLibrary = () => {
  const [icons, setIcons] = useState([]);
  const [status, setStatus] = useState("loading");
  const [searchQuery, setSearchQuery] = useState("");
  const [platform, setPlatform] = useState("web");
  const [toast, setToast] = useState({
    visible: false,
    message: "",
    path: ""
  });
  useEffect(() => {
    let cancelled = false;
    fetch("/images/anvil2-icons.json").then(response => {
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      return response.json();
    }).then(manifest => {
      if (cancelled) return;
      setIcons(Array.isArray(manifest.icons) ? manifest.icons : []);
      setStatus("ready");
    }).catch(() => {
      if (cancelled) return;
      setStatus("error");
    });
    return () => {
      cancelled = true;
    };
  }, []);
  const importPath = name => {
    const pkg = platform === "react-native" ? "@servicetitan/anvil2-icons/react-native" : "@servicetitan/anvil2-icons";
    return `import { ${name} } from "${pkg}";`;
  };
  const query = searchQuery.toLowerCase().replace(/[-_]/g, "");
  const visible = query ? icons.filter(icon => `${icon.name} ${icon.source} ${icon.lucide ?? ""}`.toLowerCase().replace(/[-_]/g, "").includes(query)) : icons;
  const handleClick = icon => {
    const path = importPath(icon.name);
    navigator.clipboard.writeText(path).then(() => {
      setToast({
        visible: true,
        message: "Import path copied",
        path
      });
      window.setTimeout(() => {
        setToast({
          visible: false,
          message: "",
          path: ""
        });
      }, 3000);
    }).catch(() => {
      setToast({
        visible: true,
        message: "Could not copy path",
        path: ""
      });
      window.setTimeout(() => {
        setToast({
          visible: false,
          message: "",
          path: ""
        });
      }, 3000);
    });
  };
  if (status === "loading") {
    return <div className="my-8 text-sm text-gray-500 dark:text-gray-400">
        Loading icon allowlist…
      </div>;
  }
  if (status === "error") {
    return <div role="alert" className="my-8 text-sm text-red-600 dark:text-red-400">
        Could not load the icon allowlist. Regenerate with{" "}
        <code>pnpm generate:anvil2-icons</code> and confirm{" "}
        <code>images/anvil2-icons.json</code> is present.
      </div>;
  }
  return <div className="my-8">
      <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-5">
        <div className="inline-flex rounded-lg border border-gray-300 dark:border-neutral-700 p-1 bg-white/50 dark:bg-neutral-800/50">
          <button type="button" onClick={() => setPlatform("web")} className={`px-3 py-1.5 text-sm rounded-md transition-colors ${platform === "web" ? "bg-gray-900 text-white dark:bg-white dark:text-gray-900" : "text-gray-700 dark:text-gray-300"}`}>
            Web
          </button>
          <button type="button" onClick={() => setPlatform("react-native")} className={`px-3 py-1.5 text-sm rounded-md transition-colors ${platform === "react-native" ? "bg-gray-900 text-white dark:bg-white dark:text-gray-900" : "text-gray-700 dark:text-gray-300"}`}>
            React Native
          </button>
        </div>
        <p className="text-xs text-gray-500 dark:text-gray-400">
          Click an icon to copy the{" "}
          {platform === "web" ? "web" : "React Native"} import.
        </p>
      </div>

      <div className="relative mb-5">
        <input type="text" placeholder={`Search ${icons.length} icons...`} value={searchQuery} onChange={event => setSearchQuery(event.target.value)} className="w-full px-4 py-3 border border-gray-300 dark:border-neutral-700 rounded-lg bg-white/50 dark:bg-neutral-800/50 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors" />
      </div>

      <div className="py-4 rounded-lg overflow-y-auto max-h-[calc(100vh-200px)] border border-gray-200 dark:border-neutral-700">
        <div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-4 lg:grid-cols-5 gap-4 px-4">
          {visible.map(icon => <div key={icon.name} className="flex flex-col items-center rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors text-center pb-3">
              <button type="button" onClick={() => handleClick(icon)} className="flex flex-col items-center justify-center w-full pt-4 pb-1 rounded-lg cursor-pointer" title={`Click to copy: ${importPath(icon.name)}`}>
                <span aria-hidden="true" className="w-8 h-8 text-gray-700 dark:text-gray-300 flex items-center justify-center [&>svg]:w-8 [&>svg]:h-8" dangerouslySetInnerHTML={{
    __html: icon.svg
  }} />
                <span className="mt-1 text-xs text-gray-600 dark:text-gray-400" style={{
    wordBreak: "break-word",
    overflowWrap: "break-word"
  }}>
                  {icon.name}
                </span>
              </button>
              {icon.source === "custom" || !icon.lucide ? <span className="text-[10px] uppercase tracking-wide text-gray-400 dark:text-gray-500">
                  Custom
                </span> : <a href={`https://lucide.dev/icons/${icon.lucide}`} target="_blank" rel="noreferrer" className="text-[10px] uppercase tracking-wide text-blue-600 dark:text-blue-400 hover:underline" title={`Open ${icon.lucide} on Lucide`}>
                  Lucide
                </a>}
            </div>)}
          {visible.length === 0 && <div className="col-span-full text-center py-12 text-gray-500">
              No icons found for "{searchQuery}"
            </div>}
        </div>
      </div>

      {toast.visible && <div className="fixed bottom-6 right-6 bg-gray-900 dark:bg-gray-800 text-white p-4 rounded-lg shadow-2xl transition-all duration-300 z-50" style={{
    opacity: toast.visible ? 1 : 0,
    transform: toast.visible ? "translateY(0)" : "translateY(10px)"
  }}>
          <p className="font-semibold text-sm">{toast.message}</p>
          {toast.path && <code className="text-xs text-gray-300 mt-1 block bg-black/30 p-1 rounded">
              {toast.path}
            </code>}
        </div>}
    </div>;
};

`@servicetitan/anvil2-icons` ships [Lucide](https://lucide.dev/) icons and custom ServiceTitan SVGs for AI Kit (web) and Anvil2 RN. Import from this package, not from [`lucide-react`](https://lucide.dev/) or [`lucide-react-native`](https://lucide.dev/). Core Anvil2 Material and ST SVG icons stay on [Icons](/docs/web/icons).

The grid lists every allowlisted export. Choose Web or React Native, then click a cell to copy the named import. See [Usage](/docs/kits/anvil2-icons/usage) for install, styles, and props.

<Anvil2IconLibrary />
