** for content that's tangentially related to the main content (like sidebars or pull quotes)
### Common Pitfalls
* Using generic **\** elements instead of semantic tags—class names are not a substitute for proper semantics
* Misusing semantic tags for layout purposes, which can confuse assistive technologies about the flow of information
***
## Text Style Variations
Directly Related WCAG Guidelines(s):
* [1.3.1 Info and Relationships (Level A)](https://www.w3.org/WAI/WCAG22/quickref/#info-and-relationships)
* [1.4.1 Use of Color (Level A)](https://www.w3.org/WAI/WCAG22/quickref/#use-of-color)
* [1.4.3 Contrast (Minimum) (Level AA)](https://www.w3.org/WAI/WCAG22/quickref/#contrast-minimum)
* [4.1.2 Name, Role, Value (Level A)](https://www.w3.org/WAI/WCAG22/quickref/#name-role-value)
| Anvil2 Responsibility | Implementor Responsibility |
| ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| Anvil2 provides a text component for consistent typography visuals but not styling utilities for style variations. | Implementors ensure they are using the correct html tags to denote their content within the text component. |
Text style variations help users understand the importance, emphasis, and context of content through both visual and programmatic means. Screen readers rely on semantic markup to announce text with appropriate emphasis, while visual users need clear visual cues to understand content hierarchy and meaning.
Proper use of semantic text elements ensures that all users can access the same information regardless of how they interact with content. This includes using appropriate emphasis for important information, maintaining visual hierarchy, and ensuring that text variations enhance rather than hinder comprehension.
Without proper text variation implementation, users may miss critical information, become confused about content importance, or fail to understand the intended meaning of emphasized text.
### Best Practices
* Use **\
** when text is important or urgent—like warnings, required field indicators, or critical information
* Use **\** when you want to draw attention without conveying importance—like product names, keywords, or UI elements
* Use **\** when you want to emphasize or stress specific words or phrases to change the meaning
* Use **\** when text has a different voice or mood—like foreign words, technical terms, book titles, or thoughts
### Common Pitfalls
* Using semantic tags for purely decorative purposes instead of conveying meaning
* Relying on visual styles alone instead of using appropriate semantic tags
# Overview
Source: https://anvil.servicetitan.com/docs/ai-and-anvil2
An overview of how Anvil2 incorporates AI
Anvil2 and AI can be divided into two high-level categories: applying AI components, patterns, and styles into ServiceTitan's user interfaces, and using Anvil2 in internal AI tooling.
## AI in the user interface
This section defines the visual language, interaction patterns, and behaviors that shape that representation, ensuring AI feels clear and consistent across the product.
### Atlas
The Atlas extended library represents the conversational workflows within ST. Anvil hosts and collaboratively maintains it as an extended library.
}
href="/docs/kits/atlas"
>
Components and utilities for building the Atlas chat experience.
### Embedded AI
Anvil's UI components include embedded options that signal to end users that AI has touched the component.
} href="/docs/web/patterns/embedded-ai">
Patterns for embeddeding AI into the UI.
## AI in internal tooling
How internal teams at ServiceTitan can leverage AI and the Anvil design system. Refer to the [AI Tooling and Anvil2](/docs/ai-tooling-and-anvil2) page for more information on how to integrate Anvil2 into AI tooling.
# AI Tooling and Anvil2
Source: https://anvil.servicetitan.com/docs/ai-tooling-and-anvil2
Anvil2 can be integrated into a variety of AI tooling to speed up workflows. This guide shows how to use the design system across these tools.
# Overview of Tooling
Anvil2 is set up to be consumable to other AI tooling. Anvil2's codebase is reachable as a [public NPM package](https://www.npmjs.com/package/@servicetitan/anvil2), and its documentation via an MCP server. The Anvil team is always available to provide additional help on the [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) Slack channel.
Below is a list of common tooling that utilizes Anvil2 in some way.
| Tool | Primary audience | Capabilities | Uses Anvil2's codebase? |
| ------------------------------------------------------------------------------------ | -------------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------ |
| [Figma Design](#figma-design) | Product Designers | Produce high-quality design artifacts. Minimal AI. | Indirectly |
| [Figma Dev Mode (Code Connect)](#figma-dev-mode-code-connect) | Engineers | Translate Figma designs into code and Figma Make that uses Anvil2. Enhances Figma MCP capabilities. | Yes |
| [Figma Make](#figma-make) | Product Managers and Product Designers | Prompt out prototype-grade coded UI examples using Anvil2. | Yes, if using Anvil's template |
| [AI-powered Code Editors](#with-ai-powered-code-editors) (Cursor, Claude Code, etc.) | Engineers, Product Designers | AI-assisted coding with Anvil2-specific rules and guidelines. | Yes, if prompted to |
| [Storybook](#with-storybook) | Engineers and Product Designers | Generate UIs and test components with Anvil2 in an interactive workshop environment. | Yes, if prompted to |
| [Claude Design](#with-claude-design) | Product Managers and Product Designers | Prompt out conceptual, prototype-grade coded UI examples. | No |
## With Figma
Figma is the tool of choice for many builders to create UI artifacts. [Figma Design](#figma-design) is the most common way product designers create UI artifacts. [Figma Dev Mode (Code Connect)](#figma-dev-mode-code-connect) allows builders to both implement Figma Design work and to generate code using Anvil2 + Figma Design. [Figma Make](#figma-make), their most recent tool, provides a way to prompt out prototype-grade code, used by many builders.
### Figma Design
| Primary audience | Capabilities | Uses Anvil2's codebase? |
| ----------------- | --------------------------------------------------------------------------------------------------------- | ----------------------- |
| Product Designers | Produce high-quality design artifacts for other AI tooling. Heavy usage of Anvil2, but minimal direct AI. | Indirectly |
Figma Design on its own utilizes Anvil2 significantly through its component library, but by itself does not provide additional AI capabilities. Anvil2, through Code Connect, allows engineers to translate Figma Design work into code that uses Anvil2.
#### Optimizing a Figma Design file for other AI tooling
A Figma design file can be consumed by other AI tooling. The best practices within a Figma file revolve around smoothing the translation.
* Avoid detaching Anvil2 components as much as possible. Anvil2 components use Code Connect to integrate the Figma asset with its code counterpart. Detaching diminishes this integration.
* Use Figma variables with styling whenever possible. Avoid using the A1 Figma styles.
* Use semantic variables (labeled as `Anvil2 / Semantic` in Figma) over primitive variables (labeled as `Anvil2 / Primitive` in Figma). This enables dark mode compatibility.
* Colors in particular should be defined using variables.
* Spacing values in general should use defined variables.
### Figma Dev Mode (Code Connect)
| Primary audience | Capabilities | Uses Anvil2's codebase? |
| ---------------- | --------------------------------------------------- | ----------------------- |
| Engineers | Translate Figma designs into code that uses Anvil2. | Yes |
Figma Dev Mode with Code Connect allows engineers to see the exact React code for any component directly in Figma, making handoff seamless and accurate.
```mermaid theme={null}
flowchart LR
A[Figma Design handoff] --> B[Figma Dev Mode]
C[Anvil codebase] --> D["Code Connect (maintained by Anvil team)"]
B -->|Figma MCP| E["Code environment (production, cursor, etc.)"]
D -->|Figma MCP| E
```
#### Code Connect using Anvil2
Code Connect is automatically set up for Anvil2. When viewing the frame of a Figma Design file, a consuming application can pull in the correct configuration of the component.
#### Code Connect using product-specific components
It is possible for product-specific components to also be paired with Code Connect. By creating your own consumable component in code, a designer can in turn create a Figma-equivalent asset, and then an engineer can pair both to Code Connect. This process is owned by individual product teams. More information on how to do this can be found in [Figma's documentation on Code Connect](https://help.figma.com/hc/en-us/articles/23920389749655-Code-Connect).
### Figma Make
| Primary audience | Capabilities | Uses Anvil2's codebase? |
| -------------------------------------- | ---------------------------------------------------------- | ------------------------------ |
| Product Managers and Product Designers | Prompt out prototype-grade coded UI examples using Anvil2. | Yes, if using Anvil's template |
Using the Anvil Figma Make Template generates real Anvil2 component code. While most use it for prototyping, all Figma Make output should be reviewed before production use.
Additional guidelines for builders on Figma Make can be found in [Confluence](https://servicetitan.atlassian.net/wiki/spaces/DES/pages/4018307106/Figma+Guidelines+for+Builders).
#### Combining Figma Make with Anvil2
When using [Anvil's official Figma Make template](https://www.figma.com/make/jN6MrHnzJtjFsWUIuH5nSW/-WIP--Anvil-Make-Template?t=Jynl768tQDfDFQmc-1), Anvil2 is already integrated into the code. If starting from a fresh file, consult the official [Figma Make documentation](https://help.figma.com/hc/en-us/articles/35946832653975-Use-your-design-system-package-in-Figma-Make) for guidance on how to include Anvil2 in your file (our package is called `@servicetitan/anvil2` and is a public package).
##### Anvil2 Documentation Connector
You can also [add an official Anvil2 connector](https://help.figma.com/hc/en-us/articles/36343926263703-Manage-connectors-in-Figma-Make) to your Figma Make file that ties into the Anvil2 documentation site. This allows you to get access to Anvil2-specific rules and guidelines, as well as the ability to generate code examples from the documentation. This is a useful addition when not using the official Anvil2 template.
#### Using guidelines
Guidelines are a critical step in ensuring Figma Make generates accurate UIs. Within a Figma Make file, guidelines can be found by clicking on the gear icon ('Make Settings') and going to 'Adjust guidelines'.
Anvil provides pre-made guidelines for Figma Make, and are provided in our starter template file.
#### Adding product requirements documents (PRDs)
Figma Make can intake in PRDs in a few different ways. Refer to the [Figma Make documentation](https://help.figma.com/hc/en-us/articles/35440096186007-Connect-external-tools-using-Figma-Make-connectors#h_01KAC4Y0E71YB3NMHJJ9HH4PZ8) on how to do this.
#### Basic file hygiene
Many Figma Make files are produced by Builders from many backgrounds. This section outlines some basic practices for maintaining a Figma Make file.
* File location
* Make files should follow the convention of your product area. Read more about the proper workspace structure in [Confluence](https://servicetitan.atlassian.net/wiki/spaces/DES/pages/4018307106/Figma+Guidelines+for+Builders).
* Make files should not live in a drafts directory. This makes them inaccessible to other Figma users, including as shared demos.
* Make files can co-exist with other Figma files (design, Figjam, etc.).
* You can move files after the fact ('Move file...' from the file name dropdown menu) if they are in the wrong location.
* Verify that [guidelines](#using-guidelines) in the file are accurate and up to date.
## With AI-powered Code Editors
| Primary audience | Capabilities | Uses Anvil2's codebase? |
| ------------------------------- | ------------------------------------------------------------- | ----------------------- |
| Engineers and Product Designers | AI-assisted coding with Anvil2-specific rules and guidelines. | Yes, if prompted to |
AI-powered Code Editors, such as Cursor and Claude Code, can leverage Anvil2-specific rules and guidelines to help write better code and component implementations. You can connect your code editor to [Anvil2's MCP server](#connecting-anvil2-documentation-to-other-tools) to get access to Anvil2-specific rules and guidelines.
#### Using AI-powered Code Editors as a prototyping tool
AI-powered Code Editors may be used in place of a tool like Figma Make to generate UI examples. It can be used in conjunction [with Storybook](#with-storybook), or a product environment provided by the product team.
##### Advantages of using AI-powered Code Editors over Figma Make
* Provides more powerful developer tooling than Figma Make.
* Can use many more models.
* Can more easily reference the codebase in prompts.
* Can be used in multiple frontend environments.
##### Disadvantages of using AI-powered Code Editors over Figma Make
* More difficult to share outside of a local environment.
* Can be more difficult to get started with.
## With Storybook
| Primary audience | Capabilities | Uses Anvil2's codebase? |
| ------------------------------- | ------------------------------------------------------------------------------------ | ----------------------- |
| Engineers and Product Designers | Generate UIs and test components with Anvil2 in an interactive workshop environment. | Yes, if prompted to |
This section refers to setting up your own Storybook. Anvil2 itself also uses Storybook, but that setup is not related to this section.
Storybook provides a basic frontend shell to build your own UIs using Anvil2. Its ease of use and integration with other tools makes it a popular choice for many builders. Individuals and product areas can spin up their own [Storybook](https://storybook.js.org/). Storybook is often paired with an [AI-powered coding tool](#with-ai-powered-code-editors) to generate the actual code.
Advantages of using Storybook:
* Can quickly set up a front-end environment with minimal setup or dependencies.
* Easy to use UI for navigating your own generative content.
* On its own, is a free tool to use.
* Can be hooked up to [Chromatic](https://www.chromatic.com/) for visual regression testing.
Disadvantages of using Storybook:
* It is more difficult to share outside of a local environment.
#### Visual Regression Testing
Storybook integrates with [Chromatic](https://www.chromatic.com/) for visual regression testing, ensuring components maintain their appearance across changes. Individual projects need to be manually set up to use Chromatic.
## With Claude Design
| Primary audience | Capabilities | Uses Anvil2's codebase? |
| -------------------------------------- | --------------------------------------------- | ----------------------- |
| Product Managers and Product Designers | Prompt out prototype-grade coded UI examples. | No |
Claude Design does not use the design system under the hood. We suggest it be used only for concept work at this time. If you intend to create within the guidelines of the design system, we suggest [Figma Make](#figma-make).
Claude Design should not be confused with Claude Code, which can use Anvil2. See [AI-powered Code Editors](#with-ai-powered-code-editors).
[Claude Design](https://claude.ai/design) allows builders to generate interactive prototypes and presentations using a rough representation of Anvil2. This tool is best used for quick concept work to get ideas across, relying on other tooling for higher fidelity work. Anvil provides a design system option, `Anvil2 Design System (Official • Web Product)`, to utilize.
Advantages of using Claude Design:
* Can quickly generate many ideas for a UI, and easily compare against them
* Asks the builder many useful questions when preparing a UI
* Easy to share across the ST org
* Can be sent to Claude Code for later refinement
Disadvantages of using Claude Design:
* Cannot directly use the design system
* Contains many opinionated, difficult to change instructions under the hood that deviate from ST guidance
* Cannot easily share outside of ServiceTitan
* Locked into Claude models, while other tools can utilize models such as Gemini or ChatGPT
## With other tooling
### Connecting Anvil2 documentation to other tools
Anvil2 provides a MCP server that can be used to connect to other tools. For some tools, such as Figma Make, a connector is provided to do this. For other tools, you can manually set up the connection by taking the MCP server URL and adding it to the tool's configuration. Most setups can be additionally guided through your preferred LLM chat assistant.
#### Mintlify Documentation
The Anvil2 documentation site is built on [Mintlify](https://www.mintlify.com/), which provides:
* AI-powered chat assistant for asking questions about the design system
* MCP (Model Context Protocol) server for integration with other AI tools
* Searchable, comprehensive documentation
# General prompting guidance
We recommend making sure some level of prompting guidance is applied to any AI tooling so that the AI is aware of the context of the project and the design system. Some specific tools mentioned above provide pre-configured prompting guidance and do not need to be manually configured.
##### Have the AI know which packages are available to use
```md Example guidelines theme={null}
**ALWAYS use components from @servicetitan/anvil2**
- Import components from `@servicetitan/anvil2` or its extensions:
- `@servicetitan/anvil2` (core components)
- Check `node_modules/@servicetitan/anvil2` to see what components are available
- Prefer Anvil2 components over any other UI library or custom implementations
- Use the Anvil2 design tokens, which can be imported from `@servicetitan/anvil2/tokens`
```
##### Have the AI know to use the correct styling
Without knowing the correct way to apply styling, AI tooling will frequently create its own system, or rely on an unsupported 3rd party library. This should be avoided.
```md Example guidelines theme={null}
**Don't use these design systems or frameworks:**
- ❌ **Tailwind CSS** - Do NOT use Tailwind utility classes or Tailwind components
- ❌ **shadcn/ui** - Do NOT use shadcn components
- ❌ **Custom UI components** - Do NOT create custom button, input, or other UI components when Anvil2 provides them
- ❌ **Other design systems** - Do NOT use Material UI, Ant Design, Chakra UI, or any other design system
**Styling Rules:**
- Use Anvil2's design tokens and styling system exclusively
- Do NOT invent colors, shadows, tokens, animations, or new UI elements
- Style aligned to Anvil2 design system only
- If you need styling, check Anvil2's tokens and theming system first
```
# Additional Support
Choosing the right combination of these tools depends on your role and workflow—designers and PMs may lean on Figma, while engineers often pair Code Connect with an AI code editor and Storybook. Whatever path you take, the Anvil team is here to help in [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) if you hit snags or want to extend Anvil2 into other tools.
# Code Contributions
Source: https://anvil.servicetitan.com/docs/contributing/code-contributions
# Design Contributions
Source: https://anvil.servicetitan.com/docs/contributing/design-contributions
# Getting Started
Source: https://anvil.servicetitan.com/docs/getting-started
A guide to starting with Anvil2 in the ServiceTitan product.
The steps to start developing using Anvil2 change depending on if you are working in a fully standalone project, the ServiceTitan main project, or the ServiceTitan [Startup](https://www.npmjs.com/package/@servicetitan/startup) package.
## First Steps
### Prerequisites
Before starting, you will need [Node](https://nodejs.org/en/learn/getting-started/how-to-install-nodejs) installed with version 22 or later. Additionally, a package manager such as `npm` or `pnpm` is required. In our examples, we use `npm`.\
Anvil2 has `react` and `react-dom` as peer dependencies. To use Anvil2, your project must use React 18 or React 19. If these dependencies are missing, install them using your preferred package manager before continuing:
```bash theme={null}
npm install react@18 react-dom@18
```
### Installation
With Node and a package manager installed, go to the root of your project. Install Anvil2 using your package manager.
```bash theme={null}
npm install --save @servicetitan/anvil2
```
#### Installation within the ServiceTitan App
The ServiceTitan app already has Anvil2 installed. Verify the current version by checking the list of dependencies in the [package.json](https://github.com/servicetitan/app/blob/master/Clients/Web/package.json).
#### Installation within a Microfrontend (MFE)
The `@servicetitan/web-components` package has Anvil2 installed starting in version `27.0.0`. If your MFE project has this installed, we recommend staying up-to-date with the latest version to be consistent with other MFEs.
### Adding the AnvilProvider
Many Anvil2 components require that the `AnvilProvider` component exists somewhere higher in the component tree. This is usually done at the highest level component, such as `App.tsx`.
```tsx theme={null}
import { AnvilProvider } from "@servicetitan/anvil2";
export const App = ({ children }) => (
{/* ...other providers */}
{children}
);
```
#### AnvilProvider in the ServiceTitan App and MFE
The ServiceTitan monolith app already has the `AnvilProvider` wrapping all React code, and most MFEs that are up-to-date with platform infrastructure do as well.
If you are working in these contexts, only worry about adding it if a component breaks and logs a warning that it is missing a provider.
## Usage
With the design system installed, Anvil2 is available for use in your React applications.
```tsx theme={null}
import { Button, TextField } from "@servicetitan/anvil2";
export const MyComponent = () => {
return (
<>
Click me!
>
);
};
```
## Further Documentation
There are a number of available resources to continue learning to use Anvil2 effectively:
* For assistance using specific components in the design system, check the [component documentation](/docs/web/components).
* For a deeper understanding of how to use Anvil2, use the [foundations documentation](/docs/web/foundations).
* For established patterns in Anvil2, use the [patterns documentation](/docs/web/patterns).
## Additional Resources
With Anvil2 installed in your project, there are a number of additional resources available:
* The [Github repository for Anvil2](https://github.com/servicetitan/hammer) provides further information on getting started.
* To submit a feature request, use our [Slack feature board](https://servicetitan.enterprise.slack.com/lists/T08QRUZ6W/F07EUDU0WS1). We check this as a team every week.
* For questions and immediate assistance, reach out in the [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) Slack channel.
# ServiceTitan Design System
Source: https://anvil.servicetitan.com/docs/index
Anvil2 empowers product teams to create great experiences for ServiceTitan software with inclusive tools, alignment, and processes.
## Core libraries
The core library for building ServiceTitan web experiences.
The icon library for ServiceTitan.
The design tokens for ServiceTitan.
***
## Extended libraries
}
href="/docs/kits/atlas"
>
Components and utilities for building the Atlas chat experience.
Components and utilities for building common use cases in the ServiceTitan app monolith and MFEs.
Coming soon
Themes and utilities for building data visualization experiences with amCharts 5.
***
## Connect with us
You can reach the Anvil team in the #ask-designsystem Slack channel.
Join the Anvil team for office hours on Mondays or Wednesdays to get hands-on help.
Ran into a bug in the code or design? We use Jira to track outstanding
issues with Anvil.
Have an idea for a component, pattern, or documentation? Fill out the Feature Request Form in Slack.
View what is coming up this year for the design system.
Interested in contributing to Anvil? Read our contribution guide to learn
more!
# Complex Reasoning with AI
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/ai-ux/complex-reasoning-with-ai
Atlas shows its reasoning and proposes a plan before acting on multi-step, consequential work — nothing runs until the human approves.
## When to Use
Use this pattern when work spans multiple steps, a wrong action is costly, and the human owns the outcome.
## When Not to Use
Avoid this pattern for trivial, low-stakes, easily-undone actions.
## UX Pattern
### Human-Initiated Interaction
#### 1. Atlas proposes an Action Plan
The plan shows the high-level steps Atlas intends to take, each with the reason behind it. Atlas revises the plan as needed, and the conversation doesn't progress until the human approves one.
#### 2. Atlas assembles content and reasons through judgment calls
As it assembles and reasons, Atlas shows what it's reading, checking, and assuming. It surfaces this as a one-line summary the human can expand for detail.
**Related:** Assemble and Reason steps from the 5 parts
**UX Surface:** Atlas thinking pattern
#### 3. The situation package appears when review needs it
Atlas surfaces the read-only situation package — the context and evidence its reasoning is based on — whenever the human needs to validate that reasoning against the underlying data.
**UX Surface:** Situation Package
#### 4. Atlas proposes recommendations
Each recommendation carries what Atlas recommends, why, and the evidence it used. It offers the actions the human can take.
**UX Surfaces:** Atlas in-line message, Suggested Changes pattern
#### 5. The human decides
The human acts on the recommendation, or asks questions and requests changes. Atlas revises and re-presents, and nothing runs until the human confirms.
**Related:** Decide step from the 5 parts
**UX Surfaces:** User message, Atlas in-line message, Suggested Changes pattern
#### 6. Modify in place
If the human chooses to modify, the content appears in edit mode in the right panel (for now), pre-filled with the selected changes. The human edits fields directly, then saves or cancels.
**UX Surface:** Suggested Changes pattern
#### 7. The human confirms and Atlas acts
Atlas performs the action. Every executed action and decision stays visible for the audit trail.
**Related:** Act step from the 5 parts
**UX Surfaces:** Atlas in-line message, What Atlas Did pattern
#### 8. Atlas proposes the next step
Atlas keeps the conversation moving by bringing the human directly to the next step, or by suggesting next-step options.
### Scheduled or Time-Based Trigger
#### 1. Atlas performs a skill on its own
Atlas triggers on a schedule or an event, without a human initiating it first. Scheduled triggers run at a fixed time — a morning brief at 6am daily, an AR staging scan at 10pm on Mondays. Event triggers fire when something happens — a payment fails, an estimate sits dormant for 48 hours, a job completes.
#### 2. Atlas assembles content and reasons through judgment calls
This step stays invisible in the moment because Atlas runs in the background. Its output becomes the guidance content surfaced in the next step.
**Related:** Assemble and Reason steps from the 5 parts
#### 3. Atlas surfaces guidance in the human's Workspace
When surfacing guidance is part of the skill, Atlas presents one or more recommendations in the human's Workspace. Each piece of guidance carries what Atlas recommends, why, the evidence it used, what it's unsure about, and whether it's one action or several sequenced steps. Atlas offers specific actions — accept, modify, defer, reject, escalate, or assign — using concrete action copy like “Approve and Send” rather than a generic “Accept.”
**UX Surfaces:** Workspace, recommendations pattern
#### 4. Some recommendations open a guided flow
For recommendations that need more detail — specific line-item changes, for example — the action brings the human into Atlas to walk through a flow. The human allows, denies, or modifies each item individually or in bulk.
**UX Surfaces:** Atlas UI shell, Suggested Changes pattern
#### 5. The human decides
The human accepts, modifies, defers, rejects, escalates, or assigns the recommendation, and can ask Atlas for more information before deciding.
**Related:** Decide step from the 5 parts
**UX Surfaces:** Workspace, recommendations pattern
#### 6. The human confirms and Atlas acts
Atlas performs the action — it saves the invoice, sends the note, creates the task. High-stakes actions, like customer-facing sends or escalations, sometimes include an explicit confirmation step before execution.
When the action launches inside an Atlas conversation, Atlas confirms what happened in-line. The What Atlas Did section is the person's audit of the act: what actually changed in ServiceTitan, on whose authority, and when. When the action launches a confirmation dialog instead, the recommendation card shows as a completed step once the human confirms.
**Related:** Act step from the 5 parts
**UX Surfaces:** Atlas in-line message, What Atlas Did, recommendations pattern
#### 7. Atlas remembers this interaction
Atlas creates a Decision Trace for every interaction, so the person can audit a past decision and so Atlas gets better over time. The domain team authors two things for every trace:
* **What carries forward** — what goes into Memory versus what stays in the record. Not every trace becomes a lesson: the domain team writes only governed, durable lessons to Memory.
* **At what scope** — customer-level (this customer always short-pays), tenant-level (shop policy), or platform-level. A lesson stays at the person's scope unless the domain team deliberately promotes it.
The Decision Trace stays available on any past guidance: what Atlas assembled, what it reasoned, what it recommended, what the person decided, and what actually happened. This is the causal trust layer in retrospect — the person can always ask why something happened.
**Related:** Remember step from the 5 parts
**UX Surfaces:** Decision Trace (surfaced only when the human asks, not revealed by default), What Atlas Did
## Do / Don't
**Do**
* State each step's concrete impact — the specific records, recipients, or amounts it touches — so a wrong step is easy to spot
* Let the human edit the plan — remove, reorder, or adjust steps — before running, not just approve or reject it whole
* Approve once at the plan level, then run the full sequence without re-asking about each step
* Stop and re-present the plan if a step fails or the approach changes mid-run — never improvise silently
* Let the human interrupt and redirect mid-run without losing completed work
**Don't**
* Ask for approval on a vague plan, like “update the relevant records” — the human should know exactly what they're approving
* Show one plan, get approval, then execute a different one
* Put trivial steps behind a full plan — it teaches people to approve without reading
* Make reviewing the plan cost more than doing the task itself
* Expose raw tool logs — summarize, and let the human expand for detail
# AI UX
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/ai-ux/overview
Guidance for designing AI-powered experiences in Carto.
# UX and the Five Parts of a Moment
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/ai-ux/ux-and-the-five-parts-of-a-moment
How the five parts of a moment — Assemble, Reason, Decide, Act, Remember — shape AI UX depending on what triggers the moment.
## What is a Moment?
A moment is a specific instance in a person's day when something happens that needs handling: a job to be done. A flow is a path through a system, but a moment is a thing that happens in the world and needs a response. The moment is the unit design centers on because it's the unit where the cognitive load question becomes answerable: in this moment, who's doing the assembling and reasoning — pulling together the scattered sources, working out the next move — the person, or something built to do it?
This matters because a role is a collection of recurring moments. Designing an AI role doesn't mean designing "an AR agent" — it means scoping one moment at a time: the clerk's twelve failures landing, then the next moment after that. The composition model is built for exactly this: each scoped moment becomes the Capability Plugin/Skill (Capability Plugins/Skills are technical pieces and instructions defined by the PM and engineers that enable Atlas to perform the work) that handles it, and the platform assembles the Role Plugin from all of them at runtime (runtime is the point when AI starts its processes for a specific Capability Plugin/Skill).
The five parts of a moment are constant — Assemble, Reason, Decide, Act, Remember — but the UX varies significantly depending on whether the moment was triggered by a domain event, a schedule, or a person initiating it. This view makes those differences explicit, part by part.
## 1. Assemble
**Agent's half — invisible to the person**
Atlas reads sources, reconciles what they say, and builds the Situation Package — the compilation of evidence Atlas references while reasoning through the Capability Plugin/Skill. This is backstage work — the person never sees the assembly directly. The assembled Situation Package is available for the person to review as part of the output guidance (the recommendation surface — a Workspace feed item, domain page alert, or chat response — where Reason's output becomes visible to the person).
| Trigger | What the Person Sees | UX Surfaces |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| Event | Nothing. Assembly happens at event time — payment failure, estimate expiry, job completion. By the time the person sees the output guidance, the assembly is already done. The output guidance carries a timestamp to show when it ran, signaling freshness. | Output guidance (Workspace feed item or Domain page Atlas alert) · Freshness timestamp on output guidance · Situation Package |
| Scheduled | Same as event — assembly runs at the scheduled time (morning brief, nightly AR scan). | Same as Event |
| Human | The Atlas thinking pattern. The person asks Atlas a question or triggers an Atlas action and waits in the conversation while Atlas assembles in real time. | Atlas thinking pattern (inside an Atlas conversation) · Situation Package |
### When It Breaks
A source is stale or two sources conflict, creating a risk. Atlas surfaces this in the output guidance rather than guessing — noting which data is missing, flagging conflicting signals, and letting the person know the recommendation is incomplete.
## 2. Reason
**Agent's half — the result becomes the output guidance**
Reason produces the output guidance content. Everything the person can read — recommendation, evidence, confidence, alternatives, and risks — is Reason's output made visible. The details of the output guidance show Reason's work: why this guidance was generated, what evidence it drew on, and what it's uncertain about.
| Trigger | What the Person Sees | UX Surfaces |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| Event | Reasoning ran at the time the event triggered. The output guidance is pre-populated and ready on page load. The description, recommendations, evidence, and risks reflect the moment the event fired, not when the person reads it. | Output guidance headline · Evidence · Recommendations · Risks |
| Scheduled | Same as Event. Reasoning ran at the scheduled time. Data in the card reflects when it ran, not when it's read. | Output guidance headline · Evidence · Recommendations · Risks |
| Human | Reasoning happens live in Atlas chat. Because the person initiated it, they're invested — this is the highest-scrutiny surface. They can ask follow-up questions ("why this one?") and Atlas explains its reasoning, sources, and uncertainty interactively. | Inline reasoning in chat · Situation Package · Conversational follow-up |
### When It Breaks
Confidence is low — not enough signal for a confident recommendation. Atlas surfaces this in the output guidance rather than overstating certainty: "I found three payment failures but can't classify the third — here are the two I can recommend, and here's the third for your review." Naming the gap gives the person something to act on; a silent gap doesn't.
## 3. Decide
**Person's half — always visible, always actionable**
The output guidance is the main UX artifact. It carries the recommendation, evidence, confidence, alternatives, and proposed next step. The design follows a trust ramp: default view is enough to act on quickly; progressive disclosure opens fuller evidence for verification. The goal is to make the right action the easy one.
| Trigger | What the Person Sees | UX Surfaces |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| Event | Output guidance either in the Workspace feed or on a domain page. Workspace feed item expands details; a recommendation card on a domain page can bring the user to more details or directly into an Atlas conversation. | Output guidance (Workspace feed item or Domain page Atlas alert) · Situation Package · Recommendations |
| Scheduled | Same as Event. | Output guidance (Workspace feed item or Domain page Atlas alert) · Situation Package · Recommendations |
| Human | Decision surfaces inline in Atlas chat — the approval appears in the conversation thread. The person can review reasoning in the same thread and ask follow-ups before approving. | Inline reasoning in chat · Situation Package · Recommendations · Conversational follow-up |
| Autonomous | If an Atlas action has graduated to an Automation (e.g. retry payments under \$200 automatically), Decide doesn't interrupt the person. The decision was made in advance by policy. A decision trace is still written, and the action surfaces in "What Atlas did" with full attribution. | What Atlas did · Decision trace logged |
### When It Breaks
The person rejects the recommendation. The card must capture the reason — not just "rejected" but enough signal to learn from. "Wrong" vs "right but not now" vs "right but never for this customer" — the system can only learn differences the card can capture.
## 4. Act
**Deterministic — gated, not improvised**
Accepting the guidance is not the same as executing the action. Two actions in the same moment can have different trust ceilings: a reversible internal write can graduate to autonomous; a customer-facing send stays behind explicit approval permanently.
| Trigger | What the Person Sees | UX Surfaces |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Event | Runs right after the person approves the action. High-consequence actions get an explicit confirm step before execution and surface in "What Atlas did" after completion. | Action confirmation toast · What Atlas did · Approval Gate with explicit confirm (high-risk) |
| Scheduled | Same as Event. High-consequence actions have an explicit confirm step during setup. | Action confirmation toast · What Atlas did · Approval Gate with explicit confirm (high-risk) |
| Human | Act runs in the same chat session. Atlas confirms what changed and can answer follow-up questions about the result in the same thread. The conversation becomes part of the evidence record — what was asked, what was approved, what happened. | Inline action result in chat · What Atlas did · Conversational follow-up |
| Autonomous | Runs automatically, without a preceding approval step. Only reversible, low-consequence actions graduate to this ceiling (e.g. retry payments under \$200). The action still surfaces in "What Atlas did" with full attribution and a decision trace. | What Atlas did · Decision trace logged · Automate Action follow-up |
### When It Breaks
Action fails — payment gateway times out, downstream system rejects the write. The UX surfaces what happened (not a silent failure), names the gap, and doesn't leave the person starting over. If a handoff to a human is needed, the Situation Package travels with it.
## 5. Remember
**Platform's job — written before the action fires**
Every time Atlas (or Atlas with a person's approval) is about to implement a decision that changes something in ServiceTitan — retry a payment, send a collections notice, reassign a technician — it records what it observed, what it concluded, and what was decided before executing. This record is the decision trace: an audit trail with reasoning attached. Remember doesn't vary by trigger — the same write-before-action step runs whether the moment started with an event, a schedule, a person, or a policy, which is why this section has no trigger table.
A decision trace captures the situation at the time of the decision (the customer's balance, how many days overdue, any promise to pay), what Atlas recommended and why, its confidence, and what the person chose to do — accept the recommendation, reject it, modify it, or override it — including what they changed it to and why. For example, if a manager overrides a suggested retry amount, Atlas adds that override to the decision trace as evidence of how the shop wants retries handled.
Atlas writes the decision trace automatically, just before the action fires. It's what makes Atlas improve over time and what a person can audit to understand a past decision. The decision trace is available on any past output guidance: what was assembled, reasoned, recommended, decided, and what happened as a result.
UX patterns for this framework are in progress.
# ActionLog – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/action-log/design
Action logs surface a chronological record of agent and system activity.
```tsx lines theme={null}
import { ActionLog } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
## Options
The Action Log supports step statuses, optional descriptions, and per-step expandable details.
### Statuses
```tsx lines theme={null}
import { ActionLog } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
Four statuses mark each step: success, in-progress, not-started, and error. The error status uses a distinct red to signal a failed step; all others render in the neutral foreground.
| Status | When to use |
| ----------- | ----------------------------------- |
| Success | The step completed without issues |
| In Progress | The step is actively running |
| Not Started | The step is queued but hasn't begun |
| Error | The step failed and halted the run |
### Descriptions
```tsx lines theme={null}
import { ActionLog } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
An optional description renders below the step title as body text, providing additional context about what the step found or did. When descriptions are present, the connector line between steps spans a taller area, making the timeline easier to scan. Apply descriptions consistently across a log — mixing steps with and without descriptions creates an uneven vertical rhythm.
### Details
```tsx lines theme={null}
import { ActionLog, Text } from "@servicetitan/carto-react-kit";
const detailsContent = (
);
const steps = [
{
id: "a",
title: "Looked up the customer account",
status: "success" as const,
description: "Retrieved account details and recent payment history.",
},
{
id: "b",
title: "Drafting the follow-up summary",
status: "in-progress" as const,
description: "Summarizing overdue balances and recent payment history.",
details: detailsContent,
},
{
id: "c",
title: "Send the summary to the customer",
status: "not-started" as const,
},
];
function App() {
return (
{/* Details collapsed */}
{/* Details open */}
);
}
export default App;
```
A step with a `details` value gains an independent "Show details" disclosure. The details content renders inside a Card below the description and is collapsed by default, keeping the log compact while allowing deeper inspection on demand.
## Behavior
The Action Log uses two levels of disclosure: one for the entire step list and one per step that has details content.
### Expand and Collapse
```tsx lines theme={null}
import { ActionLog } from "@servicetitan/carto-react-kit";
const steps = [
{
id: "a",
title: "Looked up the customer account",
status: "success" as const,
description: "Retrieved account details and recent payment history.",
},
{
id: "b",
title: "Pulled the last 20 invoices",
status: "success" as const,
description: "Found 3 overdue invoices totaling $1,240.",
},
{
id: "c",
title: "Drafting the follow-up summary",
status: "in-progress" as const,
description: "Summarizing overdue balances and payment history.",
},
];
function App() {
return (
{/* Collapsed */}
{/* Expanded (default) */}
);
}
export default App;
```
The whole log starts expanded and collapses behind a "Show steps" / "Hide steps" toggle. Collapsing the log hides all step rows but preserves the expanded or collapsed state of any individual details disclosures.
### Overflow
```tsx lines theme={null}
import { ActionLog } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
Long titles and descriptions wrap to multiple lines within the content column. The connector line extends to match the full height of each step, regardless of how much text wraps.
## Keyboard Interaction
Users can navigate and operate the Action Log using standard keyboard controls.
| Key | Description |
| ------------- | --------------------------------------------- |
| Tab | Moves focus to the next disclosure trigger |
| Enter / Space | Toggles the focused disclosure open or closed |
# Agent Prompt – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/agent-prompt/design
Agent prompts present an inline clarifying question for the user to answer before the agent continues.
```tsx lines theme={null}
import { useState } from "react";
import { AgentPrompt } from "@servicetitan/carto-react-kit";
function App() {
const [value, setValue] = useState(null);
return (
);
}
export default App;
```
## Options
The Agent Prompt supports single-select and multi-select modes, a free-text option, and multi-step flows.
### Selection Modes
Each mode drives a different selection interaction. Single-select uses radio semantics; multi-select uses checkbox semantics.
#### Single Select
```tsx lines theme={null}
import { useState } from "react";
import { AgentPrompt } from "@servicetitan/carto-react-kit";
const options = [
{ value: "choc", label: "Chocolate" },
{ value: "van", label: "Vanilla" },
{ value: "straw", label: "Strawberry" },
{ value: "cookie", label: "Cookie Dough" },
];
function App() {
const [value, setValue] = useState(null);
const [other, setOther] = useState("");
return (
);
}
export default App;
```
Each option row acts as a radio — selecting one deselects all others. The selected row is outlined to confirm the choice.
#### Multi Select
```tsx lines theme={null}
import { useState } from "react";
import { AgentPrompt } from "@servicetitan/carto-react-kit";
const options = [
{ value: "choc", label: "Chocolate" },
{ value: "van", label: "Vanilla" },
{ value: "straw", label: "Strawberry" },
{ value: "cookie", label: "Cookie Dough" },
];
function App() {
const [value, setValue] = useState([]);
const [other, setOther] = useState("");
return (
);
}
export default App;
```
Each row carries a checkbox, and any number of options can be selected. The resolved answer is the set of checked values.
### Free Text
```tsx lines theme={null}
import { useState } from "react";
import { AgentPrompt } from "@servicetitan/carto-react-kit";
const options = [
{ value: "choc", label: "Chocolate" },
{ value: "van", label: "Vanilla" },
{ value: "straw", label: "Strawberry" },
];
function App() {
const [value, setValue] = useState(null);
const [other, setOther] = useState("Pistachio");
return (
);
}
export default App;
```
The "Something else…" free-text row renders when `onOtherChange` is wired. In single-select, the row is mutually exclusive with the option rows — typing deselects any chosen option, and selecting a row retains but subordinates the typed text. In multi-select, the typed value is an independent addition to the selection.
### Multi-step
```tsx lines theme={null}
import { AgentPrompt } from "@servicetitan/carto-react-kit";
const options = [
{ value: "choc", label: "Chocolate" },
{ value: "van", label: "Vanilla" },
{ value: "straw", label: "Strawberry" },
];
const noop = () => {};
function App() {
return (
{/* First step — Next only */}
{/* Middle step — Back + Next */}
{/* Last step — Back + Send */}
);
}
export default App;
```
Passing a `step` object switches the action row to a Back/Next/Send stepper and shows the current step count in the header. The first step shows only Next; middle steps show both Back and Next; the last step shows Back and Send.
## Behavior
The primary action is always enabled — attempting to send with nothing selected surfaces an inline error rather than disabling the button.
### Visual States
```tsx lines theme={null}
import { AgentPrompt } from "@servicetitan/carto-react-kit";
const options = [
{ value: "choc", label: "Chocolate" },
{ value: "van", label: "Vanilla" },
{ value: "straw", label: "Strawberry" },
];
const noop = () => {};
function App() {
return (
{/* Empty — no selection */}
{/* Selected */}
{/* Disabled */}
);
}
export default App;
```
The prompt renders in an empty (no selection), selected, and disabled state. The disabled state dims the options and action row but keeps the close button active so the user can always dismiss.
### Location
The Agent Prompt occupies the same position as the [Prompt Bar](/docs/kits/ai-kit/components/prompt-bar/design) — replacing it for the duration of the question. Once the user sends an answer, the Prompt Bar returns.
### Empty Answer Error
```tsx lines theme={null}
import { AgentPrompt } from "@servicetitan/carto-react-kit";
const options = [
{ value: "choc", label: "Chocolate" },
{ value: "van", label: "🍨 Vanilla" },
{ value: "straw", label: "🍓 Strawberry" },
];
function App() {
return (
{}}
allowOther={false}
onClose={() => {}}
/>
);
}
export default App;
```
When the user presses Send or Next without selecting an option or entering free text, an error message appears beneath the options. Selecting any option or typing in the free-text field clears the error immediately.
## Keyboard Interaction
Users can navigate and operate the Agent Prompt using standard keyboard controls.
| Key | Description |
| --------------- | ------------------------------------------------------------------ |
| Tab | Moves focus through the option group, free-text field, and buttons |
| Arrow Up / Down | Navigates between options in single-select mode |
| Space | Checks or unchecks the focused option in multi-select mode |
| Enter | Activates the focused button (Send, Next, Back, or Close) |
# AppHeader – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/app-header/design
App headers provide top-level navigation and identity for Carto experiences.
```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 (
,
label: "Toggle left panel",
onPress: () => {},
},
]}
trailingActions={[
{
id: "artifact",
icon:
,
label: "Toggle right panel",
onPress: () => {},
},
{
id: "minimize",
icon:
,
label: "Minimize",
onPress: () => {},
},
{
id: "close",
icon:
,
label: "Close",
onPress: () => {},
},
]}
/>
);
}
export default App;
```
## Options
AppHeader supports an optional centered title and two independent action clusters.
### Title
```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 (
{/* With title */}
,
label: "Toggle left panel",
onPress: () => {},
},
]}
trailingActions={[
{ id: "close", icon:
, label: "Close", onPress: () => {} },
]}
/>
{/* Without title — action-only header */}
,
label: "Toggle left panel",
onPress: () => {},
},
]}
trailingActions={[
{ id: "close", icon:
, label: "Close", onPress: () => {} },
]}
/>
);
}
export default App;
```
`title` renders centered between the two action clusters with `` 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
```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 (
,
label: "Toggle left panel",
onPress: () => {},
},
]}
trailingActions={[
{
id: "artifact",
icon:
,
label: "Toggle right panel",
onPress: () => {},
},
{ id: "close", icon:
, label: "Close", onPress: () => {} },
]}
/>
);
}
export default App;
```
`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
```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 (
,
label: "Toggle left panel",
onPress: () => {},
},
]}
trailingActions={[
{
id: "artifact",
icon:
,
label: "Toggle right panel",
onPress: () => {},
},
{
id: "minimize",
icon:
,
label: "Minimize",
onPress: () => {},
},
{ id: "close", icon:
, label: "Close", onPress: () => {} },
]}
/>
);
}
export default App;
```
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
```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 (
,
label: "Toggle left panel",
onPress: () => {},
},
]}
trailingActions={[
{
id: "artifact",
icon:
,
label: "Toggle right panel",
onPress: () => {},
disabled: true,
},
{
id: "minimize",
icon:
,
label: "Minimize",
onPress: () => {},
disabled: true,
},
{ id: "close", icon:
, label: "Close", onPress: () => {} },
]}
/>
);
}
export default App;
```
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
# AppShell – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/app-shell/design
App shells frame the overall layout for Carto product experiences.
```tsx lines theme={null}
import { AppShell, Text } from "@servicetitan/carto-react-kit";
function App() {
return (
}
/>
);
}
export default App;
```
## Options
AppShell composes up to four regions — a full-width header, a left rail, a required center chat, and a right artifact panel. All region state (visibility, width, presentation) lives inside the shell; drive it imperatively through `useAppSurface()` or a `controlRef`, never through props that re-render the slots.
### Regions
```tsx lines theme={null}
import {
AppHeader,
AppShell,
CartoTheme,
Text,
useAppSurface,
} from "@servicetitan/carto-react-kit";
import {
IconPanelLeft,
IconPanelRight,
} from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
function ShellHeader() {
const { toggleRail, toggleArtifact } = useAppSurface();
return (
,
label: "Toggle rail",
onPress: toggleRail,
},
]}
trailingActions={[
{
id: "artifact",
icon: ,
label: "Toggle artifact panel",
onPress: toggleArtifact,
},
]}
/>
);
}
function RegionPlaceholder({
label,
background,
}: {
label: string;
background: string;
}) {
return (
);
}
function App() {
return (
}
rail={
}
chat={
}
artifact={
}
/>
);
}
export default App;
```
#### Artifact
The right-side region — typically a canvas or generated-artifact panel. Omit it to render no artifact. It's drag-resizable from its leading edge and yields to a modal drawer, after the rail, once the shell can no longer fit it inline.
#### Chat
The required, invariant center. Chat is always inline and never collapses — it's the one region guaranteed to remain, and it flexes to absorb whatever width the side regions yield.
#### Header
The full-width region across the top, typically an `AppHeader`. Omit it for a shell with no header.
#### Left Rail
The left region — typically navigation. It's fixed-width (240px) and never drag-resized; when it no longer fits, it yields to a drawer before the artifact does.
### Artifact Width
```tsx lines theme={null}
import {
AppHeader,
AppShell,
CartoTheme,
Text,
useAppSurface,
} from "@servicetitan/carto-react-kit";
import {
IconPanelLeft,
IconPanelRight,
} from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
function LargeArtifactHeader() {
const { toggleRail, toggleArtifact } = useAppSurface();
return (
,
label: "Toggle rail",
onPress: toggleRail,
},
]}
trailingActions={[
{
id: "artifact",
icon: ,
label: "Toggle artifact panel (large)",
onPress: () => toggleArtifact({ width: "large" }),
},
]}
/>
);
}
function RegionPlaceholder({
label,
background,
}: {
label: string;
background: string;
}) {
return (
);
}
function App() {
return (
}
rail={
}
chat={
}
artifact={
}
defaultArtifactWidth="large"
/>
);
}
export default App;
```
`defaultArtifactWidth` sets the starting preset — `"regular"`, a fixed 560px (the default, shown in Regions above), or `"large"`, a 720px floor that fills the available width and collapses the rail to reclaim its space. Either preset is a starting point only; the artifact stays drag-resizable from there, and the same presets are available imperatively via `showArtifact({ width })` / `toggleArtifact({ width })`.
## Behavior
Region presentation responds purely to available width — there are no viewport breakpoints — and every region can be shown or hidden imperatively regardless of how it got there.
### Artifact Resize
```tsx lines theme={null}
import {
AppHeader,
AppShell,
CartoTheme,
Text,
useAppSurface,
} from "@servicetitan/carto-react-kit";
import {
IconPanelLeft,
IconPanelRight,
} from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
function ShellHeader() {
const { toggleRail, toggleArtifact } = useAppSurface();
return (
,
label: "Toggle rail",
onPress: toggleRail,
},
]}
trailingActions={[
{
id: "artifact",
icon: ,
label: "Toggle artifact panel",
onPress: toggleArtifact,
},
]}
/>
);
}
function RegionPlaceholder({
label,
background,
}: {
label: string;
background: string;
}) {
return (
);
}
function App() {
return (
}
chat={
}
artifact={
}
/>
);
}
export default App;
```
Drag the artifact's leading edge (or focus its handle and use the arrow keys) to resize it. Dragging past the point where chat would drop below its minimum width detaches the artifact into a drawer that keeps widening up to the full viewport; dragging back re-attaches it inline. Chat never goes below its minimum — the artifact and rail absorb the constraint instead.
### Auto-collapse
```tsx lines theme={null}
import {
AppHeader,
AppShell,
CartoTheme,
Text,
useAppSurface,
} from "@servicetitan/carto-react-kit";
import {
IconPanelLeft,
IconPanelRight,
} from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
function ShellHeader() {
const { toggleRail, toggleArtifact } = useAppSurface();
return (
,
label: "Toggle rail",
onPress: toggleRail,
},
]}
trailingActions={[
{
id: "artifact",
icon: ,
label: "Toggle artifact panel",
onPress: toggleArtifact,
},
]}
/>
);
}
function RegionPlaceholder({
label,
background,
}: {
label: string;
background: string;
}) {
return (
);
}
function App() {
return (
}
rail={
}
chat={
}
artifact={
}
/>
);
}
export default App;
```
When a side region no longer fits inline, it becomes a modal drawer rather than disappearing — reachable again via the header's toggle. The rail collapses first (lowest priority), then the artifact, purely based on each region's own minimum width against the space available. Widening the shell re-inlines a region the instant it fits again.
### Initial Visibility
```tsx lines theme={null}
import { useEffect, useRef } from "react";
import {
AppHeader,
AppShell,
CartoTheme,
Text,
useAppSurface,
type AppSurfaceHandle,
} from "@servicetitan/carto-react-kit";
import {
IconPanelLeft,
IconPanelRight,
} from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
function ShellHeader() {
const { toggleRail, toggleArtifact } = useAppSurface();
return (
,
label: "Toggle rail",
onPress: toggleRail,
},
]}
trailingActions={[
{
id: "artifact",
icon: ,
label: "Toggle artifact panel",
onPress: toggleArtifact,
},
]}
/>
);
}
function RegionPlaceholder({
label,
background,
}: {
label: string;
background: string;
}) {
return (
);
}
function App() {
const controlRef = useRef(null);
useEffect(() => controlRef.current?.hideRail(), []);
return (
}
rail={
}
chat={
}
artifact={
}
/>
);
}
export default App;
```
Region state is internal, so initial visibility is set imperatively rather than through a prop: grab a `controlRef` and call `hideRail()` (or `hideArtifact()`) in an effect on mount. Its space is reclaimed by chat until the header's toggle brings it back.
## Keyboard Interaction
Users can navigate, resize, and dismiss AppShell's regions using standard keyboard controls.
| Key | Description |
| ------------------------ | -------------------------------------------------------------------- |
| Tab | Moves focus among header actions, resize handles, and region content |
| Arrow Left / Arrow Right | Resizes the focused handle by one step |
| Escape | Dismisses an open drawer |
| Enter / Space | Activates the focused header action |
## Patterns that use this component
* [Agentic Experience Shell](/docs/kits/ai-kit/patterns/agentic-experience-shell) — Arranges the header, history rail, conversation, and supporting panel
# Artifact Card – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/artifact-card/design
Artifact cards present Atlas-generated content as a compact, actionable row in the chat transcript.
```tsx lines theme={null}
import { ArtifactCard } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
ArtifactCard surfaces content Atlas generated during the conversation — never content the user retrieved or uploaded themselves. It lives in the chat transcript only, never inside the artifact panel: pressing it either opens the panel to reveal the full result, or downloads the artifact directly, depending on what it represents. Don't use it as a collapsible card inside the artifact panel.
## Options
The Artifact Card supports an icon and an optional description.
### Icon
```tsx lines theme={null}
import { ArtifactCard } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* With icon */}
{}}
/>
{/* Without icon */}
{}}
/>
);
}
export default App;
```
The icon is a decorative avatar that signals the kind of artifact the card represents. `artifactType` only accepts `document` for the time being — the prop stays in place so more kinds can be added later without a breaking change. Omitting `artifactType` renders the card without an icon — title and description only.
### Description
```tsx lines theme={null}
import { ArtifactCard } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* With description */}
{}}
/>
{/* Without description */}
{}}
/>
);
}
export default App;
```
An optional description renders beneath the title as compact body text, providing supporting context about the artifact's content.
## Behavior
The entire card surface is a single button. Padding, the trailing caret, and interactivity are fixed and not configurable.
### Visual States
```tsx lines theme={null}
import { ArtifactCard } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* Rest */}
{}}
/>
{/* Hover */}
{}}
/>
{/* Focus */}
{}}
/>
);
}
export default App;
```
The card responds to hover, focus, and press with a surface elevation change inherited from the Card component. The focus ring appears on keyboard navigation.
### Overflow
```tsx lines theme={null}
import { ArtifactCard } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
Long titles and descriptions wrap within the card's content column. The caret stays aligned to the top of the content area.
## Keyboard Interaction
Users can navigate and activate the Artifact Card using standard keyboard controls.
| Key | Description |
| ------------- | ---------------------------------------------- |
| Tab | Moves focus to the card button |
| Enter / Space | Activates the focused card and fires `onPress` |
# BottomSheet – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/bottom-sheet/design
Bottom sheets present secondary content in a panel that rises from the bottom of the screen.
```tsx lines theme={null}
import { BottomSheet, Button } from "@servicetitan/carto-react-kit";
function App() {
return (
}
/>
);
}
export default App;
```
## Options
The Bottom Sheet supports an optional subheader, an optional close button, and any content composed from Carto components.
### Subheader
```tsx lines theme={null}
import { BottomSheet, Button } from "@servicetitan/carto-react-kit";
function App() {
return (
}
/>
);
}
export default App;
```
An optional subheader renders a secondary line beneath the title — useful for a short status or timestamp. Omit it for a title-only header, as in the hero example above.
### Close Button
```tsx lines theme={null}
import { BottomSheet, Button } from "@servicetitan/carto-react-kit";
function App() {
return (
}
/>
);
}
export default App;
```
The header close button is shown by default. Set `showCloseButton={false}` to hide it — the sheet remains dismissable via the drag handle, the backdrop, and Escape.
### Content
```tsx lines theme={null}
import { BottomSheet, Button, Checkbox } from "@servicetitan/carto-react-kit";
function App() {
return (
}
defaultOpen
trigger={ }
/>
);
}
export default App;
```
`content` accepts any `ReactNode`, so compose Carto components — `Checkbox`, `ArtifactCard`, `ActionLog`, and so on — to build the sheet's body.
## Behavior
The Bottom Sheet negotiates its own height from content and viewport, and its body scrolls independently of the pinned header.
### Sizing and Resizing
The sheet has a single, self-negotiated size rather than discrete size options. It opens at a start height that hugs its content up to 50% of the viewport, and the drag handle — draggable by pointer, or by Arrow Up / Arrow Down / Home / End when focused — resizes it between that start height and a 90% viewport ceiling. Dragging or pressing Arrow Down below the start height closes the sheet.
### Scrolling Body
```tsx lines theme={null}
import { BottomSheet, Button, Text } from "@servicetitan/carto-react-kit";
function App() {
return (
{Array.from({ length: 12 }, (_, i) => (
))}
}
defaultOpen
trigger={ }
/>
);
}
export default App;
```
When content exceeds the current detent, the body scrolls independently while the header and drag handle stay pinned. A scroll shadow appears once the body has scrolled.
## Keyboard Interaction
Users can navigate, resize, and dismiss the Bottom Sheet using standard keyboard controls.
| Key | Description |
| --------------------- | ----------------------------------------------------------------- |
| Tab | Moves focus among the drag handle, close button, and body content |
| Arrow Up / Arrow Down | Resizes the sheet by one detent when the drag handle is focused |
| Home / End | Jumps the drag handle to the start or maximum detent |
| Escape | Dismisses the sheet |
# Button – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/button/design
Buttons are interactive controls that trigger an action or event.
```tsx lines theme={null}
import { Button } from "@servicetitan/carto-react-kit";
function App() {
return ;
}
export default App;
```
## Options
The Button supports variants, sizes, and icon configurations to fit different action hierarchies.
### Variants
```tsx lines theme={null}
import { Button } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
| Variant | When to use |
| --------- | ------------------------------------------------------------------------ |
| Primary | The main action in a view. Prefer one primary action in the same context |
| Secondary | Supporting actions that sit alongside or below a primary action |
| Ghost | Low-emphasis actions, including many icon-only controls |
| Danger | Destructive or irreversible actions that need clear caution |
### Sizes
```tsx lines theme={null}
import { Button } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
| Size | Typical use |
| ------ | ----------------------------------------------------------------------------------------------- |
| Medium | Default size for most product actions |
| Small | Dense layouts where space is limited |
| Xsmall | Compact desktop UI. On touch devices, the tap target is promoted to meet accessibility guidance |
### Icons
```tsx lines theme={null}
import { Button } from "@servicetitan/carto-react-kit";
import {
IconChevronRight,
IconPencil,
IconPlus,
} from "@servicetitan/carto-react-kit/icons";
function App() {
return (
, position: "left" }}
/>
, position: "right" }}
/>
} />
);
}
export default App;
```
Icons can sit before or after the label, or replace the label entirely. Icon-only buttons keep a short label for accessibility and show that label in a tooltip on hover and focus.
## Behavior
The Button responds to pointer, touch, and keyboard interaction with clear resting, hover, pressed, focused, and disabled states.
### Visual States
```tsx lines theme={null}
import { Button } from "@servicetitan/carto-react-kit";
const variants = ["primary", "secondary", "ghost", "danger"] as const;
function App() {
return (
{variants.map((variant) => (
))}
);
}
export default App;
```
Hover and press update the surface to confirm the control is interactive. Keyboard focus shows a visible focus ring. Disabled buttons remain in the tab order where appropriate but do not activate.
### Icon-only labeling
```tsx lines theme={null}
import { Button } from "@servicetitan/carto-react-kit";
import { IconPencil } from "@servicetitan/carto-react-kit/icons";
function App() {
return (
}
/>
);
}
export default App;
```
Icon-only buttons always carry a required label. That label becomes the accessible name and the tooltip content so pointer and assistive-technology users share the same affordance name.
## Keyboard Interaction
Users can activate the Button using standard keyboard controls.
| Key | Description |
| ------------- | ---------------------------- |
| Tab | Moves focus to the button |
| Enter / Space | Activates the focused button |
### Accessibility
Every Button requires a label so it always has an accessible name. Icon-only buttons expose that label through `aria-label` and a presentational tooltip so the name is not announced twice. Prefer press handlers that work for click, touch, and keyboard activation together.
# Card – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/card/design
Cards group related content in a contained surface.
```tsx lines theme={null}
import { Card, Text } from "@servicetitan/carto-react-kit";
function App() {
return (
}
/>
);
}
export default App;
```
## Options
The Card supports padding and three interaction shapes — static, link, and button.
### Variants
```tsx lines theme={null}
import { Card, Text } from "@servicetitan/carto-react-kit";
function App() {
return (
} />
}
/>
{}}
label="Run example action"
content={ }
/>
);
}
export default App;
```
Card renders one of three shapes based on which props are passed: neither `href` nor `onPress` renders a non-interactive `` surface; `href` renders a whole-card link (`
`); `onPress` renders a whole-card button (``). Interactive cards require a `label` for their accessible name.
### Padding
```tsx lines theme={null}
import { Card, Text } from "@servicetitan/carto-react-kit";
function App() {
return (
} />
}
/>
}
/>
);
}
export default App;
```
| Padding option | Value |
| -------------- | ----- |
| Snug | 12px |
| Base (default) | 16px |
| Relaxed | 24px |
### Interactive Card
```tsx lines theme={null}
import { Card, Text } from "@servicetitan/carto-react-kit";
function App() {
return (
{}}
label="Run this action"
content={ }
/>
}
/>
);
}
export default App;
```
Both interactive shapes use a stretched hit target that covers the full surface, so the entire card — not just its text — responds to a click or tap. Their accessible name comes from the required `label` prop, not from the visible content.
## Behavior
Only the interactive variants — link and button — respond to hover, focus, and disabled states. The static variant has no interactive behavior.
### Visual States
```tsx lines theme={null}
import { Card, Text } from "@servicetitan/carto-react-kit";
function App() {
return (
}
/>
}
/>
}
/>
{}} label="Rest" content={ } />
{}}
label="Hover"
data-interactive="hover"
content={ }
/>
{}}
label="Focus"
data-interactive="focus-visible"
content={ }
/>
{}}
label="Disabled"
isDisabled
content={ }
/>
);
}
export default App;
```
Hover lifts the surface with a stronger shadow, and keyboard focus draws a ring around the whole card. Disabled — button cards only, since links have no disabled state — dims the surface and suppresses the hover lift.
## Keyboard Interaction
Keyboard interaction applies only to the interactive variants — link and button. The static variant is not focusable.
| Key | Description |
| ----- | -------------------------------------------------------------- |
| Tab | Moves focus to the card |
| Enter | Activates a focused link or button card |
| Space | Activates a focused button card (links don't respond to Space) |
# ChatHistory – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/chat-history/design
Chat history lists prior conversations for navigation and resume.
```tsx lines theme={null}
import { ChatHistory } from "@servicetitan/carto-react-kit";
import type { ChatHistoryItem } from "@servicetitan/carto-react-kit";
const items: ChatHistoryItem[] = [
{ id: "q3", label: "Q3 planning notes", subtitle: "35 mins" },
{
id: "invoice",
label: "Invoice discrepancy follow-up",
subtitle: "2 hours",
},
{ id: "customer", label: "Customer churn analysis", subtitle: "Yesterday" },
];
function App() {
return (
);
}
export default App;
```
## Options
ChatHistory renders a typed `items` array — each chat carries a required label and timestamp, and can flag itself as unread.
### Selected State
```tsx lines theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/carto-react-kit";
import type { ChatHistoryItem } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const items: ChatHistoryItem[] = [
{ id: "q3", label: "Q3 planning notes", subtitle: "35 mins" },
{
id: "invoice",
label: "Invoice discrepancy follow-up",
subtitle: "2 hours",
},
];
function App() {
return (
);
}
export default App;
```
Selection is locked to single — at most one chat is ever active. The selected row keeps a persistent tint and bolds its title; drive it with `selectedKeys` / `onSelectionChange` (controlled) or `defaultSelectedKeys` (uncontrolled), the same way you would a [Menu](/docs/kits/ai-kit/components/menu/design).
### Unread
```tsx lines theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/carto-react-kit";
import type { ChatHistoryItem } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const items: ChatHistoryItem[] = [
{
id: "auth",
label: "Refactoring the auth flow",
subtitle: "Just now",
unread: true,
},
{ id: "q3", label: "Q3 planning notes", subtitle: "35 mins", unread: true },
{ id: "karaoke", label: "Karaoke track separation", subtitle: "Yesterday" },
];
function App() {
return (
);
}
export default App;
```
Set `unread` on a chat to render a dot at the end of its title row (announced as "Unread" to assistive tech). Unread is independent of selection — a chat can be both unread and active.
### Time stamp
```tsx lines theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/carto-react-kit";
import type { ChatHistoryItem } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
// `subtitle` is rendered verbatim — the consumer owns any relative-time
// formatting. These four rows show the range a stamp typically takes: a
// relative duration, a clock time, a day name, and an absolute date.
const items: ChatHistoryItem[] = [
{ id: "auth", label: "Refactoring the auth flow", subtitle: "35 mins" },
{ id: "karaoke", label: "Karaoke track separation", subtitle: "10:40am" },
{
id: "onboarding",
label: "Rewrite the onboarding email",
subtitle: "Yesterday",
},
{ id: "schema", label: "Postgres schema migration plan", subtitle: "Mar 14" },
];
function App() {
return (
);
}
export default App;
```
`subtitle` is rendered verbatim — Carto doesn't compute relative time. Pass whatever stamp fits the moment: a relative duration, a clock time, a day name, or an absolute date. The consumer owns the formatting and any policy for when it changes.
## Behavior
Rows respond to the standard interaction states and truncate or scroll rather than reflow the rail.
### Visual States
```tsx lines theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/carto-react-kit";
import type { ChatHistoryItem } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const items: ChatHistoryItem[] = [
{ id: "default", label: "Default", subtitle: "Just now" },
{ id: "hover", label: "Hover target", subtitle: "35 mins" },
{ id: "focus", label: "Focus target", subtitle: "10:40am" },
{ id: "disabled", label: "Disabled", subtitle: "Yesterday" },
{ id: "selected", label: "Selected", subtitle: "Friday" },
];
function App() {
return (
);
}
export default App;
```
Unselected rows stay neutral — transparent at rest, with a subtle fill on hover and press. The selected row keeps that fill persistently. Keyboard focus shows the ring only, and a disabled chat (via `disabledKeys`) can't be focused, selected, or activated.
### Truncation
```tsx lines theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/carto-react-kit";
import type { ChatHistoryItem } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const items: ChatHistoryItem[] = [
{
id: "long",
label: "A conversation title long enough to overflow the rail width",
subtitle: "An unread stamp long enough to overflow the rail width too",
},
];
function App() {
return (
);
}
export default App;
```
A title or subtitle longer than the rail's width truncates to a single line with a trailing ellipsis — the row never wraps or grows taller.
### Overflow
```tsx lines theme={null}
import { CartoTheme, ChatHistory } from "@servicetitan/carto-react-kit";
import type { ChatHistoryItem } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const items: ChatHistoryItem[] = [
{ id: "auth", label: "Refactoring the auth flow", subtitle: "Just now" },
{ id: "q3", label: "Q3 planning notes", subtitle: "35 mins" },
{ id: "karaoke", label: "Karaoke track separation", subtitle: "10:40am" },
{
id: "onboarding",
label: "Rewrite the onboarding email",
subtitle: "Yesterday",
},
{
id: "schema",
label: "Postgres schema migration plan",
subtitle: "Yesterday",
},
{ id: "pricing", label: "Pricing page copy review", subtitle: "Friday" },
{ id: "incident", label: "Incident postmortem draft", subtitle: "Mar 14" },
{ id: "roadmap", label: "2027 roadmap brainstorm", subtitle: "Mar 12" },
];
function App() {
return (
);
}
export default App;
```
A history longer than the rail's height scrolls within it; the "New Chat" button stays pinned above the list.
## Keyboard Interaction
Users can navigate and select chats using standard keyboard controls.
| Key | Description |
| --------------------- | ---------------------------------------------------------- |
| Tab | Moves focus into or out of the list |
| Arrow Up / Arrow Down | Moves focus to the previous/next chat |
| Enter / Space | Selects the focused chat |
| Type-ahead | Jumps focus to the next chat matching the typed characters |
## Patterns that use this component
* [Agentic Experience Shell](/docs/kits/ai-kit/patterns/agentic-experience-shell) — Provides access to previous conversations
# ChatLayout – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/chat-layout/design
Chat layouts structure the primary conversation experience.
```tsx lines theme={null}
import { useState } from "react";
import {
ChatLayout,
PromptBar,
UserMessage,
} from "@servicetitan/carto-react-kit";
function App() {
const [value, setValue] = useState("");
return (
);
}
export default App;
```
## Options
ChatLayout is configuration-first: `conversation` and `prompt` are typed props, not composed children, and both are optional.
### Conversation slot
```tsx lines theme={null}
import { type ReactNode, useState } from "react";
import {
CartoTheme,
ChatLayout,
PromptBar,
UserMessage,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function Frame({ children }: { children: ReactNode }) {
return (
{children}
);
}
function App() {
const [value, setValue] = useState("");
const prompt = (
);
const conversation = (
);
return (
);
}
export default App;
```
The three slot configurations side by side: the full column, `conversation` alone (a read-only or archived view, with no composer), and `prompt` alone (an empty landing state, ready for the first message). Either slot can be omitted independently — the other still renders correctly on its own.
### Prompt slot
The `prompt` composer is typically a [PromptBar](/docs/kits/ai-kit/components/prompt-bar/design), rendered below `conversation` and pinned to the bottom of the region regardless of how much transcript precedes it.
## Behavior
The transcript manages its own scroll position so a growing conversation never fights the reader.
### Stick to Bottom
```tsx lines theme={null}
import { useState } from "react";
import {
CartoTheme,
ChatLayout,
PromptBar,
UserMessage,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
const [value, setValue] = useState("");
return (
{Array.from({ length: 8 }, (_, i) => (
))}
>
}
prompt={
}
/>
);
}
export default App;
```
The transcript sticks to the bottom on its own as messages arrive. The moment the user scrolls up to read history, it stops auto-scrolling — so incoming content never yanks them back down — and re-engages the lock once they scroll back to the bottom.
### Scroll to Latest
```tsx lines theme={null}
import { useState } from "react";
import {
CartoTheme,
ChatLayout,
PromptBar,
UserMessage,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
const [value, setValue] = useState("");
return (
{Array.from({ length: 8 }, (_, i) => (
))}
>
}
prompt={
}
/>
);
}
export default App;
```
A floating "scroll to latest" button appears whenever the user has scrolled away from the bottom. Pressing it scrolls smoothly back to the latest content and re-engages the stick-to-bottom lock. Force the same scroll programmatically — e.g. right after the user sends a message — via `controlRef`'s `scrollToBottom()`.
## Keyboard Interaction
Users can scroll the transcript using standard keyboard controls.
| Key | Description |
| --------------------- | -------------------------------------- |
| Tab | Moves focus into the transcript region |
| Arrow Up / Arrow Down | Scrolls the focused transcript |
| Page Up / Page Down | Scrolls the transcript by a page |
## Patterns that use this component
* [Agentic Experience Shell](/docs/kits/ai-kit/patterns/agentic-experience-shell) — Defines the conversation region
# Checkbox – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/checkbox/design
Checkboxes let users select one or more options from a set.
```tsx lines theme={null}
import { Checkbox } from "@servicetitan/carto-react-kit";
function App() {
return ;
}
export default App;
```
## Options
The Checkbox supports selection, an indeterminate mixed state, and either a visible label or an indicator-only presentation.
### Selection
```tsx lines theme={null}
import { Checkbox } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
The checkbox toggles between unselected and selected. Use `isSelected` for controlled selection or `defaultSelected` for uncontrolled.
### Indeterminate
```tsx lines theme={null}
import { useState } from "react";
import { Checkbox } from "@servicetitan/carto-react-kit";
function App() {
const [completed, setCompleted] = useState(true);
const [cancelled, setCancelled] = useState(false);
const allSelected = completed && cancelled;
const someSelected = completed || cancelled;
return (
{
setCompleted(isSelected);
setCancelled(isSelected);
}}
/>
);
}
export default App;
```
`isIndeterminate` renders a minus indicator for a select-all row whose children are only partially selected. It's a display-only override — pair it with your own logic to derive the mixed state from the children's selection, as shown above.
### Label
```tsx lines theme={null}
import { Checkbox } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* With visible label */}
{/* Indicator only, named via aria-label */}
);
}
export default App;
```
A visible `label` doubles as the accessible name. Omit `label` and supply `aria-label` or `aria-labelledby` instead for an indicator-only checkbox — common in dense table rows.
## Behavior
The Checkbox responds to selection, focus, and disabled states.
### Visual States
```tsx lines theme={null}
import { Checkbox } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* Rest */}
{/* Selected */}
{/* Indeterminate */}
{/* Focus */}
{/* Disabled */}
{/* Disabled + selected */}
);
}
export default App;
```
Selecting the checkbox fills the indicator; keyboard focus draws a ring scoped to the square indicator, not the full label width. `isDisabled` dims the control and blocks interaction in either selection state.
## Keyboard Interaction
Users can navigate and toggle the Checkbox using standard keyboard controls.
| Key | Description |
| ----- | -------------------------------- |
| Tab | Moves focus to the next checkbox |
| Space | Toggles the focused checkbox |
# Citation – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/citation/design
Citations attribute content to a source within agent responses.
```tsx lines theme={null}
import { Citation } from "@servicetitan/carto-react-kit";
function App() {
return ;
}
export default App;
```
## Options
The Citation supports opening its destination in a new tab and can be placed on its own or inline within body text.
### Open in New
```tsx lines theme={null}
import { Citation } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* Opens in a new tab */}
{/* Stays on the page */}
);
}
export default App;
```
Pass `target="_blank"` to open the citation in a new tab. That also renders a trailing external-link icon labeled "opens in a new tab" and sets `rel="noopener noreferrer"` unless you supply a `rel`. Omit `target` for on-page citations that scroll to a same-page sources list — the icon is omitted in that case.
### Inline
```tsx lines theme={null}
import { Citation, Text } from "@servicetitan/carto-react-kit";
function App() {
return (
The customer's account is in good standing with no overdue balances{" "}
.
>
}
/>
);
}
export default App;
```
Citations sit inline within body text, immediately after the claim they support.
## Behavior
The Citation responds to hover and keyboard focus like any other link.
### Visual States
```tsx lines theme={null}
import { Citation } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* Rest */}
{/* Hover */}
{/* Focus */}
);
}
export default App;
```
Hover shifts the pill to the informative (blue) intent. Keyboard focus draws a ring around the whole pill.
## Keyboard Interaction
Users can navigate and activate the Citation using standard keyboard controls.
| Key | Description |
| ----- | --------------------------------------- |
| Tab | Moves focus to the citation |
| Enter | Activates the focused citation's `href` |
# Dialog – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/dialog/design
Dialogs present focused tasks or decisions in a modal overlay.
```tsx lines theme={null}
import { Button, Dialog } from "@servicetitan/carto-react-kit";
function App() {
return (
}
/>
);
}
export default App;
```
## Options
The Dialog supports a primary action, an optional secondary action, and a danger variant for destructive confirmations.
### Actions
```tsx lines theme={null}
import { Button, Dialog } from "@servicetitan/carto-react-kit";
function App() {
return (
}
/>
);
}
export default App;
```
Omit `secondaryAction` for a single-action, acknowledge-only dialog — the lone action sits at the trailing edge of the footer. Pass both for a confirm/cancel pair, as in the hero example above.
### Action Variants
#### Primary Action
```tsx lines theme={null}
import { Button, Dialog } from "@servicetitan/carto-react-kit";
function App() {
return (
}
/>
);
}
export default App;
```
`primaryAction` defaults to `variant: "primary"`, rendering the standard confirm treatment.
#### Danger Action
```tsx lines theme={null}
import { Button, Dialog } from "@servicetitan/carto-react-kit";
function App() {
return (
}
/>
);
}
export default App;
```
Set `primaryAction`'s `variant` to `"danger"` for a destructive confirmation — it renders the critical (red) Button so the risk of the action is visually distinct.
#### No Actions
```tsx lines theme={null}
import { Button, Dialog, Text } from "@servicetitan/carto-react-kit";
function App() {
return (
}
defaultOpen
trigger={ }
/>
);
}
export default App;
```
Omit both `primaryAction` and `secondaryAction` for a footerless dialog — useful for a media preview or informational dialog where the header's X close button is the only dismiss affordance.
## Behavior
The Dialog traps focus while open and scrolls its own body independently of the pinned header and footer.
### Scrolling Body
```tsx lines theme={null}
import { Button, Dialog, Text } from "@servicetitan/carto-react-kit";
function App() {
return (
{Array.from({ length: 12 }, (_, i) => (
))}
}
primaryAction={{ label: "Accept" }}
secondaryAction={{ label: "Decline" }}
defaultOpen
trigger={ }
/>
);
}
export default App;
```
Body content beyond 90% of the viewport height scrolls within the dialog while the header and footer stay pinned.
## Keyboard Interaction
Users can navigate, scroll, and dismiss the Dialog using standard keyboard controls.
| Key | Description |
| ------------- | -------------------------------------------------------------------- |
| Tab | Moves focus among the close button, body content, and footer actions |
| Enter / Space | Activates the focused close button or footer action |
| Escape | Dismisses the dialog |
# DiffValue – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/diff-value/design
Diff values highlight changes between previous and updated values.
```tsx lines theme={null}
import { DiffValue } from "@servicetitan/carto-react-kit";
function App() {
return ;
}
export default App;
```
## Options
The Diff Value supports empty and removed source values, and renders either an inline or a block layout.
### Empty and Removed Values
```tsx lines theme={null}
import { DiffValue, Text } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
Omit or pass `null` for `from` to render `(Empty)` — there was no prior value. Omit or pass `null` for `to` to render the highlighted `(Removed)` label — the value was taken away entirely.
### Layout
```tsx lines theme={null}
import { DiffValue } from "@servicetitan/carto-react-kit";
function App() {
return ;
}
export default App;
```
```tsx lines theme={null}
import { DiffValue } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
Layout defaults to `inline` for short values on a single line, and `block` — stacking `from` above `to` — once either side exceeds roughly 80 characters or contains a newline. Pass `layout` explicitly to override the automatic choice.
## Behavior
The Diff Value is otherwise static; only its block layout can become interactive when text is truncated.
### Truncation
```tsx lines theme={null}
import { DiffValue } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
In block layout, text beyond `truncateLines` (default `2`) clips with a "Show more" control that expands it in place; an expanded side shows "Show less" to collapse it again. Each side truncates independently.
## Keyboard Interaction
The diff itself is static text and is not part of the tab sequence. When block layout truncates a side, its "Show more" / "Show less" control is a standard button.
| Key | Description |
| ------------- | ----------------------------------------------------------- |
| Tab | Moves focus to a visible "Show more" or "Show less" control |
| Enter / Space | Toggles the focused control's expanded state |
# GuidanceCard – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/guidance-card/design
Guidance cards surface recommendations or instructional content.
```tsx lines theme={null}
import { GuidanceCard } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
## Options
The Guidance Card supports four lifecycle states, single and table change sections, and an optional linked reference.
### States
```tsx lines theme={null}
import { GuidanceCard } from "@servicetitan/carto-react-kit";
const sections = [
{
type: "single" as const,
changes: [
{ id: "labor-hours", label: "Labor hours", from: "6.5h", to: "8.0h" },
],
},
];
function App() {
return (
{/* Default — interactive review */}
{/* Accepted — collapsed summary */}
{/* Rejected — collapsed summary */}
{/* Skipped — collapsed summary */}
);
}
export default App;
```
`default` shows selectable diffs with Accept, Edit, and Reject actions. `accepted`, `rejected`, and `skipped` collapse the same content into a read-only disclosure that starts closed — expand it to review what was suggested.
### Change Sections
```tsx lines theme={null}
import { GuidanceCard } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* Single section — scalar diff rows */}
{/* Table section — labeled line-item table */}
);
}
export default App;
```
A change section is either `single` — one or more scalar diff rows — or `table` — one or more labeled line-item tables. Combine both types, in any order, within `changeSections` to build a mixed review.
### Reference Link
```tsx lines theme={null}
import { GuidanceCard } from "@servicetitan/carto-react-kit";
const sections = [
{
type: "single" as const,
changes: [
{ id: "labor-hours", label: "Labor hours", from: "6.5h", to: "8.0h" },
],
},
];
function App() {
return (
{/* Plain text reference */}
{/* Linked reference — opens in a new tab */}
);
}
export default App;
```
`referenceHref` turns the reference label into a link that opens the related estimate or invoice in a new tab. Omit it to render the label as plain text.
## Behavior
In the default state, every change starts selected; the card tracks its own selection from there and disables its footer actions once everything is deselected.
### Selection
```tsx lines theme={null}
import { GuidanceCard } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* Multiple changes — each row gets its own checkbox */}
{/* One change — no checkbox; Accept applies to the sole item */}
);
}
export default App;
```
When a section has more than one selectable change, each row starts checked and gets its own checkbox — uncheck a row to exclude it before accepting. A lone change omits the checkbox entirely; Accept always applies to it.
### Actions
```tsx lines theme={null}
import { GuidanceCard } from "@servicetitan/carto-react-kit";
const changes = [
{ id: "labor-hours", label: "Labor hours", from: "6.5h", to: "8.0h" },
{ id: "labor-rate", label: "Labor rate", from: "$95/hr", to: "$110/hr" },
];
const sections = [{ type: "single" as const, changes }];
function App() {
return (
{/* Default — every change starts selected; Accept and Edit are enabled */}
{/* Everything deselected — Accept and Edit become disabled */}
);
}
export default App;
```
Accept and Edit are enabled by default — every change starts selected — and disable only once every change is deselected. Reject is always available and needs no selection. Accept and Edit each receive the ids of the currently selected changes and table rows.
## Keyboard Interaction
Users can navigate, select, and act on the Guidance Card using standard keyboard controls.
| Key | Description |
| ------------- | -------------------------------------------------------------------- |
| Tab | Moves focus among checkboxes, the reference link, and footer actions |
| Space | Toggles the focused checkbox |
| Enter / Space | Activates the focused reference link or footer action |
# Link – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/link/design
Links navigate users to another page or location.
```tsx lines theme={null}
import { Link } from "@servicetitan/carto-react-kit";
function App() {
return ;
}
export default App;
```
## Options
The Link supports same-page or new-tab navigation and reads naturally either on its own or inline within body text.
### Target
```tsx lines theme={null}
import { Link } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* Same-page navigation (default) */}
{/* Opens in a new tab */}
);
}
export default App;
```
Omitting `target` navigates within the app. Set `target="_blank"` to open in a new tab — Link appends a trailing icon, defaults `rel` to `noopener noreferrer`, and adds an "(opens in a new tab)" announcement for screen readers.
### Inline
```tsx lines theme={null}
import { Link, Text } from "@servicetitan/carto-react-kit";
function App() {
return (
Review the before
continuing.
>
}
/>
);
}
export default App;
```
Link renders inline by default, so it flows naturally inside body copy built with `Text` or `Markdown`.
## Behavior
The Link responds to hover and keyboard focus, and wraps like any other inline text.
### Visual States
```tsx lines theme={null}
import { Link } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* Rest */}
{/* Hover */}
{/* Focus */}
);
}
export default App;
```
Hover shifts the link to the informative (blue) color while the underline stays as the resting affordance. Keyboard focus draws a ring around the whole link and removes the underline, since the ring itself becomes the focus affordance.
### Wrapping
```tsx lines theme={null}
import { Link } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
Long link text wraps naturally within its container, the same as any other inline text.
## Keyboard Interaction
Users can navigate and activate the Link using standard keyboard controls.
| Key | Description |
| ----- | ----------------------------------- |
| Tab | Moves focus to the link |
| Enter | Activates the focused link's `href` |
# Markdown – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/markdown/design
Markdown renders formatted text content from agent responses.
```tsx lines theme={null}
import { Markdown } from "@servicetitan/carto-react-kit";
const source = [
"# Page title",
"",
"A paragraph with **bold**, *emphasis*, and a [link](https://example.com).",
"",
"Inline `code` sits between words.",
].join("\n");
function App() {
return (
);
}
export default App;
```
## Options
Markdown renders GitHub Flavored Markdown source into Carto-styled HTML — headings, inline formatting, lists, code, quotes, and tables.
### Basic text
```tsx lines theme={null}
import { Markdown } from "@servicetitan/carto-react-kit";
const source = [
"Plain paragraph text.",
"",
"**Bold text** emphasizes a word or phrase.",
"",
"*Italicized text* adds a lighter emphasis.",
"",
"~~Strikethrough text~~ marks something as removed or done.",
].join("\n");
function App() {
return (
);
}
export default App;
```
Bold (`**text**`) and italic (`*text*`) are standard CommonMark. Standard markdown has no distinct underline syntax, so strikethrough (`~~text~~`, GitHub Flavored Markdown) is the closest additional emphasis available.
### Headings
```tsx lines theme={null}
import { Markdown } from "@servicetitan/carto-react-kit";
const source = [
"# Heading 1",
"## Heading 2",
"### Heading 3",
"#### Heading 4",
].join("\n");
function App() {
return (
);
}
export default App;
```
Four heading levels map to `Text`'s headline sizes: `h1` renders `hero`, `h2` renders `major`, `h3` renders `substantial`, and `h4` renders `regular`. `h5` and `h6` also render but reuse the `regular` size, relying on the semantic tag alone to signal depth.
### Inline Formatting
```tsx lines theme={null}
import { Markdown } from "@servicetitan/carto-react-kit";
const source =
"A paragraph with **bold**, *emphasis*, `inline code`, and a [link](https://example.com).";
function App() {
return (
);
}
export default App;
```
Bold, italic, inline code, and links combine freely within a single line of prose.
### Lists
```tsx lines theme={null}
import { Markdown } from "@servicetitan/carto-react-kit";
const source = [
"- First item",
"- Second item",
" - Nested item",
"",
"1. First step",
"2. Second step",
"",
"- [x] Completed task",
"- [ ] Open task",
].join("\n");
function App() {
return (
);
}
export default App;
```
Unordered and ordered lists nest to any depth. GitHub Flavored Markdown task lists (`- [x]` / `- [ ]`) render as read-only checkboxes reflecting each item's completion — they aren't interactive.
### Code
````tsx lines theme={null}
import { Markdown } from "@servicetitan/carto-react-kit";
const source = [
"Inline `code` sits between words.",
"",
"```ts",
"function greet(name: string) {",
" return `Hello, ${name}!`;",
"}",
"```",
].join("\n");
function App() {
return (
);
}
export default App;
````
Inline code uses a tile background. Fenced code blocks (` ``` `) render in a bordered surface; Markdown doesn't syntax-highlight by language.
### Quotes
```tsx lines theme={null}
import { Markdown } from "@servicetitan/carto-react-kit";
const source = [
"> A blockquote sets quoted text apart from the surrounding prose.",
"",
"---",
"",
"A horizontal rule separates unrelated sections of content.",
].join("\n");
function App() {
return (
);
}
export default App;
```
A blockquote (`>`) indents quoted prose with a leading rule. A horizontal rule (`---`) separates unrelated sections of content.
### Tables
```tsx lines theme={null}
import { Markdown } from "@servicetitan/carto-react-kit";
const source = [
"| Invoice | Status |",
"| ------- | ------ |",
"| #1024 | Paid |",
"| #1025 | Overdue |",
].join("\n");
function App() {
return (
);
}
export default App;
```
Tables (GitHub Flavored Markdown) render with a bordered header row and body rows below it.
## Behavior
Markdown is presentational — the only interactive element it can render is a link, and task-list checkboxes are read-only.
## Keyboard Interaction
Users can navigate and activate any link rendered within the Markdown using standard keyboard controls.
| Key | Description |
| ----- | ----------------------------------- |
| Tab | Moves focus to the next link |
| Enter | Activates the focused link's `href` |
# Menu – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/menu/design
Menus present a list of actions or options from a trigger.
```tsx lines theme={null}
import { Button, Menu } from "@servicetitan/carto-react-kit";
function App() {
return (
}
items={[
{ id: "rename", label: "Rename" },
{ id: "duplicate", label: "Duplicate" },
{ id: "archive", label: "Move to archive" },
{ id: "delete", label: "Delete" },
]}
defaultOpen
/>
);
}
export default App;
```
## Options
The Menu supports an action mode and two persisted selection modes, leading icons, disabled options, and either edge of its trigger for the popover.
### Selection Mode
`selectionMode` switches the menu between three behaviors: `"none"` (the default) is a plain action menu; `"single"` and `"multiple"` persist a checked selection instead of closing on pick.
#### Single Select
```tsx lines theme={null}
import { Button, Menu } from "@servicetitan/carto-react-kit";
function App() {
return (
}
items={[
{ id: "name", label: "Name" },
{ id: "modified", label: "Last modified" },
{ id: "created", label: "Date created" },
{ id: "size", label: "Size" },
]}
selectionMode="single"
defaultSelectedKeys={["modified"]}
defaultOpen
/>
);
}
export default App;
```
`selectionMode="single"` behaves like a "sort by" control: picking an option checks it, unchecks any other, and closes the menu to confirm the single choice.
#### Multi Select
```tsx lines theme={null}
import { Button, Menu } from "@servicetitan/carto-react-kit";
function App() {
return (
}
items={[
{ id: "name", label: "Name" },
{ id: "modified", label: "Last modified" },
{ id: "created", label: "Date created" },
{ id: "size", label: "Size" },
]}
selectionMode="multiple"
defaultSelectedKeys={["name", "size"]}
defaultOpen
/>
);
}
export default App;
```
`selectionMode="multiple"` lets several options stay checked at once, and the menu stays open after a pick so more can be toggled.
### Action Menu
The hero example above is an action menu — `selectionMode`'s default, `"none"`. Each option runs a command via `onAction` and the menu closes immediately on pick. Action menus and the selection modes render identically at rest; the difference is entirely behavioral — whether picking an option persists a checked state or fires a one-off command.
### Icons
```tsx lines theme={null}
import { Button, Menu } from "@servicetitan/carto-react-kit";
import {
IconCopy,
IconInbox,
IconPencil,
} from "@servicetitan/carto-react-kit/icons";
function App() {
return (
}
items={[
{ id: "rename", label: "Rename", icon: },
{ id: "duplicate", label: "Duplicate", icon: },
{ id: "archive", label: "Move to archive", icon: },
]}
defaultOpen
/>
);
}
export default App;
```
An optional leading icon renders at 16px before the label. Icons are decorative and don't contribute to the option's accessible name.
### Disabled Options
```tsx lines theme={null}
import { Button, Menu } from "@servicetitan/carto-react-kit";
function App() {
return (
}
selectionMode="multiple"
defaultSelectedKeys={["archive"]}
items={[
{ id: "rename", label: "Rename" },
{ id: "duplicate", label: "Duplicate", isDisabled: true },
{ id: "archive", label: "Move to archive", isDisabled: true },
{ id: "delete", label: "Delete" },
]}
defaultOpen
/>
);
}
export default App;
```
A disabled option can't be focused, selected, or activated, and renders dimmed — including an option that is already selected; the dimmed check still shows its state.
### Placement
```tsx lines theme={null}
import { Button, Menu } from "@servicetitan/carto-react-kit";
function App() {
return (
}
items={[
{ id: "rename", label: "Rename" },
{ id: "duplicate", label: "Duplicate" },
{ id: "delete", label: "Delete" },
]}
placement="top"
defaultOpen
/>
);
}
export default App;
```
`placement` opens the popover on the `"bottom"` (default, shown in the hero example above) or `"top"` edge of the trigger, with the same 4px gap either way.
## Behavior
The popover hugs its content between a 180px floor and a 360px ceiling, and every option responds to hover, focus, press, and disabled states.
### Visual States
```tsx lines theme={null}
import { Button, Menu } from "@servicetitan/carto-react-kit";
function App() {
return (
}
selectionMode="multiple"
defaultSelectedKeys={["selected", "selected-disabled"]}
items={[
{ id: "default", label: "Default" },
{ id: "hover", label: "Hover target" },
{ id: "focus", label: "Focus target" },
{ id: "disabled", label: "Disabled", isDisabled: true },
{ id: "selected", label: "Selected" },
{
id: "selected-disabled",
label: "Selected, disabled",
isDisabled: true,
},
]}
defaultOpen
/>
);
}
export default App;
```
Unselected options stay neutral — a subtle gray fill on hover and press, with unchanged text. Selected options use the informative-blue family instead. Keyboard focus draws a ring only, with no background fill. Disabled options render dimmed regardless of selection.
### Wrapping
```tsx lines theme={null}
import { Button, Menu } from "@servicetitan/carto-react-kit";
function App() {
return (
}
items={[
{ id: "short", label: "Rename" },
{
id: "long",
label:
"Move this item to a different project workspace and notify collaborators",
},
{ id: "delete", label: "Delete" },
]}
defaultOpen
/>
);
}
export default App;
```
Labels wrap to multiple lines once the popover reaches its 360px max-width, rather than growing the popover without bound.
### Scrolling List
```tsx lines theme={null}
import { Button, Menu } from "@servicetitan/carto-react-kit";
function App() {
return (
}
items={Array.from({ length: 20 }, (_, index) => ({
id: `option-${index}`,
label: `Option ${index + 1}`,
}))}
defaultOpen
/>
);
}
export default App;
```
Enough options to exceed the popover's available height scroll internally; the scroll region shares the popover's corner radius so its scrollbar stays clipped to the rounded shape.
## Keyboard Interaction
Users can open, navigate, and act on the Menu using standard keyboard controls.
| Key | Description |
| -------------------------- | ------------------------------------------------------------------- |
| Enter / Space / Down Arrow | Opens the menu from the trigger, focusing the first option |
| Up / Down Arrow | Moves focus between options |
| Typeahead | Jumps focus to the next option starting with the typed character(s) |
| Enter / Space | Activates or toggles the focused option |
| Escape | Closes the menu and returns focus to the trigger |
# Components
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/overview
Components are reusable building blocks of the UI, crafted from Foundations.
# PromptBar – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/prompt-bar/design
Prompt bars let users compose and submit prompts to an agent.
```tsx lines theme={null}
import { useState } from "react";
import { PromptBar } from "@servicetitan/carto-react-kit";
function App() {
const [value, setValue] = useState("");
return (
);
}
export default App;
```
## Options
PromptBar is controlled — the parent owns `value`, `attachments`, and the `transcription` feed — and derives its mode from them. It supports an attachment row, a voice-transcription entry point, and a customizable disclaimer line.
### With Text
```tsx lines theme={null}
import { useState } from "react";
import { PromptBar } from "@servicetitan/carto-react-kit";
function App() {
const [value, setValue] = useState(
"What's the weather like in San Francisco today?",
);
return {}} />;
}
export default App;
```
Once there's text, the editor rises to its own row above the controls, and the primary action switches from disabled to send.
### Attachments
```tsx lines theme={null}
import { useState } from "react";
import { PromptBar } from "@servicetitan/carto-react-kit";
function App() {
const [value, setValue] = useState("");
const [attachments, setAttachments] = useState([
{ id: "1", name: "invoice.pdf" },
{ id: "2", name: "site-photo.jpg" },
]);
return (
{}}
onRemoveAttachment={(id) =>
setAttachments((current) =>
current.filter((attachment) => attachment.id !== id),
)
}
/>
);
}
export default App;
```
Attachments render as thumbnail tiles in a row above the editor; adding one expands the row and reflows the bar. A tile without a `thumbnailUrl` falls back to showing the file name.
### Transcribing
```tsx lines theme={null}
import { useState } from "react";
import { PromptBar } from "@servicetitan/carto-react-kit";
function App() {
const [value, setValue] = useState("");
const [draftValue, setDraftValue] = useState(
"What's the weather like in San Francisco today?",
);
return (
{/* Active transcription with a live waveform */}
{}}
onConfirmTranscription={() => {}}
/>
{/* Text in the prompt — no transcription active */}
{}}
/>
{/* Transcription error */}
{}}
onConfirmTranscription={() => {}}
/>
);
}
export default App;
```
`transcription.active` switches the bar into a distinct mode: the attach and mic controls are replaced by a Cancel button, an animated waveform, and a Done primary action. Prefer wiring real audio into `transcription.source` (a `MediaStream`, an `AnalyserNode`, or a computed amplitude); fall back to a named preset (`ambient` / `speaking` / `thinking`) only when none is available. Setting `transcription.error` instead swaps in an error layout — an error icon, the message, and Done dismisses the error and exits.
### Disclaimer
```tsx lines theme={null}
import { useState } from "react";
import { PromptBar } from "@servicetitan/carto-react-kit";
function App() {
const [value, setValue] = useState("");
return (
);
}
export default App;
```
A disclaimer line sits below the bar, with a "Learn more" link out to more information.
### Disabled
```tsx lines theme={null}
import { PromptBar } from "@servicetitan/carto-react-kit";
function App() {
return (
{}}
disabled
/>
);
}
export default App;
```
`disabled` dims the whole bar and blocks the editor, attachments, and every action.
## Behavior
The editor grows with its content, and the bar reflows as attachments and text come and go.
### Text Wrapping
```tsx lines theme={null}
import { useState } from "react";
import { PromptBar } from "@servicetitan/carto-react-kit";
function App() {
const [value, setValue] = useState(
"Can you help me draft a detailed message to the customer explaining the delay in their service appointment, the reason for the delay, and what we're doing to make it right? Please keep the tone apologetic but professional.",
);
return {}} />;
}
export default App;
```
Once there's text, the editor lifts to its own row above the controls and wraps across multiple lines, growing up to 8 rows before it scrolls internally — it never truncates or clips.
## Keyboard Interaction
Users can compose, send, and control transcription using standard keyboard controls.
| Key | Description |
| -------------------------- | --------------------------------------------------------------------------- |
| Enter | Sends the draft (draft mode) or confirms the transcript (transcribing mode) |
| Shift / Ctrl / Cmd + Enter | Inserts a newline in the editor instead of sending |
| Escape | Cancels transcription and returns to the editor |
| Tab | Moves focus among the attach, editor, mic, and primary controls |
# Suggestion – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/suggestion/design
Suggestions offer quick follow-up actions or prompts.
```tsx lines theme={null}
import { Suggestion } from "@servicetitan/carto-react-kit";
function App() {
return ;
}
export default App;
```
## Options
A single chip is one tappable follow-up; `SuggestionList` groups a capped set of chips (design caps at 3, \~40 characters each) beneath an agent response.
### List of Suggestions
```tsx lines theme={null}
import { SuggestionList } from "@servicetitan/carto-react-kit";
function App() {
return (
{}}
/>
);
}
export default App;
```
`SuggestionList` renders one chip per entry in `suggestions` and exposes the set to assistive tech as one labelled group — "Suggested replies" by default. Selecting a chip fires `onSelect` with that suggestion's text and index; clearing the list and inserting the text into the message input is the consumer's job.
### Disabled
```tsx lines theme={null}
import { Suggestion } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
`isDisabled` dims the chip and blocks press and hover, but keeps it focusable for keyboard users.
## Behavior
The chip responds to hover, press, and keyboard focus, and a list of chips wraps to fit its container.
### Visual States
```tsx lines theme={null}
import { Suggestion } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
Hover and press both deepen the chip's translucent fill; keyboard focus adds a ring without changing the fill.
### Wrapping
```tsx lines theme={null}
import { SuggestionList } from "@servicetitan/carto-react-kit";
function App() {
return (
{}}
/>
);
}
export default App;
```
`SuggestionList` wraps chips onto new lines once a row runs out of width, rather than overflowing or shrinking the chips.
## Keyboard Interaction
Users can navigate and select a Suggestion using standard keyboard controls.
| Key | Description |
| ------------- | ---------------------------- |
| Tab | Moves focus to the next chip |
| Enter / Space | Activates the focused chip |
# TabbedLayout – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/tabbed-layout/design
Tabbed layouts organize related views into selectable tabs.
```tsx lines theme={null}
import { TabbedLayout, Text } from "@servicetitan/carto-react-kit";
import type { TabbedLayoutTab } from "@servicetitan/carto-react-kit";
const tabs: TabbedLayoutTab[] = [
{
key: "tab-a",
label: "Tab Name A",
content: ,
},
{
key: "tab-b",
label: "Tab Name B",
content: ,
},
];
function App() {
return (
);
}
export default App;
```
## Options
TabbedLayout holds one or more tabs and arranges them either as a tab strip or two simultaneously-visible panes.
### Display Modes
```tsx lines theme={null}
import { CartoTheme, TabbedLayout, Text } from "@servicetitan/carto-react-kit";
import type {
TabbedLayoutMode,
TabbedLayoutTab,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const tabs: TabbedLayoutTab[] = [
{
key: "tab-a",
label: "Tab Name A",
content: ,
},
{
key: "tab-b",
label: "Tab Name B",
content: ,
},
];
function Labeled({ label, mode }: { label: string; mode: TabbedLayoutMode }) {
return (
);
}
function App() {
return (
);
}
export default App;
```
#### Stacked
The classic tabs interaction: a tab strip with one panel visible at a time. Always available, and the mode every layout falls back to below 720px or with anything other than exactly two tabs.
#### Vertical Split
Both panels shown side by side, divided by a vertical rule. Offered only with exactly two tabs, at ≥ 720px.
#### Horizontal Split
Both panels stacked top and bottom, divided by a horizontal rule. Same two-tab, ≥ 720px constraint as Vertical Split.
### Narrow
```tsx lines theme={null}
import { CartoTheme, TabbedLayout, Text } from "@servicetitan/carto-react-kit";
import type { TabbedLayoutTab } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const tabs: TabbedLayoutTab[] = [
{
key: "tab-a",
label: "Tab Name A",
content: ,
},
{
key: "tab-b",
label: "Tab Name B",
content: ,
},
];
function App() {
return (
);
}
export default App;
```
Below 720px — measured on the layout's own width, not the viewport — the layout is forced to Stacked and the mode menu hides, regardless of `defaultMode`. The last chosen mode is remembered and restored once it grows back past the threshold.
### Single Tab
```tsx lines theme={null}
import { CartoTheme, TabbedLayout, Text } from "@servicetitan/carto-react-kit";
import type { TabbedLayoutTab } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const tabs: TabbedLayoutTab[] = [
{
key: "preview",
label: "Preview",
content: ,
},
];
function App() {
return (
);
}
export default App;
```
A single tab shows neither a tab strip nor the mode menu — there's nothing to switch between or arrange, so the layout renders just that tab's label heading and content.
## Behavior
The layout adapts to its own measured width and lets the user resize or rearrange its panes.
### Mode Picker
```tsx lines theme={null}
import { CartoTheme, TabbedLayout, Text } from "@servicetitan/carto-react-kit";
import type { TabbedLayoutTab } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const tabs: TabbedLayoutTab[] = [
{
key: "tab-a",
label: "Tab Name A",
content: ,
},
{
key: "tab-b",
label: "Tab Name B",
content: ,
},
];
function App() {
return (
);
}
export default App;
```
The top-right options menu lets the user pick a display mode — shown only when a split is currently possible (≥ 720px, exactly two tabs). The current mode carries a check. When the layout is the artifact drawer, a close button joins the same corner and dismisses the drawer via the app surface.
### Split Resize
```tsx lines theme={null}
import { CartoTheme, TabbedLayout, Text } from "@servicetitan/carto-react-kit";
import type { TabbedLayoutTab } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const tabs: TabbedLayoutTab[] = [
{
key: "tab-a",
label: "Tab Name A",
content: ,
},
{
key: "tab-b",
label: "Tab Name B",
content: ,
},
];
function App() {
return (
);
}
export default App;
```
Drag the separator between the two panes (or focus it and use the arrow keys) to resize them. Both panes are clamped to a per-axis minimum, so neither can be dragged small enough to lose its heading and all its content.
### Overflow Handling
```tsx lines theme={null}
import { CartoTheme, TabbedLayout, Text } from "@servicetitan/carto-react-kit";
import type { TabbedLayoutTab } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const tabs: TabbedLayoutTab[] = [
{
key: "doc",
label: "Document",
content: (
<>
{Array.from({ length: 12 }, (_, i) => (
Paragraph {i + 1}. Overflowing content scrolls within the panel's
padding, not flush to its edge.
))}
>
),
},
{ key: "notes", label: "Notes", content: },
];
function App() {
return (
);
}
export default App;
```
Content taller than a panel scrolls inside its padding — the scroll region is inset, so the scrollbar and the scrolled content stop at the padded edge rather than running flush to the panel's border.
## Keyboard Interaction
Users can switch tabs, resize a split, and choose a display mode using standard keyboard controls.
| Key | Description |
| ------------------------ | ---------------------------------------------------- |
| Tab | Moves focus among the tab strip, panel, and controls |
| Arrow Left / Arrow Right | Moves focus to the previous/next tab (Stacked) |
| Arrow keys | Resizes the split when the separator is focused |
| Enter / Space | Activates the focused tab or control |
| Escape | Closes the layout options menu |
## Patterns that use this component
* [Agentic Experience Shell](/docs/kits/ai-kit/patterns/agentic-experience-shell) — Organizes multiple related views within the supporting workspace
# Text – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/text/design
Text displays typographic content with Carto styles.
```tsx lines theme={null}
import { Text } from "@servicetitan/carto-react-kit";
function App() {
return ;
}
export default App;
```
## Options
Text supports three variants — headline, body, and eyebrow — each with its own size scale, plus a highlight treatment that works across all three.
### Headline
```tsx lines theme={null}
import { Text } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
`el` sets the rendered heading tag (`h1`–`h6`) and is required whenever `variant="headline"` — a headline always commits to a semantic level. `size` picks the visual scale independently of `el`, so a page can use `h2` for document structure while sizing it `hero` for emphasis.
| Size | Value |
| --------------- | ----- |
| Regular | 16px |
| Substantial | 20px |
| Major (default) | 24px |
| Hero | 32px |
### Body
```tsx lines theme={null}
import { Text } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
Body is the default variant — omit `variant` for a ``. `size` picks the scale.
| Size | Value |
| ----------------- | ----- |
| Fine | 12px |
| Compact | 14px |
| Regular (default) | 16px |
| Featured | 18px |
#### Inline
```tsx lines theme={null}
import { Text } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
`inline` renders body text as a `` instead of a ``, so a fragment composes on the same line as surrounding content — useful for nesting `Text` inside running copy or inside another `Text`.
### Eyebrow
```tsx lines theme={null}
import { Text } from "@servicetitan/carto-react-kit";
function App() {
return ;
}
export default App;
```
Eyebrow is a small, uppercased overline for section labels. It has a single fixed size (10px) — `size` isn't configurable on this variant.
### Highlighted
```tsx lines theme={null}
import { Text } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
`highlighted` applies the suggested-change treatment — a flat `blue-50` background — on any variant, size, or inline fragment. It's the same treatment `DiffValue` and `TruncatedText` use for a revised value.
## Behavior
Text is presentational — it renders no interactive states of its own. Content wraps within its container by default; for line-clamped body copy with a Show more / Show less control, use `TruncatedText` instead.
## Keyboard Interaction
Text renders no focusable elements of its own. When `text` composes an interactive child — a Carto `Link`, for example — that child's own keyboard interaction applies.
# TruncatedText – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/truncated-text/design
Truncated text shortens long strings while keeping content accessible.
```tsx lines theme={null}
import { TruncatedText } from "@servicetitan/carto-react-kit";
const longText =
"The AI Adoption Maturity Model is a framework for assessing the ability of an organization to perform and sustain specific technical practices to achieve two key, high-level goals. It helps teams identify gaps, prioritize investments, and track progress over time. Maturity levels run from ad hoc experimentation to scaled deployment.";
function App() {
return (
);
}
export default App;
```
## Options
TruncatedText renders body text through `Text`, so it inherits its size scale and highlight treatment, and adds line truncation on top.
### Sizes
```tsx lines theme={null}
import { TruncatedText } from "@servicetitan/carto-react-kit";
const longText =
"The AI Adoption Maturity Model is a framework for assessing the ability of an organization to perform and sustain specific technical practices to achieve two key, high-level goals. This is a long text that should be truncated. We should be able to see the full text, but it should be truncated at the end of the line.";
function App() {
return (
);
}
export default App;
```
`size` sets the body scale the text renders at. A smaller size fits more characters per line, so "Show more" may appear at a different point for the same copy.
### Truncate Line Length
```tsx lines theme={null}
import { TruncatedText } from "@servicetitan/carto-react-kit";
const longText =
"The AI Adoption Maturity Model is a framework for assessing the ability of an organization to perform and sustain specific technical practices to achieve two key, high-level goals. It helps teams identify gaps, prioritize investments, and track progress over time.";
function App() {
return (
{/* truncateLines={1} */}
{/* truncateLines={3} */}
{/* truncateLines={0} — truncation off */}
);
}
export default App;
```
`truncateLines` sets how many lines show before "Show more" appears; it defaults to `2`. Pass `0` to turn truncation off entirely.
### Highlighted
```tsx lines theme={null}
import { TruncatedText } from "@servicetitan/carto-react-kit";
const longText =
"The AI Adoption Maturity Model is a framework for assessing the ability of an organization to perform and sustain specific technical practices to achieve two key, high-level goals.";
function App() {
return (
{/* Default */}
{/* Highlighted */}
);
}
export default App;
```
`highlighted` applies the suggested-change treatment — a flat `blue-50` background — matching a `DiffValue` revised value in a block diff.
## Behavior
The full text always stays in the DOM; only its visual clipping toggles.
### Expand and Collapse
```tsx lines theme={null}
import { TruncatedText } from "@servicetitan/carto-react-kit";
const longText =
"The AI Adoption Maturity Model is a framework for assessing the ability of an organization to perform and sustain specific technical practices to achieve two key, high-level goals. It helps teams identify gaps, prioritize investments, and track progress over time.";
function App() {
return (
{/* Collapsed */}
{/* Expanded */}
);
}
export default App;
```
"Show more" expands the full text in place and moves focus to "Show less"; collapsing returns focus to "Show more". Because the complete text remains in the DOM regardless of state, screen readers and find-in-page can always reach it.
## Keyboard Interaction
Users can toggle the expanded state using standard keyboard controls.
| Key | Description |
| ------------- | -------------------------------------------- |
| Tab | Moves focus to "Show more" or "Show less" |
| Enter / Space | Toggles the focused control's expanded state |
# UserMessage – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/user-message/design
User messages display content sent by the user in a conversation.
```tsx lines theme={null}
import { UserMessage } from "@servicetitan/carto-react-kit";
function App() {
return (
);
}
export default App;
```
## Options
UserMessage supports arbitrary content and an optional error line with a retry action.
### Message Content
```tsx lines theme={null}
import { UserMessage } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* Short */}
{/* Long — wraps at the 640px cap */}
);
}
export default App;
```
The bubble caps at 640px and wraps long content; short messages stay compact. `content` accepts any `ReactNode` — a plain string, inline composition (Carto `Link`, `strong`, etc.), or a rendered Markdown tree.
### Error
```tsx lines theme={null}
import { UserMessage } from "@servicetitan/carto-react-kit";
function App() {
return (
{}}
timestamp={new Date(2026, 3, 1, 16, 40)}
/>
);
}
export default App;
```
`error` renders a message below the bubble. Pair it with `onRetry` to add a retry action to the toolbar — the button appears only when both are set.
## Behavior
The toolbar's actions and visibility both respond to the message's content and its position in the conversation.
### Content Length
The bubble never truncates or clips its content — it only wraps. There's no maximum height or line count; a long message simply grows taller.
### Toolbar Actions
```tsx lines theme={null}
import { UserMessage } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* Copy only */}
{/* Copy + retry */}
{}}
timestamp={new Date(2026, 3, 1, 16, 41)}
/>
);
}
export default App;
```
The toolbar always shows the timestamp, plus a copy action whenever there's text to copy; retry joins them only when `error` is also set. Copy is text-only — pass `copyText` for rich content (Markdown, fragments, images) to keep the message copyable; a message with no text shows no copy button.
### Reveal Toolbar on Hover
```tsx lines theme={null}
import { UserMessage } from "@servicetitan/carto-react-kit";
function App() {
return (
{/* Earlier message — toolbar hidden until hover or focus (pinned to its hover state here) */}
{/* Latest message — toolbar always visible */}
);
}
export default App;
```
Set `revealToolbarOnHover` on every message except the most recent of its type, so only the latest keeps its toolbar always visible — earlier messages reveal theirs on hover or keyboard focus. This distinction doesn't apply on touch (coarse-pointer) devices, which always show the toolbar regardless of `revealToolbarOnHover`.
## Keyboard Interaction
Users can navigate and activate toolbar actions using standard keyboard controls; focus also reveals a hidden toolbar.
| Key | Description |
| ------------- | ---------------------------------------------------- |
| Tab | Moves focus into the toolbar, revealing it if hidden |
| Arrow keys | Moves focus between toolbar actions |
| Enter / Space | Activates the focused action |
# Window – Design
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/components/window/design
Windows present contained, moveable or framed content regions.
```tsx lines theme={null}
import { Text, Window } from "@servicetitan/carto-react-kit";
function App() {
return (
}
/>
);
}
export default App;
```
## Options
Window owns the same slots as [AppShell](/docs/kits/ai-kit/components/app-shell/design) — `header` and `chat` (shown in both states), plus `rail` and `artifact` (fullscreen only) — and presents them in one of two sizes.
### Window States
```tsx lines theme={null}
import {
AppHeader,
CartoTheme,
Text,
Window,
useAppSurface,
} from "@servicetitan/carto-react-kit";
import { IconMinimize2, IconX } from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
function WindowHeader() {
const { toggleSize, close } = useAppSurface();
return (
,
label: "Restore",
onPress: toggleSize,
},
{ id: "close", icon: , label: "Close", onPress: close },
]}
/>
);
}
function App() {
return (
);
}
export default App;
```
**Docked** (shown above) is a fixed-size floating panel — header + chat, anchored bottom-right by default — that sits non-modally over the rest of the page. **Fullscreen** (shown here) is a modal takeover revealing the full `AppShell`: header, rail, chat, and artifact, over a scrim, with focus trapped inside. The `header` and `chat` nodes are the same React instance in both states — same DOM, scroll position, and in-flight state — so expanding or restoring never resets the conversation.
### Open Control
```tsx lines theme={null}
import { useRef } from "react";
import {
AppHeader,
Button,
CartoTheme,
Text,
Window,
useAppSurface,
} from "@servicetitan/carto-react-kit";
import type { AppSurfaceHandle } from "@servicetitan/carto-react-kit";
import { IconMaximize2, IconX } from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
function WindowHeader() {
const { toggleSize, close } = useAppSurface();
return (
,
label: "Maximize",
onPress: toggleSize,
},
{ id: "close", icon: , label: "Close", onPress: close },
]}
/>
);
}
function App() {
const ctrl = useRef(null);
return (
ctrl.current?.open()} />
}
chat={
}
/>
);
}
export default App;
```
Open/close and docked/fullscreen work uncontrolled by default (seeded by `defaultOpen` / `defaultState`), or controlled by passing `open` / `state` alongside `onOpenChange` / `onStateChange` so an external store can be the source of truth. Either way, drive the same transitions imperatively via the `AppSurfaceHandle` — from a parent through `controlRef` (as shown here, with an external launcher button), or from inside the window (a header button, or a card in the chat) via `useAppSurface()`.
## Behavior
Both states share one surface, so the panel animates between them, and the docked panel can be repositioned.
### Maximize and Restore
```tsx lines theme={null}
import {
AppHeader,
CartoTheme,
Text,
Window,
useAppSurface,
} from "@servicetitan/carto-react-kit";
import { IconMaximize2, IconX } from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
function WindowHeader() {
const { toggleSize, close } = useAppSurface();
return (
,
label: "Maximize",
onPress: toggleSize,
},
{ id: "close", icon: , label: "Close", onPress: close },
]}
/>
);
}
function App() {
return (
);
}
export default App;
```
The header's maximize control expands the docked panel into the fullscreen modal shell; restore (or Esc) shrinks it back. Docked and fullscreen share one surface, so the panel visibly grows and shrinks between the two rects rather than swapping instantly.
### Drag
```tsx lines theme={null}
import {
AppHeader,
CartoTheme,
Text,
Window,
useAppSurface,
} from "@servicetitan/carto-react-kit";
import {
IconGrip,
IconMaximize2,
IconX,
} from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
// A marker at the panel's default anchor (bottom-right, 24px inset — matching
// the docked panel's own resting corner) so a screenshot of the dragged panel
// still shows where it started, proving it moved.
function DefaultCornerMarker() {
return (
);
}
function WindowHeader() {
const { toggleSize, close, dragHandleProps } = useAppSurface();
return (
,
label: "Move window",
dragHandleProps,
},
]}
trailingActions={[
{
id: "size",
icon: ,
label: "Maximize",
onPress: toggleSize,
},
{ id: "close", icon: , label: "Close", onPress: close },
]}
/>
);
}
function App() {
return (
);
}
export default App;
```
Docked, the panel is draggable: spread the surface's `dragHandleProps` (from `controlRef` / `useAppSurface()`) onto a leading header action — here an `IconGrip` — to turn it into a grip that moves the panel anywhere in the viewport. Arrow keys nudge it too, for keyboard users. The dragged position survives a maximize→restore round-trip (restore returns to it) and resets to the default corner only on close.
### Scrim Dismiss
Fullscreen is modal: a scrim sits behind the shell and dismisses back to docked on click, the same as pressing Esc. Because the shell fills the viewport, the scrim itself has no visible edge — the affordance is the dismissal, not a visible backdrop.
## Keyboard Interaction
Users can maximize, restore, move, and dismiss a window using standard keyboard controls.
| Key | Description |
| ------------- | ------------------------------------------------------ |
| Tab | Moves focus among header actions and chat content |
| Enter / Space | Activates the focused action |
| Arrow keys | Moves the docked panel when the drag handle is focused |
| Escape | Restores a fullscreen window to docked |
## Patterns that use this component
* [Agentic Experience Shell](/docs/kits/ai-kit/patterns/agentic-experience-shell) — Presents the shell as a floating or fullscreen experience
# Getting Started
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/getting-started
Learn how to build agentic experiences with Anvil2 AI Kit and the Carto theme.
Anvil2 AI Kit provides guidance, patterns, and components for building agentic product experiences. Carto is the visual theme AI Kit uses to give agentic surfaces a consistent identity across web and mobile.
Use this guide to understand when to use AI Kit, how it relates to the rest of Anvil2, and how to get started.
## What is "agentic"?
An agentic experience uses AI to act on your behalf, not just to advise you. Four things define it:
* **Context**: the system understands what's relevant.
* **Reasoning**: it makes thoughtful, adaptive decisions.
* **Conversation**: it explains what it's doing.
* **Action**: it takes meaningful steps for you, not just suggestions you have to act on yourself.
Compare that to non-agentic AI, which stays advisory: it surfaces insights and recommendations, but a person still has to do the work.
## Overview
AI Kit is part of Anvil2. It extends the Anvil2 ecosystem with design guidance and building blocks for agentic experiences, including chat, generative UI, panels, action surfaces, and agent-mediated flows.
Carto provides the visual layer for AI Kit through its typography, iconography, semantic tokens, component tokens, and component styling. Core Anvil2 continues to support standard product surfaces, while AI Kit applies Carto to agentic surfaces within AgentOS.
### When do I use AI Kit?
Use AI Kit when building an agentic surface within AgentOS.
Do not apply the Carto theme to an entire screen solely because it contains an AI-powered feature. When an agentic element appears within a core Anvil2 surface, use the defined integration patterns for that element instead of applying AI Kit components or the Carto theme to the surrounding product UI.
### How does AI Kit relate to Anvil2?
AI Kit belongs to the Anvil2 design system and specializes it for agentic product experiences. Use core Anvil2 components and guidance for standard product UI. Use AI Kit when a surface requires agentic interaction patterns, AI-specific components, or the Carto visual treatment.
The Carto theme gives agentic surfaces a distinct visual identity while keeping their implementation within the Anvil2 ecosystem.
Beyond the Carto theme, AI Kit uses agent-friendly APIs to improve
consistency. Its components favor prop-driven interfaces over compositions of
subcomponents, limit reliance on open-ended `children` props, and use a
tighter token system to keep web and mobile libraries aligned automatically.
Over time, these API improvements will be incorporated into the core Anvil2
library.
### How is this different from Atlas v1?
Atlas v1 was the previous iteration of ServiceTitan's agentic chat experience. It was built with [`ext-atlas`](/docs/kits/atlas), Anvil2's extended Atlas component library, and focused on chat-based interactions.
Atlas today uses AI Kit and the Carto theme to support agentic experiences beyond chat. `ext-atlas` supports existing Atlas v1 integrations and is deprecated for new Atlas experiences.
## How do I start?
### Design assets
1. Get familiar with what's covered above: when to use AI Kit, how it relates to Anvil2, and how Carto styles agentic surfaces.
2. Explore the component libraries in Figma:
* [Web Components](https://www.figma.com/design/QEIsArLktLeJAaFvcpw3PO/Carto-Web-Components?m=auto\&node-id=505-2597\&t=95kmwGT5HM0WAnlp-1)
* [React Native Components](https://www.figma.com/design/gjGWnAOnI1kukfzjsUQ6dP/Mobile-Agentic-Component-Library?m=auto\&node-id=1-20524\&t=TomzmvO7I642gBup-1)
* [Design Tokens and Iconography](https://www.figma.com/design/6WeJz8JMm1in6Wb4g0wLP5/Carto-Design-Tokens?m=auto\&node-id=105-264\&t=aYAxxbtkRw4ttuUB-1)
3. Explore the [AI Kit component Storybook](https://design-platform-docs.vercel.app/carto/react-kit/).
4. Have questions on agentic patterns? Ask in [#support-agentic-design](https://servicetitan.enterprise.slack.com/archives/C08PPU97WJG). Have questions about components or implementation? Ask in [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS). Since the system and its conventions are still forming, a quick check-in helps you avoid rework as things solidify.
### Engineering assets
Use `@servicetitan/carto-react-kit` to implement AI Kit experiences on the web. It provides chat shells, prompts, artifacts, supporting primitives, and the Carto theme through a consistent React API.
Peer requirements: `react@>=18 <20`, `react-dom@>=18 <20`.
#### Storybook
Explore API documentation and interactive component examples.
AI Kit API documentation will move to this site soon.
#### Install
```bash theme={null}
pnpm add @servicetitan/carto-react-kit
```
Or with npm:
```bash theme={null}
npm install --save @servicetitan/carto-react-kit
```
#### Styles
Import the CSS bundle once at your application entry point so vanilla-extract styles load:
```tsx theme={null}
import "@servicetitan/carto-react-kit/styles.css";
```
#### Theme
Wrap your AI Kit tree in `CartoTheme` so Carto design tokens inherit to every descendant, including components rendered inside MFE shadow roots:
```tsx theme={null}
import { CartoTheme } from "@servicetitan/carto-react-kit";
export function Root({ children }) {
return {children} ;
}
```
Overlays from React Aria (Tooltip, Popover, Menu, Modal, Toast) portal into a sibling inside the theme wrapper rather than `document.body`, so token context stays intact.
#### Fonts
AgentOS pairs Sofia Pro for headers with Inter for body copy. This differs from the existing setup in Web, which pairs Sofia Pro with Nunito Sans.
Prefetch Inter in your document `
` for the fastest first paint. The kit also injects this link at module load as a safety net.
```html theme={null}
```
Sofia Pro requires no additional setup: the kit reuses the same Sofia Pro already loaded by your host application, the same font Web relies on for headers.
#### Quick start
```tsx theme={null}
import { Button, CartoTheme, Text } from "@servicetitan/carto-react-kit";
import { IconPencil } from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
export function App() {
return (
} onPress={() => {}} />
);
}
```
#### Icons
AI Kit uses [Lucide](https://lucide.dev/) icons (`lucide-react`) along with a small number of custom Carto SVGs. Import icons from the kit subpath, not directly from `lucide-react`:
```tsx theme={null}
import { IconPencil, IconPlus } from "@servicetitan/carto-react-kit/icons";
```
The icon allowlist is owned by VizD and curated inside the kit. Consumers cannot extend it from outside.
#### Internationalization
`CartoIntlProvider` is optional. Without it, components render English. Mount it inside `CartoTheme` to switch locale or override strings without colliding with your app’s react-intl context:
```tsx theme={null}
import { CartoTheme, CartoIntlProvider } from "@servicetitan/carto-react-kit";
export function Root({ children }) {
return (
{children}
);
}
```
### Resources
Learn what AI Kit covers and how its documentation is organized.
Explore core terms used across AI experiences and ServiceTitan.
Explore the visual foundations of AI Kit and the Carto theme.
Browse reusable AI Kit building blocks.
Read guidance for agentic product experiences.
# AI Kit
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/index
Design guidance and building blocks for building agentic AI experiences with the Anvil2 AI Kit.
AI Kit is the Anvil2 kit for building agentic product experiences. It provides visual foundations, patterns, and components tailored to AI-powered UX, with the Carto theme applied for agentic surfaces.
Carto is the theme for new and agentic surfaces in the ServiceTitan web and mobile apps, tied to AgentOS. It layers visual treatments on the core Anvil2 theme.
AI Kit and mobile Kits use Carto so those product areas share a consistent look for agentic experiences.
## Getting started
Start with the basics, then browse Carto assets as they become available.
Learn how to start building with Carto.
Shared vocabulary for Carto design and implementation.
## Browse Carto assets
Guidance for designing AI-powered experiences in Carto.
Visual foundations are the atomic elements that bring together a Carto UI.
Patterns are a collection of Components, arranged to achieve a user goal.
Components are reusable building blocks of the UI, crafted from Visual Foundations.
***
## Connect with us
You can reach the Anvil team in the #ask-designsystem Slack channel.
Join the Anvil team for office hours on Mondays or Wednesdays to get hands-on help.
Ran into a bug in the code or design? We use Jira to track outstanding
issues with Anvil.
Have an idea for a component, pattern, or documentation? Fill out the Feature Request Form in Slack.
View what is coming up this year for the design system.
Interested in contributing to Anvil? Read our contribution guide to learn
more!
# Agentic Experience Shell
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/patterns/agentic-experience-shell
Compose Carto components into a consistent shell for agentic experiences.
The agentic experience shell provides the persistent structure around a conversation. It defines navigation, presentation, and supporting workspaces without prescribing the content inside the conversation.
## Shell Options
The agent experience takes place in a shell with two shapes: the floating shell, and the fullscreen dialog shell experience.
### Floating shell
```tsx lines theme={null}
import { useState } from "react";
import {
AppHeader,
CartoTheme,
ChatHistory,
ChatLayout,
PromptBar,
TabbedLayout,
Text,
Window,
useAppSurface,
type ChatHistoryItem,
type TabbedLayoutTab,
} from "@servicetitan/carto-react-kit";
import {
IconGrip,
IconInbox,
IconMaximize2,
IconMinimize2,
IconPanelLeft,
IconPanelRight,
IconX,
} from "@servicetitan/carto-react-kit/icons";
import * as cartoTokens from "@servicetitan/carto-react-kit/tokens";
import "@servicetitan/carto-react-kit/styles.css";
const { primitive } =
cartoTokens as unknown as typeof import("@servicetitan/carto-tokens/vanilla-extract");
const historyItems: ChatHistoryItem[] = [
{ id: "current", label: "Current conversation", subtitle: "Just now" },
{ id: "previous", label: "Previous conversation", subtitle: "Yesterday" },
];
const artifactTabs: TabbedLayoutTab[] = [
{
key: "preview",
label: "Preview",
content: ,
},
];
function WindowHeader() {
const {
close,
dragHandleProps,
isFullscreen,
toggleArtifact,
toggleRail,
toggleSize,
} = useAppSurface();
return (
,
label: "Toggle chat history",
onPress: toggleRail,
},
]
: [
{
id: "drag",
icon: ,
label: "Move window",
dragHandleProps,
},
]
}
trailingActions={[
...(isFullscreen
? [
{
id: "artifact",
icon: ,
label: "Toggle artifact panel",
onPress: toggleArtifact,
},
]
: []),
{
id: "inbox",
icon: ,
label: "Inbox",
onPress: () => {},
},
{
id: "size",
icon: isFullscreen ? : ,
label: isFullscreen ? "Restore" : "Maximize",
onPress: toggleSize,
},
{
id: "close",
icon: ,
label: "Close",
onPress: close,
},
]}
/>
);
}
function App() {
const [prompt, setPrompt] = useState("");
return (
}
rail={
}
chat={
}
prompt={
}
/>
}
artifact={ }
/>
);
}
export default App;
```
### Fullscreen shell
```tsx lines theme={null}
import { useState } from "react";
import {
AppHeader,
CartoTheme,
ChatHistory,
ChatLayout,
PromptBar,
TabbedLayout,
Text,
Window,
useAppSurface,
type ChatHistoryItem,
type TabbedLayoutTab,
} from "@servicetitan/carto-react-kit";
import {
IconMinimize2,
IconPanelLeft,
IconPanelRight,
IconX,
} from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
const historyItems: ChatHistoryItem[] = [
{ id: "current", label: "Current conversation", subtitle: "Just now" },
{ id: "previous", label: "Previous conversation", subtitle: "Yesterday" },
];
const artifactTabs: TabbedLayoutTab[] = [
{
key: "preview",
label: "Preview",
content: ,
},
];
function WindowHeader() {
const { close, toggleArtifact, toggleRail, toggleSize } = useAppSurface();
return (
,
label: "Toggle chat history",
onPress: toggleRail,
},
]}
trailingActions={[
{
id: "artifact",
icon: ,
label: "Toggle artifact panel",
onPress: toggleArtifact,
},
{
id: "restore",
icon: ,
label: "Restore",
onPress: toggleSize,
},
{
id: "close",
icon: ,
label: "Close",
onPress: close,
},
]}
/>
);
}
function App() {
const [prompt, setPrompt] = useState("");
return (
}
rail={
}
chat={
}
/>
}
artifact={ }
/>
);
}
export default App;
```
Both shells are available to a user. Implementors can default which experience a user lands on, but cannot prevent the user from switching to the other.
#### Feature Options
| Capability | Window Shell | Fullscreen Shell |
| -------------- | ------------ | ---------------- |
| Artifact panel | Unavailable | Available |
| Chat history | Available | Available |
| New chat | Available | Available |
#### When to default to which shell?
| Presentation | Use when |
| ---------------- | ---------------------------------------------------- |
| Window Shell | Context from the underlying page remains important |
| Fullscreen Shell | The workflow requires dedicated focus or an artifact |
## Supporting content
Supporting content can appear in the App Shell’s artifact panel. The content inside that panel can use a Tabbed Layout when users need access to multiple related views.
### Panel
```tsx lines theme={null}
import { useState } from "react";
import {
AppHeader,
CartoTheme,
ChatHistory,
ChatLayout,
PromptBar,
Text,
Window,
useAppSurface,
type ChatHistoryItem,
} from "@servicetitan/carto-react-kit";
import {
IconMinimize2,
IconPanelLeft,
IconPanelRight,
IconX,
} from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
const historyItems: ChatHistoryItem[] = [
{ id: "current", label: "Current conversation", subtitle: "Just now" },
{ id: "previous", label: "Previous conversation", subtitle: "Yesterday" },
];
function WindowHeader() {
const { close, toggleArtifact, toggleRail, toggleSize } = useAppSurface();
return (
,
label: "Toggle chat history",
onPress: toggleRail,
},
]}
trailingActions={[
{
id: "artifact",
icon: ,
label: "Toggle artifact panel",
onPress: toggleArtifact,
},
{
id: "restore",
icon: ,
label: "Restore",
onPress: toggleSize,
},
{
id: "close",
icon: ,
label: "Close",
onPress: close,
},
]}
/>
);
}
function App() {
const [prompt, setPrompt] = useState("");
return (
}
rail={
}
chat={
}
/>
}
artifact={
}
/>
);
}
export default App;
```
### Tabbed layout
```tsx lines theme={null}
import { useState } from "react";
import {
AppHeader,
CartoTheme,
ChatHistory,
ChatLayout,
PromptBar,
TabbedLayout,
Text,
Window,
useAppSurface,
type ChatHistoryItem,
type TabbedLayoutTab,
} from "@servicetitan/carto-react-kit";
import {
IconMinimize2,
IconPanelLeft,
IconPanelRight,
IconX,
} from "@servicetitan/carto-react-kit/icons";
import "@servicetitan/carto-react-kit/styles.css";
const historyItems: ChatHistoryItem[] = [
{ id: "current", label: "Current conversation", subtitle: "Just now" },
{ id: "previous", label: "Previous conversation", subtitle: "Yesterday" },
];
const artifactTabs: TabbedLayoutTab[] = [
{
key: "preview",
label: "Preview",
content: ,
},
{
key: "details",
label: "Details",
content: ,
},
];
function WindowHeader() {
const { close, toggleArtifact, toggleRail, toggleSize } = useAppSurface();
return (
,
label: "Toggle chat history",
onPress: toggleRail,
},
]}
trailingActions={[
{
id: "artifact",
icon: ,
label: "Toggle artifact panel",
onPress: toggleArtifact,
},
{
id: "restore",
icon: ,
label: "Restore",
onPress: toggleSize,
},
{
id: "close",
icon: ,
label: "Close",
onPress: close,
},
]}
/>
);
}
function App() {
const [prompt, setPrompt] = useState("");
return (
}
rail={
}
chat={
}
/>
}
artifact={ }
/>
);
}
export default App;
```
## Dos and Don'ts for Agentic Experience Shell
| Guidance | Practice |
| -------- | -------------------------------------------------------------------------------------------------------------- |
| Do | Use an artifact panel when a workflow benefits from dedicated space to view and interact with related content. |
| Don't | Place an artifact panel in the floating shell. |
| Don't | Modify the left rail with content other than chat history. |
| Don't | Change the floating shell's default location. |
## Related components
* [App Header](/docs/kits/ai-kit/components/app-header/design) — Identifies the experience and provides shell-level actions
* [App Shell](/docs/kits/ai-kit/components/app-shell/design) — Arranges the header, history rail, conversation, and supporting panel
* [Chat History](/docs/kits/ai-kit/components/chat-history/design) — Provides access to previous conversations
* [Chat Layout](/docs/kits/ai-kit/components/chat-layout/design) — Defines the conversation region
* [Tabbed Layout](/docs/kits/ai-kit/components/tabbed-layout/design) — Organizes multiple related views within the supporting workspace
* [Window](/docs/kits/ai-kit/components/window/design) — Presents the shell as a floating or fullscreen experience
## Related patterns
* [Chat Layout](/docs/kits/ai-kit/patterns/chat-layout) — For the layout of chat messages within the shell.
# Chat Layout
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/patterns/chat-layout
Compose messages, rich content, actions, and prompts into an agent conversation.
The chat layout organizes the content and controls inside an agent conversation. It does not define the surrounding application shell.
## Complete chat layout
```tsx lines theme={null}
import { useState } from "react";
import {
ActionLog,
AgentMediaCard,
AgentMessage,
AgentThinking,
ArtifactCard,
CartoTheme,
ChatLayout,
PromptBar,
SuggestionList,
Text,
UserMessage,
type ActionLogStep,
type MediaAttachment,
} from "@servicetitan/carto-react-kit";
import * as cartoTokens from "@servicetitan/carto-react-kit/tokens";
import "@servicetitan/carto-react-kit/styles.css";
const { primitive } =
cartoTokens as unknown as typeof import("@servicetitan/carto-tokens/vanilla-extract");
const media: MediaAttachment = {
kind: "document",
fileName: "Service agreement.pdf",
fileTypeLabel: "PDF",
href: "https://example.com/service-agreement.pdf",
};
const actionSteps: ActionLogStep[] = [
{
id: "agreement",
title: "Reviewed the service agreement",
status: "success",
},
{
id: "schedule",
title: "Prepared the arrival window update",
status: "success",
},
];
const timestamp = new Date(2026, 3, 1, 16, 40);
function App() {
const [draft, setDraft] = useState("");
return (
{}}
/>
>
}
copyText="I found the agreement and prepared the requested update."
timestamp={new Date(2026, 3, 1, 16, 40, 30)}
onLike={() => {}}
onDislike={() => {}}
/>
{}}
/>
>
}
prompt={
setDraft("")}
placeholder="Ask anything…"
/>
}
/>
);
}
export default App;
```
## User Message
```tsx lines theme={null}
import {
CartoTheme,
SuggestionList,
UserMediaMessage,
UserMessage,
type MediaAttachment,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const media: MediaAttachment = {
kind: "document",
fileName: "Service agreement.pdf",
fileTypeLabel: "PDF",
href: "https://example.com/service-agreement.pdf",
};
const timestamp = new Date(2026, 3, 1, 16, 40);
function App() {
return (
{}}
/>
);
}
export default App;
```
### User Message
```tsx lines theme={null}
import { CartoTheme, UserMessage } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
return (
);
}
export default App;
```
##### Multiple user messages
```tsx lines theme={null}
import { CartoTheme, UserMessage } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
return (
);
}
export default App;
```
### User Media
```tsx lines theme={null}
import {
CartoTheme,
UserMediaMessage,
type MediaAttachment,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const media: MediaAttachment[] = [
{
kind: "document",
fileName: "Service agreement.pdf",
fileTypeLabel: "PDF",
href: "https://example.com/service-agreement.pdf",
},
{
kind: "image",
fileName: "Site photo.jpg",
fileTypeLabel: "Image",
src: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='60'%3E%3Crect width='80' height='60' fill='%23dceeff'/%3E%3Cpath d='M8 48 28 26l14 14 10-10 20 18Z' fill='%2370b1ff'/%3E%3C/svg%3E",
href: "https://example.com/site-photo.jpg",
},
{
kind: "audio",
fileName: "Customer call.mp3",
fileTypeLabel: "Audio",
src: "https://example.com/customer-call.mp3",
href: "https://example.com/customer-call.mp3",
},
{
kind: "video",
fileName: "Walkthrough.mp4",
fileTypeLabel: "Video",
src: "https://example.com/walkthrough.mp4",
href: "https://example.com/walkthrough.mp4",
},
];
function App() {
return (
{media.map((attachment, index) => (
))}
);
}
export default App;
```
### Suggestions
```tsx lines theme={null}
import { CartoTheme, SuggestionList } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
return (
{}}
/>
);
}
export default App;
```
### User Message Do's and Don'ts
| Guidance | Practice |
| -------- | ----------------------------------------------------------------------------------------- |
| Do | Maintain the layout order of User Message content. |
| Do | Maintain consistent spacing between elements around the User Message. |
| Do | On non-touch devices, reveal the toolbar on hover for messages before the latest message. |
| Don't | Change the User Message bubble color. |
| Don't | Change the User Message toolbar. |
## Agent Message
Agent message represents responses from the Agent. Unlike user messages, agent messages are fluid in their content and layout.
```tsx lines theme={null}
import {
ActionLog,
AgentMediaCard,
AgentMessage,
AgentThinking,
ArtifactCard,
CartoTheme,
GuidanceCard,
Markdown,
type ActionLogStep,
type GuidanceChangeSection,
type MediaAttachment,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const steps: ActionLogStep[] = [
{
id: "review",
title: "Reviewed the service agreement",
status: "success",
},
];
const media: MediaAttachment = {
kind: "document",
fileName: "Service agreement.pdf",
fileTypeLabel: "PDF",
href: "https://example.com/service-agreement.pdf",
};
const changes: GuidanceChangeSection[] = [
{
type: "single",
changes: [
{
id: "arrival-window",
label: "Arrival window",
from: "8–10 AM",
to: "9–11 AM",
},
],
},
];
function App() {
return (
{}}
/>
{}}
onEdit={() => {}}
onDismiss={() => {}}
/>
>
}
copyText="I reviewed the agreement and prepared a summary."
timestamp={new Date(2026, 3, 1, 16, 40)}
onLike={() => {}}
onDislike={() => {}}
/>
);
}
export default App;
```
### Markdown Text
```tsx lines theme={null}
import { CartoTheme, Markdown } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
return (
);
}
export default App;
```
##### Rich Markdown Agent Message
```tsx lines theme={null}
import {
AgentMessage,
CartoTheme,
Markdown,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const source = `### Agreement review
I reviewed the **service agreement** and found *two items* that need attention. The source is available in the [customer record](https://example.com/customer-record).
> The renewal window closes in 14 days.
#### Review checklist
- [x] Confirm payment terms
- [x] Compare the current arrival window
- [ ] Send the renewal notice
#### Risk summary
| Area | Status | Detail |
| --- | --- | --- |
| Payment | Clear | Net 30 terms |
| Renewal | Review | Notice required |
| Schedule | Updated | 9–11 AM |
1. Confirm the proposed schedule.
2. Notify the customer.
3. Archive the previous version.
The current setting is \`autoRenewal: true\`.
\`\`\`json
{
"arrivalWindow": "9–11 AM",
"noticeRequired": true
}
\`\`\`
~~No follow-up needed.~~ **Follow-up is required.**
Visit https://example.com/agreements for the full policy.
---`;
function App() {
return (
}
copyText={source}
timestamp={new Date(2026, 3, 1, 16, 40)}
onLike={() => {}}
onDislike={() => {}}
/>
);
}
export default App;
```
Example of using many markdown types in one Agent Message. For illustrative purposes of features, this should not be considered a high quality agent message.
Agent messages are primarily built on top of markdown text. The majority of markdown features are supported. See the [Markdown component page](/docs/kits/ai-kit/components/markdown/design) for each feature in action.
### Action Log
```tsx lines theme={null}
import {
ActionLog,
CartoTheme,
type ActionLogStep,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const steps: ActionLogStep[] = [
{
id: "review",
title: "Reviewed the service agreement",
status: "success",
},
{
id: "summary",
title: "Prepared the summary",
status: "success",
},
];
function App() {
return (
);
}
export default App;
```
### Media Card
```tsx lines theme={null}
import {
AgentMediaCard,
CartoTheme,
type MediaAttachment,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const media: MediaAttachment = {
kind: "document",
fileName: "Service agreement.pdf",
fileTypeLabel: "PDF",
href: "https://example.com/service-agreement.pdf",
};
function App() {
return (
);
}
export default App;
```
Media cards represent an asset within the chat layout.
Differences between media representation between agent and user messages:
* Agent media cards can be ordered anywhere in the agent message. User message media is fixed to above the message content.
* Agent media cards stretch to the width of the agent message.
### Artifact Card
```tsx lines theme={null}
import { ArtifactCard, CartoTheme } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
return (
);
}
export default App;
```
Artifact cards represent an asset within the chat layout. When selected, content should display in the [artifact panel](/docs/kits/ai-kit/patterns/agentic-experience-shell#panel).
### Guidance Card
```tsx lines theme={null}
import {
CartoTheme,
GuidanceCard,
type GuidanceChangeSection,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const changes: GuidanceChangeSection[] = [
{
type: "single",
changes: [
{
id: "arrival-window",
label: "Arrival window",
from: "8–10 AM",
to: "9–11 AM",
},
],
},
];
function App() {
return (
{}}
onEdit={() => {}}
onDismiss={() => {}}
/>
);
}
export default App;
```
### Toolbar
```tsx lines theme={null}
import { AgentMessage, CartoTheme } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
return (
{}}
onDislike={() => {}}
/>
);
}
export default App;
```
### Agent Thinking
```tsx lines theme={null}
import { AgentThinking, CartoTheme } from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
return (
);
}
export default App;
```
Agent Thinking represents the visual representation of where the agent is currently thinking. There is exactly one Agent Thinking in the chat layout.
#### Examples of Agent Thinking location in action
```tsx lines theme={null}
import {
AgentThinking,
CartoTheme,
UserMessage,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
return (
);
}
export default App;
```
Just after a user message, Agent Thinking acts as a loading state.
```tsx lines theme={null}
import {
AgentThinking,
CartoTheme,
Text,
UserMessage,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
return (
);
}
export default App;
```
As text streams in, Agent Thinking sits below the incoming text. Its label is typically omitted during streaming.
```tsx lines theme={null}
import {
AgentMessage,
AgentThinking,
Card,
CartoTheme,
Text,
UserMessage,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
const timestamp = new Date(2026, 3, 1, 16, 40);
function App() {
return (
}
/>
>
}
copyText="This is text the agent has finished streaming. More text has finished streaming. Atlas is done."
timestamp={timestamp}
onLike={() => {}}
onDislike={() => {}}
/>
);
}
export default App;
```
After an agent message finishes, Agent Thinking sits below the toolbar.
### Agent Message Do's and Don'ts
* Do: Maintain a consistent location of the Agent Toolbar. Outside of Agent Thinking, this is always at the bottom of the agent message.
* Do: Show the Agent Toolbar only after the agent message is finished.
* Do: Use loading mechanisms when possible to indicate the agent is processing the message.
* Do: Arrange the layout of Agent Message content to produce a clear and readable message. Unlike user messages, agent messages are fluid in order for most parts of the message.
* Don't: Use multiple Agent Thinkings in the chat layout.
## Related Components
* [Prompt Bar](/docs/kits/ai-kit/components/prompt-bar/design) — Accepts free-form user input
* [User Message](/docs/kits/ai-kit/components/user-message/design) — Presents user-authored content
* Agent Message — Presents agent-authored content and feedback actions
* Media — Presents user- or agent-authored attachments
* [Artifact Card](/docs/kits/ai-kit/components/artifact-card/design) — Opens generated content
* [Suggestions](/docs/kits/ai-kit/components/suggestion/design) — Offers concise follow-up actions
* [Agent Prompt](/docs/kits/ai-kit/components/agent-prompt/design) — Requests a structured answer
* [Guidance Card](/docs/kits/ai-kit/components/guidance-card/design) — Presents proposed changes for review
## Related Patterns
* [Agentic Experience Shell](/docs/kits/ai-kit/patterns/agentic-experience-shell) — Composes the chat layout with supporting content
# Evidence Disclosure
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/patterns/evidence-disclosure
Evidence disclosure is an agent's justification for its conclusion.
Evidence disclosure is a type of progressive disclosure. What's being presented with evidence disclosure is trust-making material: the evidence a user would want in order to verify the AI didn't make something up.
This is part of the [Reasoning](/docs/kits/ai-kit/ai-ux/ux-and-the-five-parts-of-a-moment#2-reason) aspect of AI UX.
## Why evidence disclosure helps users
* Users report that seeing citations increases their confidence in an AI answer. However, users rarely actually click through to check them. The value of the disclosure is in the offer, not the click-through rate.
* Reasoning content is qualitatively different from output content—it may contain errors, false starts, or dead ends, and should be visually distinguished from the final answer rather than presented with the same confidence.
## Types of evidence disclosure
Evidence disclosure falls into two main categories:
* **Reasoning trace/extended thinking**—This typically appears above an agent response with a label like "Thought for \[x] seconds" and a chevron that allows the user to click into the reasoning that led the agent to the answer it gave. It always has different styling from the chat itself to make it clear it's categorically different from the styling of the agent responses themselves.
* **Citations**—These are pills that appear with an agent response that cite the agent's sources.
## Getting Started
Evidence Disclosure is composed from existing Carto components rather than a single dedicated component. Pick the building blocks that match what evidence you're disclosing.
| Component | When to Use |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| **[Citation](/docs/kits/ai-kit/components/citation/design)** | Attribute a specific claim in agent text to a source, inline or at the end of a response. |
| **[Action Log](/docs/kits/ai-kit/components/action-log/design)** | Shows the user how the agent arrived as its conclusion. |
## Dos and Don'ts
```tsx lines theme={null}
import { useState } from "react";
import {
CartoTheme,
ChatLayout,
PromptBar,
AgentMessage,
Text,
ActionLog,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
const [value, setValue] = useState("");
return (
);
}
export default App;
```
```tsx lines theme={null}
import { useState } from "react";
import {
CartoTheme,
ChatLayout,
PromptBar,
AgentMessage,
Text,
ActionLog,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
const [value, setValue] = useState("");
return (
);
}
export default App;
```
```tsx lines theme={null}
import { useState } from "react";
import {
CartoTheme,
ChatLayout,
PromptBar,
AgentMessage,
ActionLog,
Text,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
const [value, setValue] = useState("");
return (
);
}
export default App;
```
```tsx lines theme={null}
import { useState } from "react";
import {
CartoTheme,
ChatLayout,
PromptBar,
Text,
AgentMessage,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
const [value, setValue] = useState("");
return (
);
}
export default App;
```
```tsx lines theme={null}
import { useState } from "react";
import {
CartoTheme,
ChatLayout,
PromptBar,
AgentMessage,
ActionLog,
Text,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
const [value, setValue] = useState("");
return (
);
}
export default App;
```
```tsx lines theme={null}
import { useState } from "react";
import {
CartoTheme,
ChatLayout,
PromptBar,
AgentMessage,
Link,
Text,
} from "@servicetitan/carto-react-kit";
import "@servicetitan/carto-react-kit/styles.css";
function App() {
const [value, setValue] = useState("");
return (
);
}
export default App;
```
# Patterns
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/patterns/overview
Patterns are a collection of Components, arranged to achieve a user goal.
# Terminology
Source: https://anvil.servicetitan.com/docs/kits/ai-kit/terminology
Core terms used across AI experience, as well as internal ServiceTitan terminology.
If you haven't already, start with [Getting Started](/docs/kits/ai-kit/getting-started).
# Artifact Card – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/artifact-card/code
ArtifactCard is the in-chat trigger that opens an ArtifactPanel for an artifact surfaced by the Atlas agent.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { ArtifactCard } from "@servicetitan/anvil2-ext-atlas";
import { Flex } from "@servicetitan/anvil2";
function App() {
return (
console.log("artifact 1 clicked")}
/>
console.log("artifact 2 clicked")}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Render an artifact trigger with a title and description:
```tsx theme={null}
import { ArtifactCard } from "@servicetitan/anvil2-ext-atlas";
function ArtifactTrigger() {
return (
openPanel("insight-1")}
/>
);
}
```
### Active State
Pass `active` while the corresponding `ArtifactPanel` is open so the card shows the elevated visual state:
```tsx theme={null}
import { ArtifactCard } from "@servicetitan/anvil2-ext-atlas";
function ActiveTrigger({ openId, onOpen }) {
return (
onOpen("insight-2")}
/>
);
}
```
### Pairing with ArtifactPanel
Wire the card's `onClick` to the consumer state that drives an `ArtifactPanel`:
```tsx theme={null}
import { useState } from "react";
import { ArtifactCard, ArtifactPanel } from "@servicetitan/anvil2-ext-atlas";
function ChatWithArtifact() {
const [activeId, setActiveId] = useState(null);
return (
<>
setActiveId("insight-1")}
/>
{
if (!open) setActiveId(null);
}}
title="Insight details"
triggerKey={activeId ?? undefined}
>
{/* artifact content */}
>
);
}
```
## React Accessibility
* The card renders as a `role="group"` wrapping an interactive button via Anvil2's `InteractiveCard`. The whole surface is clickable.
* The `title` is used for both the wrapper's `aria-label` and the action button's `aria-label` (prefixed with "View ").
* `artifactId` is emitted as `data-artifact-id` for analytics and event wiring; it is not announced to assistive technology.
```tsx theme={null}
```
## `ArtifactCard` Props
Supplementary description shown beneath the title.
Called when the card is activated. Consumers typically open the corresponding `ArtifactPanel` here.
Bold heading rendered inside the card.
When true, the card shows the active visual state. Set when the corresponding `ArtifactPanel` is open.
Identifier for the artifact this card references. Emitted as a `data-artifact-id` attribute for analytics and event handling.
# Artifact Panel Layout – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/artifact-panel-layout/code
ArtifactPanelLayout is the row container that orchestrates the responsive behavior of nested ArtifactPanel components.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import {
ArtifactCard,
ArtifactPanel,
ArtifactPanelLayout,
} from "@servicetitan/anvil2-ext-atlas";
import { Flex, Text } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [activeId, setActiveId] = useState(null);
return (
I checked invoice 10492 against the service agreement and technician
notes. One thing to flag: labor came in 1.5 hours over the approved
estimate.
setActiveId("insight-1")}
/>
{
if (!open) setActiveId(null);
}}
position="right"
title="Supplemental info"
description="Variance details for invoice 10492"
triggerKey={activeId ?? undefined}
>
Resize the surrounding viewport to see the panel switch between inline
and overlay modes.
);
}
export default App;
```
## Common Examples
### Basic Usage
Wrap the chat content and any nested `ArtifactPanel` components in `ArtifactPanelLayout`. The layout establishes the positioning context and decides whether each registered panel sits inline or overlays the chat:
```tsx theme={null}
import {
ArtifactPanel,
ArtifactPanelLayout,
} from "@servicetitan/anvil2-ext-atlas";
function ChatLayout({ messages, panelOpen, setPanelOpen }) {
return (
{messages}
{/* artifact content */}
);
}
```
### Responsive Behavior
The layout observes its own width and decides per-panel whether enough room is available to sit inline alongside the chat. Inline mode kicks in only when the chat content would still have at least 20rem of room; otherwise the panel switches to overlay mode and floats over the chat anchored to the appropriate edge.
Mode is read from a context internal to `ArtifactPanelLayout` — consumers do not need to compute or pass it. When an `ArtifactPanel` is rendered outside `ArtifactPanelLayout`, it falls back to inline mode.
## React Accessibility
* The layout itself is a presentational `` with no semantics — accessibility is handled by the components nested inside (chat content, `ArtifactPanel` aside landmark, etc.).
* The layout establishes a positioning context (`position: relative`) so absolutely-positioned overlay panels anchor correctly without consumers needing to wire that up themselves.
```tsx theme={null}
```
## `ArtifactPanelLayout` Props
The chat content and any nested `ArtifactPanel` components.
# Artifact Panel – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/artifact-panel/code
ArtifactPanel renders a side panel within the Atlas full-screen chat for supplemental content surfaced by the agent.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import {
ArtifactCard,
ArtifactPanel,
ArtifactPanelLayout,
} from "@servicetitan/anvil2-ext-atlas";
import { Flex, Text } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [activeId, setActiveId] = useState(null);
return (
I checked invoice 10492 against the service agreement and technician
notes. One thing to flag: labor came in 1.5 hours over the approved
estimate.
setActiveId("insight-1")}
/>
{
if (!open) setActiveId(null);
}}
position="right"
title="Supplemental info"
description="Variance details for invoice 10492"
triggerKey={activeId ?? undefined}
>
Side-by-side details on the variance flagged in the chat.
);
}
export default App;
```
## Common Examples
`ArtifactPanel` reads its responsive mode (inline vs. overlay) from the surrounding `ArtifactPanelLayout`. Wrap chat content and any artifact panels in `ArtifactPanelLayout` so the panel can switch modes when the available width changes. Outside a layout the panel falls back to inline mode.
### Basic Usage
Wrap chat content and the panel in `ArtifactPanelLayout`. Open and close the panel from consumer state:
```tsx theme={null}
import { useState } from "react";
import {
ArtifactPanel,
ArtifactPanelLayout,
} from "@servicetitan/anvil2-ext-atlas";
function ArtifactDetails({ children }) {
const [open, setOpen] = useState(false);
return (
{children}
{/* artifact content */}
);
}
```
### Swapping Artifacts While Open
Pass `triggerKey` so the panel treats a value change as a new open event — focus shifts into the panel for the new artifact, and close-restore returns focus to the most recent trigger:
```tsx theme={null}
import { useState } from "react";
import { ArtifactCard, ArtifactPanel } from "@servicetitan/anvil2-ext-atlas";
function ChatWithArtifacts() {
const [activeId, setActiveId] = useState(null);
return (
<>
setActiveId("insight-1")}
/>
setActiveId("insight-2")}
/>
{
if (!open) setActiveId(null);
}}
title={`Insight ${activeId ?? ""}`}
triggerKey={activeId ?? undefined}
>
{/* artifact content */}
>
);
}
```
### Agent-Initiated Open
For panels opened by the agent rather than a user gesture, set `focusOnOpen` to `false` so focus is not pulled away from the chat composer:
```tsx theme={null}
{/* artifact content */}
```
## React Accessibility
* The panel renders an `` landmark with `aria-labelledby` tied to the title and `aria-describedby` tied to the description when present.
* Focus moves into the panel on open (default behavior) and returns to the previously focused element on close.
* Slide and fade animations respect `prefers-reduced-motion`.
* The panel is not modal — chat content remains interactive while the panel is open. The panel does not handle Escape or click-outside; those belong to the parent chat.
* When `ArtifactPanelLayout` switches the panel from inline to overlay mode at narrow widths, the chat content beneath remains in the document flow and reachable by assistive technology.
```tsx theme={null}
{content}
```
## `ArtifactPanel` Props
Content rendered in the panel body. Accepts arbitrary content including markdown, GenUI components, and other interactive elements.
Whether the panel is currently rendered.
Called when the panel requests an open-state change (e.g. when the close button is clicked).
Panel heading rendered in the header and referenced by `aria-labelledby`.
Optional sub-header text rendered beneath the title.
Move focus into the panel when it opens, and return focus to the previously focused element when it closes. Set false for agent-initiated opens so focus is not pulled away from the chat composer.
Pick the initial focus target when `focusOnOpen` is true. Defaults to the first non-close focusable.
Side the panel is anchored to. Drives border placement.
Identifier for the current artifact. When this value changes while the panel is already open, focus management treats it as a new open event: it captures the newly-focused trigger and re-shifts focus into the panel, so close-restore returns to the right element.
# Atlas Header – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/atlas-header/code
AtlasHeader is a shared header component for messages and recommendations.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { Header } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
function App() {
return (
console.log("back clicked")}
onClose={() => console.log("close clicked")}
onViewHistory={() => console.log("view history clicked")}
onFullscreen={() => console.log("fullscreen clicked")}
onCreateNewChat={() => console.log("new chat clicked")}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Default Atlas branding:
```tsx theme={null}
import { AtlasHeader } from "@servicetitan/anvil2-ext-atlas";
function DefaultHeader() {
return ;
}
```
### Custom Branding
Use custom icon and assistant name:
```tsx theme={null}
import { AtlasHeader } from "@servicetitan/anvil2-ext-atlas";
import CustomLogo from "./custom-logo.svg";
function CustomHeader() {
return (
);
}
```
### With Custom Styling
Apply custom CSS class:
```tsx theme={null}
import { AtlasHeader } from "@servicetitan/anvil2-ext-atlas";
function StyledHeader() {
return (
);
}
```
```tsx theme={null}
```
## `AtlasHeader` Props
Custom assistant name to display.
Additional CSS class name for custom styling.
Custom icon to display instead of the Atlas logo.
# Chat Composer Rich – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/chat-composer-rich/code
ChatComposerRich provides a rich text input for composing chat messages.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { ChatComposerRich } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [message, setMessage] = useState("");
return (
{
console.log("sent:", text);
setMessage("");
}}
onDictateMessage={() => console.log("dictate clicked")}
onUploadFile={() => console.log("upload clicked")}
placeholder="Ask anything..."
/>
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { ChatComposerRich } from "@servicetitan/anvil2-ext-atlas";
function BasicComposer() {
const [message, setMessage] = useState("");
const handleSend = (text: string) => {
console.log("Sending:", text);
};
return (
);
}
```
### With Action Menu
Enable file upload and voice dictation options:
```tsx theme={null}
import { ChatComposerRich } from "@servicetitan/anvil2-ext-atlas";
function ComposerWithActions() {
const [message, setMessage] = useState("");
return (
openFileDialog()}
onDictateMessage={() => startVoiceRecognition()}
/>
);
}
```
### Disabled State
Disable input during processing:
```tsx theme={null}
import { ChatComposerRich } from "@servicetitan/anvil2-ext-atlas";
function DisabledComposer() {
const [isProcessing, setIsProcessing] = useState(false);
return (
{}}
onSend={handleSend}
disabled={isProcessing}
placeholder="Processing your request..."
/>
);
}
```
### Custom Placeholder
```tsx theme={null}
import { ChatComposerRich } from "@servicetitan/anvil2-ext-atlas";
function CustomPlaceholderComposer() {
return (
);
}
```
```tsx theme={null}
```
## `ChatComposerRich` Props
Callback fired when the input content changes.
Additional CSS class name for custom styling.
When true, disables the entire composer including input and buttons.
The current value of the input. Used to sync external state with the input.
Callback for the "Dictate message" menu action. Menu item only renders when provided.
Callback fired when the user submits a message (Enter key or send button).
Callback for the "Upload file" menu action. Menu item only renders when provided.
Placeholder text displayed when the input is empty.
### Test ID Props
Test ID for the menu button.
Test ID for the message input element.
Test ID for the send button.
# Chat Window – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/chat-window/code
ChatWindow is the main container for the Atlas chat experience with animations and positioning.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { ChatWindow } from "@servicetitan/anvil2-ext-atlas";
import { Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
Chat content goes here
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { ChatWindow } from "@servicetitan/anvil2-ext-atlas";
function BasicChatWindow() {
const [open, setOpen] = useState(false);
return (
{/* Header, Content, Footer components */}
);
}
```
### With Position
Use the `position` prop to create a floating chat window at specific coordinates:
```tsx theme={null}
import { ChatWindow } from "@servicetitan/anvil2-ext-atlas";
function PositionedChatWindow() {
const [open, setOpen] = useState(true);
return (
{/* Chat content */}
);
}
```
### Draggable Window
Combine with the `useDraggable` hook to create a draggable chat window:
```tsx theme={null}
import { ChatWindow, Header, useDraggable } from "@servicetitan/anvil2-ext-atlas";
function DraggableChatWindow() {
const [open, setOpen] = useState(true);
const { position, isDragging, handleMouseDown, resetPosition } = useDraggable();
return (
setOpen(false)}
/>
{/* Content and Footer */}
);
}
```
### Fullscreen Mode
Enable fullscreen mode for an expanded chat experience:
```tsx theme={null}
import { ChatWindow } from "@servicetitan/anvil2-ext-atlas";
function FullscreenChatWindow() {
const [open, setOpen] = useState(true);
const [fullscreen, setFullscreen] = useState(false);
return (
setFullscreen(!fullscreen)}
onClose={() => setOpen(false)}
/>
{/* Content and Footer */}
);
}
```
```tsx theme={null}
{children}
```
## `ChatWindow` Props
The content to render inside the chat window, typically Header, Content, and Footer components.
Controls whether the chat window is visible. Triggers open/close animations.
When true, the window expands to fill the available space with a fade animation instead of the standard slide animation.
Indicates if the window is currently being dragged. When true, layout animations are disabled to prevent conflicts.
Optional coordinates for positioning the chat window. When provided, the window uses absolute positioning.
# Content – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/content/code
Content provides a scrollable message container with auto-scroll behavior.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { Content, UserMessage } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { Content, UserMessage, MarkdownMessage } from "@servicetitan/anvil2-ext-atlas";
function ChatContent() {
const messages = [
{ id: 1, type: "user", content: "Hello!" },
{ id: 2, type: "assistant", content: "Hi! How can I help you?" },
];
return (
{messages.map((msg) =>
msg.type === "user" ? (
) : (
)
)}
);
}
```
### With Loading State
Display a loading spinner while fetching initial messages:
```tsx theme={null}
import { Content } from "@servicetitan/anvil2-ext-atlas";
function LoadingContent() {
const [loading, setLoading] = useState(true);
const [messages, setMessages] = useState([]);
useEffect(() => {
fetchMessages().then((data) => {
setMessages(data);
setLoading(false);
});
}, []);
return (
{messages.map((msg) => (
))}
);
}
```
### Auto-Scroll Behavior
The component automatically scrolls to the bottom when new messages are added:
```tsx theme={null}
import { Content, UserMessage } from "@servicetitan/anvil2-ext-atlas";
function AutoScrollContent() {
const [messages, setMessages] = useState([]);
const addMessage = (text) => {
setMessages((prev) => [
...prev,
{ id: Date.now(), content: text },
]);
// Content will automatically smooth-scroll to show new message
};
return (
{messages.map((msg) => (
))}
);
}
```
```tsx theme={null}
{messages.map((msg) => (
))}
```
## `Content` Props
The current number of items (messages) in the content. Used to determine when to trigger auto-scroll behavior.
The content to render, typically message components.
Additional CSS class name for custom styling.
When true, displays a Spinner instead of the children content.
# Footer – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/footer/code
Footer provides the message input area for the Atlas chat interface.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { Footer } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [message, setMessage] = useState("");
return (
console.log("submitted:", message)}
placeholder="Ask Atlas"
/>
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { Footer } from "@servicetitan/anvil2-ext-atlas";
function BasicFooter() {
const [message, setMessage] = useState("");
const handleSubmit = () => {
console.log("Sending:", message);
setMessage("");
};
return (
);
}
```
### With Loading State
Disable input during message processing:
```tsx theme={null}
import { Footer } from "@servicetitan/anvil2-ext-atlas";
function FooterWithLoading() {
const [message, setMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async () => {
setIsLoading(true);
await sendMessage(message);
setMessage("");
setIsLoading(false);
};
return (
);
}
```
### With Additional Actions
Enable file upload and voice dictation:
```tsx theme={null}
import { Footer } from "@servicetitan/anvil2-ext-atlas";
function FooterWithActions() {
const [message, setMessage] = useState("");
return (
openFileDialog()}
onDictateMessage={() => startVoiceInput()}
/>
);
}
```
### Custom Placeholder
```tsx theme={null}
import { Footer } from "@servicetitan/anvil2-ext-atlas";
function FooterWithCustomPlaceholder() {
const [message, setMessage] = useState("");
return (
);
}
```
```tsx theme={null}
```
## `Footer` Props
The current value of the message input.
Callback fired when the message input value changes.
Callback fired when the user submits a message.
Additional CSS class name for custom styling.
When true, disables the input and prevents submission.
Callback for the "Dictate message" menu action. Menu item only renders when provided.
Callback for the "Upload file" menu action. Menu item only renders when provided.
Placeholder text displayed in the input when empty.
### Test ID Props
Test ID for the privacy policy link.
Test ID for the message input element.
Test ID for the send button.
# Header – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/header/code
Header provides navigation and action controls for the Atlas chat window.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { Header } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
function App() {
return (
console.log("back clicked")}
onClose={() => console.log("close clicked")}
onViewHistory={() => console.log("view history clicked")}
onFullscreen={() => console.log("fullscreen clicked")}
onCreateNewChat={() => console.log("new chat clicked")}
/>
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { Header } from "@servicetitan/anvil2-ext-atlas";
function BasicHeader() {
return (
console.log("Close clicked")}
/>
);
}
```
### With All Actions
Enable all available header actions:
```tsx theme={null}
import { Header } from "@servicetitan/anvil2-ext-atlas";
function FullHeader() {
return (
navigateBack()}
onViewHistory={() => openHistory()}
onFullscreen={() => toggleFullscreen()}
onCreateNewChat={() => startNewChat()}
onClose={() => closeWindow()}
historyCount={5}
/>
);
}
```
### Draggable Header
Enable the drag handle for repositioning the chat window:
```tsx theme={null}
import { Header, useDraggable } from "@servicetitan/anvil2-ext-atlas";
function DraggableHeader() {
const { isDragging, handleMouseDown } = useDraggable();
return (
closeWindow()}
/>
);
}
```
### With Title Badge
Display a badge next to the title:
```tsx theme={null}
import { Header } from "@servicetitan/anvil2-ext-atlas";
function HeaderWithBadge() {
return (
closeWindow()}
/>
);
}
```
### Expanded State
In fullscreen mode, the drag handle is hidden:
```tsx theme={null}
import { Header } from "@servicetitan/anvil2-ext-atlas";
function ExpandedHeader() {
const [isExpanded, setIsExpanded] = useState(false);
return (
setIsExpanded(!isExpanded)}
onClose={() => closeWindow()}
/>
);
}
```
```tsx theme={null}
```
## `Header` Props
Additional CSS class name for custom styling.
Disables the chat history button.
Number to display in the history button badge.
When true, displays the drag handle for repositioning the window.
Indicates if the window is currently being dragged. Changes cursor to grabbing.
Indicates fullscreen mode. Hides the drag handle and changes fullscreen tooltip.
Handler for back button click. Button only renders when provided.
Handler for close button click. Button only renders when provided.
Handler for new chat button click. Button only renders when provided.
Handler for fullscreen button click. Button only renders when provided.
Handler for drag start. Required when `isDraggable` is true.
Handler for history button click. Button only renders when provided.
The title displayed in the header.
Badge content to display next to the title.
### Test ID Props
Test ID for the back button.
Test ID for the close button.
Test ID for the fullscreen button.
Test ID for the new chat button.
Test ID for the history button.
# Infinite Content – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/infinite-content/code
InfiniteContent is a scrollable container with infinite scroll loading.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { InfiniteContent } from "@servicetitan/anvil2-ext-atlas";
import { Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
With Content
console.log("load more")}
>
Item 1
Item 2
Item 3
);
}
export default App;
```
## Common Examples
### Basic Usage
Scrollable content with infinite loading:
```tsx theme={null}
import { InfiniteContent } from "@servicetitan/anvil2-ext-atlas";
function InfiniteList() {
const [items, setItems] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const loadMore = async () => {
setLoading(true);
const newItems = await fetchMoreItems();
setItems((prev) => [...prev, ...newItems]);
setHasMore(newItems.length > 0);
setLoading(false);
};
return (
No more items
}
>
{items.map((item) => (
))}
);
}
```
### With Scroll Callbacks
Track scroll position:
```tsx theme={null}
import { InfiniteContent } from "@servicetitan/anvil2-ext-atlas";
function ScrollTrackingList() {
const [atTop, setAtTop] = useState(true);
return (
setAtTop(false)}
onScrollTop={() => setAtTop(true)}
>
{children}
);
}
```
### Custom Loading Indicators
Customize loading and end messages:
```tsx theme={null}
import { InfiniteContent, Loader } from "@servicetitan/anvil2-ext-atlas";
function CustomLoadingList() {
return (
}
renderEndMessage={() => (
You've reached the end!
)}
>
{children}
);
}
```
```tsx theme={null}
}
renderEndMessage={() => End of list
}
onScrollDown={handleScrollDown}
onScrollTop={handleScrollTop}
>
{children}
```
## `InfiniteContent` Props
The content to render inside the scrollable container.
Additional CSS class name for custom styling.
When true, enables infinite scroll loading.
When true and no children exist, shows the loading spinner.
When true, shows the loading more indicator.
Callback fired when more content should be loaded.
Callback fired when user scrolls past the threshold.
Callback fired when user scrolls back to top.
Custom render function for the end of list message.
Custom render function for the loading more indicator.
Intersection Observer root margin for load trigger.
Optional ref to custom scroll container element.
Pixel threshold for scroll callbacks.
Intersection Observer threshold for load trigger.
# Loader – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/loader/code
Loader is an animated loading indicator for pending assistant responses.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { Loader } from "@servicetitan/anvil2-ext-atlas";
import { Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
## Common Examples
### Basic Usage
Display while waiting for a response:
```tsx theme={null}
import { Loader } from "@servicetitan/anvil2-ext-atlas";
function LoadingState() {
return ;
}
```
### Within a Message
Show loading state in an assistant message:
```tsx theme={null}
import { Loader, AtlasHeader } from "@servicetitan/anvil2-ext-atlas";
function LoadingMessage() {
return (
);
}
```
```tsx theme={null}
```
## `Loader` Props
The Loader component has no configurable props. It displays an animated three-dot loading indicator.
# Markdown Text – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/markdown-text/code
MarkdownText renders markdown content with GitHub Flavored Markdown support.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { MarkdownText } from "@servicetitan/anvil2-ext-atlas";
import { Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
Simple Text
Bold and Italic
Code Block
);
}
export default App;
```
## Common Examples
### Basic Usage
Render markdown content:
```tsx theme={null}
import { MarkdownText } from "@servicetitan/anvil2-ext-atlas";
function MarkdownContent() {
const content = `
# Welcome
Here are some **important** points:
1. First item
2. Second item
3. Third item
Visit [our website](https://example.com) for more info.
`;
return ;
}
```
### Dynamic Content
Render markdown from API response:
```tsx theme={null}
import { MarkdownText } from "@servicetitan/anvil2-ext-atlas";
function DynamicMarkdown({ content }) {
return ;
}
```
### With Tables
Render GFM tables:
```tsx theme={null}
import { MarkdownText } from "@servicetitan/anvil2-ext-atlas";
function TableContent() {
const table = `
| Service | Price |
| ------- | ----- |
| HVAC | $150 |
| Plumbing| $120 |
`;
return ;
}
```
```tsx theme={null}
```
## `MarkdownText` Props
The markdown text to render.
# Assistant Message – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/messages/assistant-message/code
AssistantMessage displays simple text responses from the Atlas assistant.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { AssistantMessage } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
function App() {
return (
console.log("retry clicked")}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Display a simple text response:
```tsx theme={null}
import { AssistantMessage } from "@servicetitan/anvil2-ext-atlas";
function AssistantMessageExample() {
return (
);
}
```
### Loading State
Show loading indicator when waiting for response:
```tsx theme={null}
import { AssistantMessage } from "@servicetitan/anvil2-ext-atlas";
function AssistantMessageLoading() {
// When message is undefined, shows loading indicator
return ;
}
```
### Custom Branding
Use custom icon and assistant name:
```tsx theme={null}
import { AssistantMessage } from "@servicetitan/anvil2-ext-atlas";
import CustomLogo from "./custom-logo.svg";
function CustomBrandedMessage() {
return (
);
}
```
```tsx theme={null}
```
## `AssistantMessage` Props
Custom assistant name to display instead of "Atlas".
Additional CSS class name for custom styling.
Error state. When truthy, displays error message instead of content.
The message content. When undefined, shows loading indicator.
Callback for retry button in error state.
Custom icon to display instead of the Atlas logo.
# Error Message – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/messages/error-message/code
ErrorMessage displays error states with optional retry functionality.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { ErrorMessage } from "@servicetitan/anvil2-ext-atlas";
import { Card, Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
With Error String
console.log("retry clicked")}
/>
With Error Boolean
console.log("retry clicked")}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Standalone error display:
```tsx theme={null}
import { ErrorMessage } from "@servicetitan/anvil2-ext-atlas";
function ErrorMessageExample() {
return (
retryRequest()}
/>
);
}
```
### With Fallback Message
Use boolean error with fallback message:
```tsx theme={null}
import { ErrorMessage } from "@servicetitan/anvil2-ext-atlas";
function ErrorMessageFallback() {
return (
retryRequest()}
/>
);
}
```
### Without Retry
Error display without retry option:
```tsx theme={null}
import { ErrorMessage } from "@servicetitan/anvil2-ext-atlas";
function ErrorMessageNoRetry() {
return (
);
}
```
```tsx theme={null}
```
## `ErrorMessage` Props
The error message to display. If boolean `true`, uses `message` prop instead.
Additional CSS class name for custom styling.
Fallback message to display when `error` is `true`.
Callback for retry button. Button only renders when provided.
# Markdown Message – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/messages/markdown-message/code
MarkdownMessage displays rich formatted responses with markdown support.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { MarkdownMessage, UserFeedback } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
function App() {
return (
console.log("retry clicked")}
toolboxProps={{
onLike: async () => console.log("like clicked"),
onDislike: async () => console.log("dislike clicked"),
onFlag: async () => console.log("flag clicked"),
onPlay: async () => console.log("play clicked"),
currentFeedback: UserFeedback.None,
}}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Display rich markdown content:
```tsx theme={null}
import { MarkdownMessage } from "@servicetitan/anvil2-ext-atlas";
function MarkdownMessageExample() {
return (
);
}
```
### With Toolbox Actions
Enable user feedback and actions:
```tsx theme={null}
import { MarkdownMessage } from "@servicetitan/anvil2-ext-atlas";
function MarkdownWithToolbox() {
return (
handleLike(),
onDislike: () => handleDislike(),
onRetry: () => handleRetry(),
}}
/>
);
}
```
### Custom Branding
Use custom icon and assistant name:
```tsx theme={null}
import { MarkdownMessage } from "@servicetitan/anvil2-ext-atlas";
import CustomLogo from "./custom-logo.svg";
function CustomBrandedMessage() {
return (
);
}
```
```tsx theme={null}
```
## `MarkdownMessage` Props
Custom assistant name to display instead of "Atlas".
Additional CSS class name for custom styling.
Error message to display instead of content.
Markdown content to render. When undefined, shows loading indicator.
Callback for retry button in error state.
Custom icon to display instead of the Atlas logo.
Props to pass to the Toolbox component (excluding text which is set automatically).
See the [Toolbox API](/docs/kits/atlas/components/toolbox/code#toolbox-api) for more details.
# Small Action – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/messages/small-action/code
SmallAction displays quick accept/reject prompts for recommendations.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { SmallAction } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
function App() {
return (
console.log("rejected")}
onAccept={() => console.log("accepted")}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Quick action prompt:
```tsx theme={null}
import { SmallAction } from "@servicetitan/anvil2-ext-atlas";
function SmallActionExample() {
return (
applyRecommendation(id)}
onReject={(id) => dismissRecommendation(id)}
/>
);
}
```
### Without Description
Simple action prompt:
```tsx theme={null}
import { SmallAction } from "@servicetitan/anvil2-ext-atlas";
function SmallActionSimple() {
return (
handleAccept(id)}
onReject={(id) => handleReject(id)}
/>
);
}
```
### Disabled State
Prevent user interaction:
```tsx theme={null}
import { SmallAction } from "@servicetitan/anvil2-ext-atlas";
function SmallActionDisabled() {
return (
);
}
```
```tsx theme={null}
```
## `SmallAction` Props
The main prompt message.
Callback fired when accept button is clicked.
Callback fired when reject button is clicked.
Unique identifier for this recommendation, passed to callbacks.
When true, disables both action buttons.
Additional CSS class name for custom styling.
Additional description text below the message.
# System Message – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/messages/system-message/code
SystemMessage displays interactive prompts with radio button options.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { SystemMessage } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [selectedValue, setSelectedValue] = useState("option1");
return (
setSelectedValue(value)}
buttonText="Continue"
onSubmit={() => console.log("submitted:", selectedValue)}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Interactive prompt with radio options:
```tsx theme={null}
import { SystemMessage } from "@servicetitan/anvil2-ext-atlas";
function SystemMessageExample() {
const [selected, setSelected] = useState("");
return (
handleSubmit(selected)}
/>
);
}
```
### With Disabled Submit
Disable submit until selection is made:
```tsx theme={null}
import { SystemMessage } from "@servicetitan/anvil2-ext-atlas";
function SystemMessageDisabled() {
const [selected, setSelected] = useState("");
return (
handleSubmit(selected)}
buttonDisabled={!selected}
/>
);
}
```
```tsx theme={null}
```
## `SystemMessage` Props
Text to display on the submit button.
The prompt message to display.
Callback fired when the selected option changes.
Callback fired when the submit button is clicked.
Array of options to display as radio buttons.
The currently selected option value.
When true, disables the submit button.
Additional CSS class name for custom styling.
# User Message – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/messages/user-message/code
UserMessage displays messages sent by the user with a distinctive bubble style.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { UserMessage } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
function App() {
return (
console.log("retry clicked")}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Display a message from the user:
```tsx theme={null}
import { UserMessage } from "@servicetitan/anvil2-ext-atlas";
function UserMessageExample() {
return ;
}
```
### Error State with Retry
Display a failed message with retry option:
```tsx theme={null}
import { UserMessage } from "@servicetitan/anvil2-ext-atlas";
function UserMessageError() {
return (
resendMessage()}
/>
);
}
```
```tsx theme={null}
```
## `UserMessage` Props
The message content to display.
Additional CSS class name for custom styling.
When true, displays the message with error styling.
Callback for retry button. Only shown when `isError` is true.
# Notification Card – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/notification-card/code
NotificationCard displays notification items with title, message, and timestamp.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { NotificationCard } from "@servicetitan/anvil2-ext-atlas";
import { Card, Flex } from "@servicetitan/anvil2";
function App() {
return (
console.log("notification clicked")}
/>
console.log("notification clicked")}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Display a notification:
```tsx theme={null}
import { NotificationCard } from "@servicetitan/anvil2-ext-atlas";
function Notification() {
return (
openConversation()}
/>
);
}
```
### Read Notification
Display a read notification:
```tsx theme={null}
import { NotificationCard } from "@servicetitan/anvil2-ext-atlas";
function ReadNotification() {
return (
openConversation()}
/>
);
}
```
### Notification List
Display multiple notifications:
```tsx theme={null}
import { NotificationCard } from "@servicetitan/anvil2-ext-atlas";
function NotificationList({ notifications }) {
return (
{notifications.map((notification) => (
handleClick(notification.id)}
/>
))}
);
}
```
```tsx theme={null}
```
## `NotificationCard` Props
The notification message content (supports markdown).
The timestamp to display.
The notification title.
Callback fired when the notification card is clicked.
When true, shows the unread indicator and bolds the content.
# Confirmation Card – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/recommendations/confirmation-card/code
ConfirmationCard presents a simple confirmation prompt with action buttons.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { ConfirmationCard } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
function App() {
return (
console.log("submitted:", data)}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Simple confirmation prompt:
```tsx theme={null}
import { ConfirmationCard } from "@servicetitan/anvil2-ext-atlas";
function ConfirmationExample() {
return (
handleAction(response)}
/>
);
}
```
### With Link Action
Include a navigation link:
```tsx theme={null}
import { ConfirmationCard } from "@servicetitan/anvil2-ext-atlas";
function ConfirmationWithLink() {
return (
handleAction(response)}
/>
);
}
```
### Submitted State
Show disabled state after submission:
```tsx theme={null}
import { ConfirmationCard } from "@servicetitan/anvil2-ext-atlas";
function SubmittedExample() {
return (
{}}
submitted={true}
/>
);
}
```
```tsx theme={null}
```
## `ConfirmationCard` Props
Array of action buttons. Supports `type: "link"` with `url` for navigation.
The main prompt message to display.
Callback fired when an action is clicked. For link actions, includes `url` in parameters.
Additional CSS class name for custom styling.
Additional description text displayed in the card.
Unique identifier for this recommendation, included in the submit response.
When true, disables all action buttons.
## RecommendationAction Interface
```tsx theme={null}
interface RecommendationAction {
id: string;
name: string;
type?: "primary" | "secondary" | "ghost" | "danger" | "link";
url?: string;
}
```
## ConfirmationResponse Interface
```tsx theme={null}
interface ConfirmationResponse {
sourceMessageId?: string;
optionIds: string[];
actionId: string;
parameters?: Record;
}
```
# Multiple Recommendation Card – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/recommendations/multiple-recommendation-card/code
MultipleRecommendationCard presents a multi-select choice using checkboxes.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { MultipleRecommendationCard } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
function App() {
return (
console.log("submitted:", data)}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Present a multi-select choice:
```tsx theme={null}
import { MultipleRecommendationCard } from "@servicetitan/anvil2-ext-atlas";
function MultiSelectExample() {
const handleSubmit = (response) => {
console.log("Selected options:", response.optionIds);
};
return (
);
}
```
### With Multiple Actions
Include cancel option:
```tsx theme={null}
import { MultipleRecommendationCard } from "@servicetitan/anvil2-ext-atlas";
function MultiSelectWithCancel() {
return (
);
}
```
### Submitted State
Show previously selected values:
```tsx theme={null}
import { MultipleRecommendationCard } from "@servicetitan/anvil2-ext-atlas";
function SubmittedExample() {
return (
{}}
submitted={true}
selected={["opt-1", "opt-3"]}
/>
);
}
```
```tsx theme={null}
```
## `MultipleRecommendationCard` Props
Array of action buttons with `id`, `name`, and optional `type`.
The prompt message to display above the options.
Callback fired when an action button is clicked. Receives `sourceMessageId`, `optionIds`, and `actionId`.
Array of selectable options with `id`, `label`, and optional `description`.
Unique identifier for this recommendation, included in the submit response.
Array of selected option IDs to display when `submitted` is true.
When true, disables all inputs and shows the selected state.
## Options Interface
```tsx theme={null}
interface Options {
id: string;
label: string;
description?: string;
value?: string;
}
```
## RecommendationAction Interface
```tsx theme={null}
interface RecommendationAction {
id: string;
name: string;
type?: "primary" | "secondary" | "ghost" | "danger" | "link";
url?: string;
}
```
## ConfirmationResponse Interface
```tsx theme={null}
interface ConfirmationResponse {
sourceMessageId?: string;
optionIds: string[];
actionId: string;
parameters?: Record;
}
```
# Single Recommendation Card – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/recommendations/single-recommendation-card/code
SingleRecommendationCard presents a single-select choice using radio buttons.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { SingleRecommendationCard } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
function App() {
return (
console.log("submitted:", data)}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Present a single-select choice:
```tsx theme={null}
import { SingleRecommendationCard } from "@servicetitan/anvil2-ext-atlas";
function SingleSelectExample() {
const handleSubmit = (response) => {
console.log("Selected:", response.optionIds[0]);
console.log("Action:", response.actionId);
};
return (
);
}
```
### Submitted State
Show previously selected values:
```tsx theme={null}
import { SingleRecommendationCard } from "@servicetitan/anvil2-ext-atlas";
function SubmittedExample() {
return (
{}}
submitted={true}
selected={["opt-2"]}
/>
);
}
```
```tsx theme={null}
```
## `SingleRecommendationCard` Props
Array of action buttons with `id`, `name`, and optional `type`.
The prompt message to display above the options.
Callback fired when an action button is clicked. Receives `sourceMessageId`, `optionIds`, and `actionId`.
Array of selectable options with `id`, `label`, and optional `description`.
Unique identifier for this recommendation, included in the submit response.
Array of selected option IDs to display when `submitted` is true.
When true, disables all inputs and shows the selected state.
## Options Interface
```tsx theme={null}
interface Options {
id: string;
label: string;
description?: string;
value?: string;
}
```
## RecommendationAction Interface
```tsx theme={null}
interface RecommendationAction {
id: string;
name: string;
type?: "primary" | "secondary" | "ghost" | "danger" | "link";
url?: string;
}
```
## ConfirmationResponse Interface
```tsx theme={null}
interface ConfirmationResponse {
sourceMessageId?: string;
optionIds: string[];
actionId: string;
parameters?: Record;
}
```
# System Error – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/screens/system-error/code
SystemError is a full-screen error display for critical system failures.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { SystemError } from "@servicetitan/anvil2-ext-atlas";
import { Card, Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
Default Error
Custom Error
);
}
export default App;
```
## Common Examples
### Basic Usage
Display a system error state:
```tsx theme={null}
import { SystemError } from "@servicetitan/anvil2-ext-atlas";
function ErrorScreen() {
return (
);
}
```
### Custom Error Icon
Use a custom icon for the error state:
```tsx theme={null}
import { SystemError } from "@servicetitan/anvil2-ext-atlas";
import CloudOffIcon from "@servicetitan/anvil2/assets/icons/material/round/cloud_off.svg";
function OfflineError() {
return (
);
}
```
### Conditional Error Display
Show error based on application state:
```tsx theme={null}
import { SystemError, ChatWindow, Content } from "@servicetitan/anvil2-ext-atlas";
function AtlasApp() {
const [error, setError] = useState(null);
if (error) {
return (
);
}
return (
{/* messages */}
);
}
```
### Default Values
Using default title and description:
```tsx theme={null}
import { SystemError } from "@servicetitan/anvil2-ext-atlas";
function DefaultErrorScreen() {
// Uses default title "Something went wrong" and default description
return ;
}
```
```tsx theme={null}
```
## `SystemError` Props
Detailed description of the error or suggested actions.
Custom SVG icon to display. Defaults to the Material error icon.
Color for the icon.
Alternative to `description`. If provided, overrides the description.
The error title heading.
# Welcome – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/screens/welcome/code
Welcome is an onboarding screen displayed to new users before they begin chatting.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { Welcome } from "@servicetitan/anvil2-ext-atlas";
import { Card } from "@servicetitan/anvil2";
function App() {
return (
console.log("continue clicked")}
title="Welcome to Atlas"
subtitle="Your AI Assistant"
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Display the onboarding welcome screen:
```tsx theme={null}
import { Welcome } from "@servicetitan/anvil2-ext-atlas";
function WelcomeScreen() {
const [showWelcome, setShowWelcome] = useState(true);
if (!showWelcome) {
return ;
}
return (
setShowWelcome(false)}
/>
);
}
```
### Conditional Screen Display
Switch between welcome and chat states:
```tsx theme={null}
import { Welcome, ChatWindow, Content } from "@servicetitan/anvil2-ext-atlas";
function AtlasApp() {
const [showWelcome, setShowWelcome] = useState(true);
if (showWelcome) {
return (
{
setShowWelcome(false);
initializeChat();
}}
/>
);
}
return (
{/* messages */}
);
}
```
### With Test ID
Include test ID for automation:
```tsx theme={null}
import { Welcome } from "@servicetitan/anvil2-ext-atlas";
function WelcomeWithTestId() {
return (
);
}
```
```tsx theme={null}
```
## `Welcome` Props
Callback fired when the user clicks the Continue button.
Test ID for the continue button.
Secondary heading text displayed below the title.
Main heading text displayed on the welcome screen.
# Spinner – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/spinner/code
Spinner is a full-screen loading spinner for content areas.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { Spinner } from "@servicetitan/anvil2-ext-atlas";
import { Card, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
## Common Examples
### Basic Usage
Full-screen loading state:
```tsx theme={null}
import { Spinner } from "@servicetitan/anvil2-ext-atlas";
function ContentSpinner() {
return ;
}
```
### Conditional Loading
Show spinner while content loads:
```tsx theme={null}
import { Spinner } from "@servicetitan/anvil2-ext-atlas";
function ContentArea({ loading, children }) {
if (loading) {
return ;
}
return <>{children}>;
}
```
```tsx theme={null}
```
## `Spinner` Props
The Spinner component has no configurable props. It displays a centered spinning loader.
# Suggestion – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/suggestion/code
Suggestion and SuggestionList display clickable suggestion chips for common prompts.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { Suggestion, SuggestionList } from "@servicetitan/anvil2-ext-atlas";
import { Card, Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
Single Suggestion
console.log("suggestion clicked")}
/>
Suggestion List
console.log("selected:", suggestion)}
/>
);
}
export default App;
```
## Common Examples
### Single Suggestion
Display a single suggestion chip:
```tsx theme={null}
import { Suggestion } from "@servicetitan/anvil2-ext-atlas";
function SingleSuggestion() {
return (
sendMessage(text)}
/>
);
}
```
### Suggestion List
Display multiple suggestions:
```tsx theme={null}
import { SuggestionList } from "@servicetitan/anvil2-ext-atlas";
function MultipleSuggestions() {
const suggestions = [
"How do I schedule an appointment?",
"What are your business hours?",
"Tell me about your services",
];
return (
sendMessage(text)}
/>
);
}
```
### Dynamic Suggestions
Update suggestions based on context:
```tsx theme={null}
import { SuggestionList } from "@servicetitan/anvil2-ext-atlas";
function ContextualSuggestions({ topic }) {
const suggestions = useMemo(() => {
if (topic === "scheduling") {
return ["Book now", "See availability", "Reschedule"];
}
return ["Get started", "Learn more", "Contact us"];
}, [topic]);
return (
);
}
```
```tsx theme={null}
```
## `Suggestion` Props
Callback fired when the suggestion is clicked, receives the suggestion text.
The suggestion text to display and pass to the onClick callback.
```tsx theme={null}
```
## `SuggestionList` Props
Callback fired when a suggestion is selected.
Array of suggestion strings to display.
# Toolbox – Code
Source: https://anvil.servicetitan.com/docs/kits/atlas/components/toolbox/code
Toolbox provides action buttons for message interactions.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines expandable theme={null}
import { Toolbox, UserFeedback } from "@servicetitan/anvil2-ext-atlas";
import { Card, Flex, Text } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [feedback, setFeedback] = useState(UserFeedback.None);
return (
All Actions Available
console.log("retry clicked")}
onLike={async () => {
console.log("like clicked");
setFeedback(UserFeedback.Positive);
}}
onDislike={async () => {
console.log("dislike clicked");
setFeedback(UserFeedback.Negative);
}}
onFlag={async () => console.log("flag clicked")}
onPlay={async () => console.log("play clicked")}
currentFeedback={feedback}
/>
);
}
export default App;
```
## Common Examples
### Basic Usage
Message action toolbar:
```tsx theme={null}
import { Toolbox, UserFeedback } from "@servicetitan/anvil2-ext-atlas";
function MessageToolbox() {
const [feedback, setFeedback] = useState(UserFeedback.None);
return (
{
await submitFeedback("positive");
setFeedback(UserFeedback.Positive);
}}
onDislike={async () => {
await submitFeedback("negative");
setFeedback(UserFeedback.Negative);
}}
onRetry={async () => {
await regenerateResponse();
}}
/>
);
}
```
### With All Actions
Include all available actions:
```tsx theme={null}
import { Toolbox, UserFeedback } from "@servicetitan/anvil2-ext-atlas";
function FullToolbox() {
return (
);
}
```
### Copy Only
Simple copy functionality:
```tsx theme={null}
import { Toolbox } from "@servicetitan/anvil2-ext-atlas";
function CopyOnlyToolbox() {
return ;
}
```
```tsx theme={null}
```
## `Toolbox` Props
Current feedback state: `None`, `Positive`, or `Negative`. Affects which buttons are shown.
Async callback for dislike button. Button only renders when provided.
Async callback for flag button. Button only renders when provided.
Async callback for like button. Button only renders when provided.
Async callback for play audio button. Button only renders when provided.
Async callback for retry button. Button only renders when provided.
The text content to copy when the copy button is clicked.
## UserFeedback Enum
```tsx theme={null}
enum UserFeedback {
None = "None",
Positive = "Positive",
Negative = "Negative",
}
```
# Hooks
Source: https://anvil.servicetitan.com/docs/kits/atlas/hooks/index
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
# useDraggable
Source: https://anvil.servicetitan.com/docs/kits/atlas/hooks/use-draggable
A hook for making the chat window draggable within the viewport.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
A hook for making the chat window draggable within the viewport.
## Usage
```tsx theme={null}
import { ChatWindow, Header, useDraggable } from "@servicetitan/anvil2-ext-atlas";
function DraggableChatWindow() {
const {
position,
isDragging,
handleMouseDown,
resetPosition,
} = useDraggable({
initialX: window.innerWidth - 390,
initialY: 64,
width: 390,
});
return (
{/* Content */}
);
}
```
## Options
| Option | Type | Default | Description |
| -------------------- | ------ | ------------------------ | -------------------------------------------------- |
| initialX | number | window\.innerWidth - 390 | Initial horizontal position |
| initialY | number | 64 | Initial vertical position |
| minVisibleHorizontal | number | 390 | Minimum pixels visible horizontally when near edge |
| minVisibleVertical | number | 56 | Minimum pixels visible vertically when near edge |
| width | number | 390 | Width of the draggable element |
## Returns
| Property | Type | Description |
| --------------- | -------------------------- | ---------------------------------------------- |
| position | `{ x: number; y: number }` | Current position coordinates |
| isDragging | `boolean` | Whether the element is currently being dragged |
| handleMouseDown | `(e: MouseEvent) => void` | Handler to attach to the drag trigger element |
| resetPosition | `() => void` | Resets position to calculated initial position |
## Behavior
* **Viewport Constraints**: Position is constrained to keep the minimum visible pixels within the viewport
* **Resize Handling**: Automatically adjusts position when window is resized
* **Text Selection**: Prevents text selection while dragging
* **Mouse Events**: Attaches global mouse events during drag for smooth tracking
## Example: Reset on Fullscreen Exit
```tsx theme={null}
function ChatWithReset() {
const [fullscreen, setFullscreen] = useState(false);
const { position, isDragging, handleMouseDown, resetPosition } = useDraggable();
const handleFullscreenToggle = () => {
if (fullscreen) {
resetPosition(); // Reset to default position when exiting fullscreen
}
setFullscreen(!fullscreen);
};
return (
);
}
```
# useInfiniteScroll
Source: https://anvil.servicetitan.com/docs/kits/atlas/hooks/use-infinite-scroll
A hook for implementing infinite scroll loading behavior using Intersection Observer.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
A hook for implementing infinite scroll loading behavior using Intersection Observer.
## Usage
```tsx theme={null}
import { useInfiniteScroll } from "@servicetitan/anvil2-ext-atlas";
function InfiniteList() {
const [items, setItems] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const loadMore = async () => {
setIsLoading(true);
const newItems = await fetchItems();
setItems((prev) => [...prev, ...newItems]);
setHasMore(newItems.length > 0);
setIsLoading(false);
};
const { sentinelRef } = useInfiniteScroll({
hasMore,
isLoading,
onLoadMore: loadMore,
rootMargin: "100px",
threshold: 0.1,
});
return (
{items.map((item) => (
))}
{hasMore &&
}
);
}
```
## Options
| Option | Type | Default | Description |
| ---------- | ---------- | ------- | --------------------------------------------------- |
| hasMore | boolean | - | Whether there is more content to load |
| isLoading | boolean | - | Whether content is currently being loaded |
| onLoadMore | () => void | - | Callback to load more content |
| rootMargin | string | "100px" | Margin around the root for intersection calculation |
| threshold | number | 0.1 | Visibility threshold to trigger loading |
## Returns
| Property | Type | Description |
| ----------- | -------------------------- | ------------------------------------- |
| sentinelRef | RefObject\
| Ref to attach to the sentinel element |
## Behavior
* **Intersection Observer**: Uses native browser API for efficient scroll detection
* **Load Gating**: Only triggers `onLoadMore` when `hasMore` is true and `isLoading` is false
* **Cleanup**: Properly disconnects observer on unmount or option changes
## Example: With InfiniteContent Component
The `InfiniteContent` component uses this hook internally, but you can use the hook directly for custom implementations:
```tsx theme={null}
import { useInfiniteScroll, Spinner } from "@servicetitan/anvil2-ext-atlas";
function CustomInfiniteScroll() {
const [messages, setMessages] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const { sentinelRef } = useInfiniteScroll({
hasMore,
isLoading: loading,
onLoadMore: async () => {
setLoading(true);
const older = await fetchOlderMessages();
setMessages((prev) => [...older, ...prev]);
setHasMore(older.length === 20);
setLoading(false);
},
rootMargin: "200px",
});
return (
{hasMore &&
{loading && }
}
{messages.map((msg) => (
))}
);
}
```
# useScrollCallback
Source: https://anvil.servicetitan.com/docs/kits/atlas/hooks/use-scroll-callback
A hook for responding to scroll position changes with callbacks.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
A hook for responding to scroll position changes with callbacks for scrolling down and returning to top.
## Usage
```tsx theme={null}
import { useScrollCallback } from "@servicetitan/anvil2-ext-atlas";
function ScrollAwareComponent() {
const containerRef = useRef(null);
const [showScrollToTop, setShowScrollToTop] = useState(false);
useScrollCallback({
element: containerRef.current,
threshold: 100,
onScrollDown: () => setShowScrollToTop(true),
onScrollTop: () => setShowScrollToTop(false),
});
return (
{/* Scrollable content */}
{showScrollToTop && (
scrollToTop()}>Back to Top
)}
);
}
```
## Options
| Option | Type | Default | Description |
| ------------ | ----------- | ------- | --------------------------------------- |
| element | HTMLElement | null | The scrollable element to monitor |
| threshold | number | 50 | Pixel threshold to trigger onScrollDown |
| onScrollDown | () => void | - | Callback when scrolled past threshold |
| onScrollTop | () => void | - | Callback when scrolled back to top |
## Returns
| Property | Type | Description |
| ---------------------- | ------- | --------------------------------------- |
| lastScrollTop | number | Last recorded scroll position |
| hasTriggeredScrollDown | boolean | Whether onScrollDown has been triggered |
## Behavior
* **One-time Trigger**: `onScrollDown` only fires once per scroll session (until returning to top)
* **Top Detection**: `onScrollTop` fires when scroll position returns to 0
* **Passive Listener**: Uses passive event listener for better scroll performance
* **Window Fallback**: Falls back to window scroll if no element is provided
## Example: Show/Hide Header on Scroll
```tsx theme={null}
import { useScrollCallback } from "@servicetitan/anvil2-ext-atlas";
function ChatWithCollapsibleHeader() {
const contentRef = useRef(null);
const [headerCollapsed, setHeaderCollapsed] = useState(false);
useScrollCallback({
element: contentRef.current,
threshold: 50,
onScrollDown: () => setHeaderCollapsed(true),
onScrollTop: () => setHeaderCollapsed(false),
});
return (
);
}
```
## Example: Lazy Load Images on Scroll
```tsx theme={null}
import { useScrollCallback } from "@servicetitan/anvil2-ext-atlas";
function LazyImageGallery() {
const galleryRef = useRef(null);
const [imagesLoaded, setImagesLoaded] = useState(false);
useScrollCallback({
element: galleryRef.current,
threshold: 200,
onScrollDown: () => {
if (!imagesLoaded) {
loadHighResImages();
setImagesLoaded(true);
}
},
});
return (
{images.map((img) => (
))}
);
}
```
# Atlas
Source: https://anvil.servicetitan.com/docs/kits/atlas/index
Components and utilities for building the Atlas AI chat experience.
Atlas is deprecated. Use [AI Kit](/docs/kits/ai-kit) for any new Atlas experiences.
```tsx lines theme={null}
import {
ChatWindow,
Header,
Footer,
Content,
MarkdownMessage,
UserMessage,
SuggestionList,
NotificationCard,
UserFeedback,
} from "@servicetitan/anvil2-ext-atlas";
import { Text } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [message1, setMessage1] = useState("");
const [message2, setMessage2] = useState("");
return (
console.log("close")} />
console.log("like"),
onDislike: async () => console.log("dislike"),
currentFeedback: UserFeedback.None,
}}
/>
Thank you for your feedback!
console.log("selected:", s)}
/>
console.log("submit")}
placeholder="Ask Atlas"
/>
console.log("close")} />
console.log("notification clicked")}
/>
console.log("notification clicked")}
/>
console.log("submit")}
placeholder="Ask Atlas"
/>
);
}
export default App;
```
Atlas is an extended component library designed for building AI-powered chat interfaces. It provides a comprehensive set of components for creating conversational UI experiences, including chat windows, message displays, input composers, and recommendation cards.
## Installation
Atlas components are available from the `@servicetitan/anvil2-ext-atlas` package:
```bash theme={null}
npm install @servicetitan/anvil2-ext-atlas
```
## Import
```tsx theme={null}
import {
ChatWindow,
Header,
Footer,
Content,
AssistantMessage,
UserMessage,
MarkdownMessage,
} from "@servicetitan/anvil2-ext-atlas";
```
## Key Features
### Chat Window System
The core chat experience is built around composable components that work together:
* **ChatWindow** - The main container with open/close animations and positioning
* **Header** - Configurable header with drag, fullscreen, and navigation controls
* **Footer** - Message input area with the rich text composer
* **Content** - Scrollable message container with auto-scroll behavior
### Message Components
Display different types of messages in the conversation:
* **UserMessage** - Messages sent by the user
* **AssistantMessage** - Simple text responses from Atlas
* **MarkdownMessage** - Rich markdown-formatted responses with toolbox actions
* **SystemMessage** - Interactive system prompts with radio options
* **ErrorMessage** - Error states with retry functionality
### Recommendation Cards
Present actionable recommendations to users:
* **SingleRecommendationCard** - Single-select options with radio buttons
* **MultipleRecommendationCard** - Multi-select options with checkboxes
* **ConfirmationCard** - Simple confirmation prompts with action buttons
### Utility Components
Supporting components for common patterns:
* **Suggestion** / **SuggestionList** - Clickable suggestion chips
* **Toolbox** - Copy, like, dislike, and retry actions for messages
* **Loader** - Animated loading indicator for pending responses
* **Spinner** - Full-screen loading spinner
* **Welcome** - Onboarding welcome screen
* **SystemError** - Error state display
### Hooks
React hooks for managing chat window behavior:
* **useDraggable** - Make the chat window draggable with boundary constraints
* **useInfiniteScroll** - Load more content when scrolling
* **useScrollCallback** - Respond to scroll position changes
## Component Architecture
The Atlas components are designed to be composed together to create a complete chat experience:
```tsx theme={null}
import {
ChatWindow,
Header,
Content,
Footer,
UserMessage,
MarkdownMessage,
} from "@servicetitan/anvil2-ext-atlas";
function AtlasChat() {
const [open, setOpen] = useState(false);
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState("");
return (
setOpen(false)}
onCreateNewChat={handleNewChat}
/>
{messages.map((msg) =>
msg.type === "user" ? (
) : (
)
)}
);
}
```
## Dependencies
Atlas components are built on top of the core Anvil2 component library and include:
* **Anvil2** - Core UI components (Button, Card, Flex, Text, etc.)
* **Framer Motion** - Animations and transitions
* **React Markdown** - Markdown rendering for message content
* **MobX React** - Observable state management (optional)
# Bar Charts
Source: https://anvil.servicetitan.com/docs/kits/charts/bar-charts
Create vertical, horizontal, grouped, and stacked bar charts using Anvil2 themes with amCharts 5.
Bar charts use the amCharts 5 `XYChart` and `ColumnSeries` classes. All three Anvil2 themes apply design-token-based colors and chart styling automatically.
For design guidelines on bar charts, see [Bar Charts design guidelines](/docs/web/data-visualization/bar-charts).
## Vertical bar chart
A basic vertical bar chart uses a `CategoryAxis` on the x-axis and a `ValueAxis` on the y-axis.
```tsx lines expandable theme={null}
import { useLayoutEffect } from "react";
import * as am5 from "@amcharts/amcharts5";
import am5themes_Animated from "@amcharts/amcharts5/themes/Animated";
import * as am5xy from "@amcharts/amcharts5/xy";
import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";
function App() {
useLayoutEffect(() => {
const data = [
{ country: "USA", value: 2025 },
{ country: "China", value: 1882 },
{ country: "Japan", value: 1809 },
{ country: "Germany", value: 1322 },
{ country: "UK", value: 1122 },
];
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root), ThemeMonochrome.new(root)]);
const chart = root.container.children.push(
am5xy.XYChart.new(root, {
panX: true,
panY: true,
wheelX: "panX",
wheelY: "zoomX",
paddingLeft: 0,
paddingRight: 1,
}),
);
// X axis (categories)
const xRenderer = am5xy.AxisRendererX.new(root, {
minGridDistance: 30,
minorGridEnabled: true,
});
xRenderer.grid.template.setAll({ location: 1 });
const xAxis = chart.xAxes.push(
am5xy.CategoryAxis.new(root, {
maxDeviation: 0.3,
categoryField: "country",
renderer: xRenderer,
tooltip: am5.Tooltip.new(root, {}),
}),
);
xAxis.data.setAll(data);
// Y axis (values)
const yAxis = chart.yAxes.push(
am5xy.ValueAxis.new(root, {
maxDeviation: 0.3,
renderer: am5xy.AxisRendererY.new(root, { strokeOpacity: 0.1 }),
}),
);
// Series
const series = chart.series.push(
am5xy.ColumnSeries.new(root, {
name: "Series 1",
xAxis: xAxis,
yAxis: yAxis,
valueYField: "value",
sequencedInterpolation: true,
categoryXField: "country",
}),
);
series.columns.template.setAll({
tooltipText: "{categoryX}: {valueY}",
tooltipY: am5.percent(10),
});
series.data.setAll(data);
return () => root.dispose();
}, []);
return
;
}
export default App;
```
## Horizontal bar chart
To create a horizontal bar chart, swap the axes: use a `CategoryAxis` on the y-axis and a `ValueAxis` on the x-axis. Set `categoryYField` and `valueXField` on the series instead of `categoryXField` and `valueYField`.
```tsx lines expandable theme={null}
import { useLayoutEffect } from "react";
import * as am5 from "@amcharts/amcharts5";
import am5themes_Animated from "@amcharts/amcharts5/themes/Animated";
import * as am5xy from "@amcharts/amcharts5/xy";
import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";
function App() {
useLayoutEffect(() => {
// Reverse data so highest values appear at the top
const data = [
{ country: "USA", value: 2025 },
{ country: "China", value: 1882 },
{ country: "Japan", value: 1809 },
{ country: "Germany", value: 1322 },
{ country: "UK", value: 1122 },
].reverse();
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root), ThemeMonochrome.new(root)]);
const chart = root.container.children.push(
am5xy.XYChart.new(root, {
panX: true,
panY: true,
wheelX: "panY",
wheelY: "zoomY",
paddingLeft: 0,
paddingRight: 1,
}),
);
// Y axis (categories — swapped from vertical)
const yRenderer = am5xy.AxisRendererY.new(root, {
minGridDistance: 30,
minorGridEnabled: true,
});
yRenderer.grid.template.setAll({ location: 1 });
const yAxis = chart.yAxes.push(
am5xy.CategoryAxis.new(root, {
maxDeviation: 0.3,
categoryField: "country",
renderer: yRenderer,
tooltip: am5.Tooltip.new(root, {}),
}),
);
yAxis.data.setAll(data);
// X axis (values — swapped from vertical)
const xAxis = chart.xAxes.push(
am5xy.ValueAxis.new(root, {
maxDeviation: 0.3,
renderer: am5xy.AxisRendererX.new(root, { strokeOpacity: 0.1 }),
}),
);
// Series — uses categoryYField and valueXField for horizontal orientation
const series = chart.series.push(
am5xy.ColumnSeries.new(root, {
name: "Series 1",
xAxis: xAxis,
yAxis: yAxis,
valueXField: "value",
sequencedInterpolation: true,
categoryYField: "country",
}),
);
series.columns.template.setAll({
tooltipText: "{categoryY}: {valueX}",
tooltipY: am5.percent(90),
});
series.data.setAll(data);
return () => root.dispose();
}, []);
return
;
}
export default App;
```
The theme detects horizontal orientation when `categoryYField` or `valueXField` is set on the series.
## Grouped bar chart
Grouped bar charts display multiple series side by side for each category. Create a separate `ColumnSeries` for each data group.
```tsx lines expandable theme={null}
import { useLayoutEffect } from "react";
import * as am5 from "@amcharts/amcharts5";
import am5themes_Animated from "@amcharts/amcharts5/themes/Animated";
import * as am5xy from "@amcharts/amcharts5/xy";
import { ThemeCategorical } from "@servicetitan/anvil2-ext-charts/am5";
function App() {
useLayoutEffect(() => {
const data = [
{ year: "2021", europe: 2.5, namerica: 2.5, asia: 2.1, lamerica: 1.0 },
{ year: "2022", europe: 2.6, namerica: 2.7, asia: 2.2, lamerica: 0.5 },
{ year: "2023", europe: 2.8, namerica: 2.9, asia: 2.4, lamerica: 0.3 },
];
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root), ThemeCategorical.new(root)]);
const chart = root.container.children.push(
am5xy.XYChart.new(root, {
panX: false,
panY: false,
paddingLeft: 0,
wheelX: "panX",
wheelY: "zoomX",
layout: root.verticalLayout,
}),
);
// Legend
const legend = chart.children.push(
am5.Legend.new(root, {
centerX: am5.p50,
x: am5.p50,
}),
);
// X axis with cell padding for grouped columns
const xRenderer = am5xy.AxisRendererX.new(root, {
cellStartLocation: 0.1,
cellEndLocation: 0.9,
minorGridEnabled: true,
});
xRenderer.grid.template.setAll({ location: 1 });
const xAxis = chart.xAxes.push(
am5xy.CategoryAxis.new(root, {
categoryField: "year",
renderer: xRenderer,
tooltip: am5.Tooltip.new(root, {}),
}),
);
xAxis.data.setAll(data);
const yAxis = chart.yAxes.push(
am5xy.ValueAxis.new(root, {
renderer: am5xy.AxisRendererY.new(root, { strokeOpacity: 0.1 }),
}),
);
// Create one series per data group
function makeSeries(name: string, fieldName: string) {
const series = chart.series.push(
am5xy.ColumnSeries.new(root, {
name: name,
xAxis: xAxis,
yAxis: yAxis,
valueYField: fieldName,
categoryXField: "year",
}),
);
series.columns.template.setAll({
tooltipText: "{name}, {categoryX}: {valueY}",
width: am5.percent(90),
tooltipY: 0,
});
series.data.setAll(data);
legend.data.push(series);
}
makeSeries("Europe", "europe");
makeSeries("North America", "namerica");
makeSeries("Asia", "asia");
makeSeries("Latin America", "lamerica");
return () => root.dispose();
}, []);
return
;
}
export default App;
```
## Stacked bar chart
Stacked bar charts show how parts contribute to a whole. Set `stacked: true` on each `ColumnSeries`.
```tsx lines expandable theme={null}
import { useLayoutEffect } from "react";
import * as am5 from "@amcharts/amcharts5";
import am5themes_Animated from "@amcharts/amcharts5/themes/Animated";
import * as am5xy from "@amcharts/amcharts5/xy";
import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";
function App() {
useLayoutEffect(() => {
const data = [
{ year: "2021", europe: 2.5, namerica: 2.5, asia: 2.1, lamerica: 1.0 },
{ year: "2022", europe: 2.6, namerica: 2.7, asia: 2.2, lamerica: 0.5 },
{ year: "2023", europe: 2.8, namerica: 2.9, asia: 2.4, lamerica: 0.3 },
];
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root), ThemeMonochrome.new(root)]);
const chart = root.container.children.push(
am5xy.XYChart.new(root, {
panX: false,
panY: false,
wheelX: "panX",
wheelY: "zoomX",
paddingLeft: 0,
layout: root.verticalLayout,
}),
);
const legend = chart.children.push(
am5.Legend.new(root, {
centerX: am5.p50,
x: am5.p50,
}),
);
const xRenderer = am5xy.AxisRendererX.new(root, {
cellStartLocation: 0.1,
cellEndLocation: 0.9,
minorGridEnabled: true,
});
xRenderer.grid.template.setAll({ location: 1 });
const xAxis = chart.xAxes.push(
am5xy.CategoryAxis.new(root, {
categoryField: "year",
renderer: xRenderer,
tooltip: am5.Tooltip.new(root, {}),
}),
);
xAxis.data.setAll(data);
const yAxis = chart.yAxes.push(
am5xy.ValueAxis.new(root, {
min: 0,
renderer: am5xy.AxisRendererY.new(root, { strokeOpacity: 0.1 }),
}),
);
// Create stacked series — set stacked: true on each ColumnSeries
function makeSeries(name: string, fieldName: string) {
const series = chart.series.push(
am5xy.ColumnSeries.new(root, {
stacked: true,
name: name,
xAxis: xAxis,
yAxis: yAxis,
valueYField: fieldName,
categoryXField: "year",
}),
);
series.columns.template.setAll({
tooltipText: "{name}, {categoryX}: {valueY}",
tooltipY: am5.percent(10),
});
series.data.setAll(data);
legend.data.push(series);
}
makeSeries("Europe", "europe");
makeSeries("North America", "namerica");
makeSeries("Asia", "asia");
makeSeries("Latin America", "lamerica");
return () => root.dispose();
}, []);
return
;
}
export default App;
```
## Using different themes
Change the theme import to apply a different color palette. The chart structure stays the same.
### Monochrome theme
Use `ThemeMonochrome` for data with a natural order or progression, or when displaying 4 or fewer variables. This is the default theme used in the examples above.
```tsx theme={null}
import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";
root.setThemes([
am5themes_Animated.new(root),
ThemeMonochrome.new(root),
]);
```
### Categorical theme
Use `ThemeCategorical` when displaying 5 or more distinct series that need maximum color differentiation.
```tsx theme={null}
import { ThemeCategorical } from "@servicetitan/anvil2-ext-charts/am5";
root.setThemes([
am5themes_Animated.new(root),
ThemeCategorical.new(root),
]);
```
The categorical palette colors do not all meet 3:1 contrast against each other. When using `ThemeCategorical`, include direct labeling on bars to meet [accessibility requirements](/docs/web/data-visualization/accessibility).
### Semantic theme
Use `ThemeSemantic` when data represents status values. Colors follow the order: Success, Neutral, Warning, Danger.
```tsx theme={null}
import { ThemeSemantic } from "@servicetitan/anvil2-ext-charts/am5";
root.setThemes([
am5themes_Animated.new(root),
ThemeSemantic.new(root),
]);
```
## Tooltips
The theme applies tooltips that match the Anvil2 [Tooltip](/docs/web/components/tooltip/design) styling across all chart types.
Customize the tooltip text format on the series:
```tsx theme={null}
series.columns.template.set("tooltipText", "{categoryX}: {valueY}");
```
# Donut Charts
Source: https://anvil.servicetitan.com/docs/kits/charts/donut-charts
Create donut and pie charts with legends using Anvil2 themes and amCharts 5.
Donut and pie charts use the amCharts 5 `PieChart` and `PieSeries` classes. All three Anvil2 themes apply design-token-based colors and chart styling automatically.
For design guidelines on donut chart spacing, direct labeling, and variable ordering, see [Donut Charts design guidelines](/docs/web/data-visualization/donut-charts).
## Basic donut chart
A donut chart is a `PieChart` with an `innerRadius` set. The theme styles slice labels with a white semi-transparent background and pointer cursors, while this example configures the label text to show percentages inside each slice.
```tsx lines expandable theme={null}
import { useLayoutEffect } from "react";
import * as am5 from "@amcharts/amcharts5";
import am5themes_Animated from "@amcharts/amcharts5/themes/Animated";
import * as am5percent from "@amcharts/amcharts5/percent";
import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";
function App() {
useLayoutEffect(() => {
const data = [
{ category: "Lithuania", value: 501.9 },
{ category: "Czechia", value: 301.9 },
{ category: "Ireland", value: 201.1 },
{ category: "Germany", value: 165.8 },
];
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root), ThemeMonochrome.new(root)]);
const chart = root.container.children.push(
am5percent.PieChart.new(root, {
layout: root.horizontalLayout,
innerRadius: am5.percent(50),
}),
);
const series = chart.series.push(
am5percent.PieSeries.new(root, {
name: "Series",
valueField: "value",
categoryField: "category",
legendValueText: "",
}),
);
// Display percentage labels on slices
series.labels.template.set(
"text",
"{valuePercentTotal.formatNumber('#.')}%",
);
series.data.setAll(data);
return () => root.dispose();
}, []);
return
;
}
export default App;
```
## Adding a legend
Add a legend to display category names alongside the chart. Set the chart layout to `horizontalLayout` and position the legend vertically beside the donut.
```tsx lines expandable theme={null}
import { useLayoutEffect } from "react";
import * as am5 from "@amcharts/amcharts5";
import am5themes_Animated from "@amcharts/amcharts5/themes/Animated";
import * as am5percent from "@amcharts/amcharts5/percent";
import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";
function App() {
useLayoutEffect(() => {
const data = [
{ category: "Lithuania", value: 501.9 },
{ category: "Czechia", value: 301.9 },
{ category: "Ireland", value: 201.1 },
{ category: "Germany", value: 165.8 },
];
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root), ThemeMonochrome.new(root)]);
// Use horizontalLayout so the legend appears to the right of the chart
const chart = root.container.children.push(
am5percent.PieChart.new(root, {
layout: root.horizontalLayout,
innerRadius: am5.percent(50),
}),
);
const series = chart.series.push(
am5percent.PieSeries.new(root, {
name: "Series",
valueField: "value",
categoryField: "category",
legendValueText: "",
}),
);
series.labels.template.set(
"text",
"{valuePercentTotal.formatNumber('#.')}%",
);
series.data.setAll(data);
// Add a vertical legend to the right of the chart
const legend = chart.children.push(
am5.Legend.new(root, {
centerY: am5.percent(50),
y: am5.percent(50),
layout: root.verticalLayout,
}),
);
legend.data.setAll(series.dataItems);
return () => root.dispose();
}, []);
return
;
}
export default App;
```
All three Anvil2 themes set `clickTarget: "none"` on legends. This disables the default amCharts behavior where clicking a legend item toggles the corresponding series on and off. If you need clickable legends, override this setting after applying the theme: `legend.set("clickTarget", "itemContainer")`.
## Hover behavior
The theme registers `pointerover` and `pointerout` event handlers on all `Slice` elements. When a user hovers over a slice, all sibling slices dim to 20% opacity. This behavior applies to every pie and donut chart using the theme and cannot be disabled without overriding the theme rules.
## Pie chart (without inner radius)
The [design guidelines](/docs/web/data-visualization/donut-charts#5-donut-vs-pie) recommend donut charts over pie charts for dashboard layouts. Donut charts provide a cleaner layout and allow central labeling of key metrics. Use a pie chart only when the open center is not needed.
To create a standard pie chart instead of a donut, omit the `innerRadius` property:
```tsx theme={null}
const chart = root.container.children.push(
am5percent.PieChart.new(root, {
layout: root.horizontalLayout,
// No innerRadius — renders as a full pie chart
}),
);
```
## Using different themes
Change the theme import to apply a different color palette. The chart structure stays the same.
### Monochrome theme
Use `ThemeMonochrome` for data with a natural order or progression, or when displaying 4 or fewer variables. This is the default theme used in the examples above.
```tsx theme={null}
import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";
root.setThemes([
am5themes_Animated.new(root),
ThemeMonochrome.new(root),
]);
```
### Categorical theme
Use `ThemeCategorical` when displaying 5 or more distinct categories that need maximum color differentiation.
```tsx theme={null}
import { ThemeCategorical } from "@servicetitan/anvil2-ext-charts/am5";
root.setThemes([
am5themes_Animated.new(root),
ThemeCategorical.new(root),
]);
```
The categorical palette colors do not all meet 3:1 contrast against each other. When using `ThemeCategorical`, include direct labeling on slices to meet [accessibility requirements](/docs/web/data-visualization/accessibility). The theme applies percentage labels by default, which satisfies this requirement for most cases.
### Semantic theme
Use `ThemeSemantic` when data represents status values. Colors follow the order: Success, Neutral, Warning, Danger.
```tsx theme={null}
import { ThemeSemantic } from "@servicetitan/anvil2-ext-charts/am5";
root.setThemes([
am5themes_Animated.new(root),
ThemeSemantic.new(root),
]);
```
## Tooltips
The theme applies tooltips that match the Anvil2 [Tooltip](/docs/web/components/tooltip/design) styling across all chart types.
Customize the tooltip text format on the series:
```tsx theme={null}
// The tooltip is automatically created by the theme on PieChart
// Customize the text format on the series instead:
series.slices.template.set("tooltipText", "{category}: {value}");
```
# Getting Started
Source: https://anvil.servicetitan.com/docs/kits/charts/getting-started
Learn the basics of amCharts 5 and how to apply Anvil2 themes to your charts in React.
This guide covers the amCharts 5 fundamentals you need to create charts with Anvil2 themes. amCharts 5 is an imperative DOM-based library, so using it in React requires manual lifecycle management that differs from typical React component patterns.
For comprehensive amCharts 5 documentation, visit [amcharts.com/docs/v5](https://www.amcharts.com/docs/v5/).
## amCharts 5 core concepts
### The root element
Every amCharts 5 chart starts with a **root element**. The root manages the chart lifecycle and is required to create every other amCharts object. Create it by passing the ID of a container ``:
```tsx theme={null}
import * as am5 from "@amcharts/amcharts5";
const root = am5.Root.new("chartdiv");
```
### The `.new()` pattern
amCharts 5 uses a static `.new()` method instead of the `new` keyword to create instances. The first argument is always the root element:
```tsx theme={null}
// Create a chart
const chart = am5percent.PieChart.new(root, {
layout: root.horizontalLayout,
innerRadius: am5.percent(50),
});
// Create a series
const series = am5percent.PieSeries.new(root, {
valueField: "value",
categoryField: "category",
});
```
### Setting data
Use `setAll()` to bind data to a series. Pass an array of objects where keys match the field names you configured on the series:
```tsx theme={null}
series.data.setAll([
{ category: "Category A", value: 501 },
{ category: "Category B", value: 302 },
{ category: "Category C", value: 201 },
]);
```
### Themes
Themes control the visual appearance of chart elements. Apply themes to the root using `setThemes()`. You can combine multiple themes, and order matters — later themes override earlier ones:
```tsx theme={null}
import am5themes_Animated from "@amcharts/amcharts5/themes/Animated";
import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";
root.setThemes([
am5themes_Animated.new(root), // Adds animations
ThemeMonochrome.new(root), // Applies Anvil2 styles
]);
```
## Using Anvil2 themes in React
### Basic setup pattern
amCharts 5 manages its own DOM, which does not fit naturally into React's rendering model. Create the root in a `useLayoutEffect` and dispose of it on cleanup to prevent memory leaks.
In React Strict Mode, effects can run twice in development, so it's important to dispose the chart root in the cleanup function to avoid duplicate chart instances and visual glitches:
```tsx lines expandable theme={null}
import { useLayoutEffect } from "react";
import * as am5 from "@amcharts/amcharts5";
import am5themes_Animated from "@amcharts/amcharts5/themes/Animated";
import * as am5percent from "@amcharts/amcharts5/percent";
import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";
function App() {
useLayoutEffect(() => {
const data = [
{ category: "Lithuania", value: 501.9 },
{ category: "Czechia", value: 301.9 },
{ category: "Ireland", value: 201.1 },
{ category: "Germany", value: 165.8 },
];
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root), ThemeMonochrome.new(root)]);
const chart = root.container.children.push(
am5percent.PieChart.new(root, {
layout: root.horizontalLayout,
innerRadius: am5.percent(50),
}),
);
const series = chart.series.push(
am5percent.PieSeries.new(root, {
name: "Series",
valueField: "value",
categoryField: "category",
legendValueText: "",
}),
);
// Display percentage labels on slices
series.labels.template.set(
"text",
"{valuePercentTotal.formatNumber('#.')}%",
);
series.data.setAll(data);
return () => root.dispose();
}, []);
return
;
}
export default App;
```
### Switching themes
To use a different Anvil2 theme, swap the import:
```tsx theme={null}
// Monochrome — ordered/progressive data (default, 4 or fewer variables)
import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";
// Categorical — 5+ distinct categories
import { ThemeCategorical } from "@servicetitan/anvil2-ext-charts/am5";
// Semantic — status-based data (success, neutral, warning, danger)
import { ThemeSemantic } from "@servicetitan/anvil2-ext-charts/am5";
```
### Combining with amCharts built-in themes
Combine the Anvil2 themes with amCharts built-in themes for additional functionality. The `Animated` theme adds smooth transitions:
```tsx theme={null}
import am5themes_Animated from "@amcharts/amcharts5/themes/Animated";
import { ThemeCategorical } from "@servicetitan/anvil2-ext-charts/am5";
root.setThemes([
am5themes_Animated.new(root),
ThemeCategorical.new(root), // Applied last so Anvil2 styles take precedence
]);
```
Theme order matters. The Anvil2 theme must come after amCharts built-in themes in the array. If you place it first, the built-in theme will override Anvil2's color palette, tooltip styling, and other rules.
## Chart modules
amCharts 5 organizes chart types into separate modules to keep bundle sizes small. Import only the modules you need:
| Module | Import | Chart types |
| ----------- | ------------------------------------- | ----------------------------------------------------------- |
| **Core** | `@amcharts/amcharts5` | Root, Legend, Tooltip, Label, ColorSet |
| **Percent** | `@amcharts/amcharts5/percent` | PieChart, PieSeries (donut/pie charts) |
| **XY** | `@amcharts/amcharts5/xy` | XYChart, ColumnSeries, CategoryAxis, ValueAxis (bar charts) |
| **Themes** | `@amcharts/amcharts5/themes/Animated` | Built-in animation theme |
## Next steps
* [Bar Charts](/docs/kits/charts/bar-charts) — Vertical, horizontal, grouped, and stacked bar chart examples
* [Donut Charts](/docs/kits/charts/donut-charts) — Pie and donut chart examples with legends
* [Data Visualization Guidelines](/docs/web/data-visualization) — Design guidelines for color palettes, variable ordering, and chart anatomy
* [amCharts 5 Documentation](https://www.amcharts.com/docs/v5/) — Full amCharts 5 reference
# Charts
Source: https://anvil.servicetitan.com/docs/kits/charts/index
Anvil2 themes for amCharts 5 data visualization, providing consistent color palettes and styling for bar charts and donut charts.
The Charts extended library provides [amCharts 5](https://www.amcharts.com/docs/v5/) themes that apply Anvil2 design tokens to bar charts and donut charts. These themes are narrow in scope — they style specific chart types rather than acting as general-purpose amCharts themes. Additional chart types will be supported in future releases.
For design guidelines on color palettes, variable ordering, and chart anatomy, see the [Data Visualization design guidelines](/docs/web/data-visualization).
## Installation
The Charts library requires `@amcharts/amcharts5` as a peer dependency.
```bash theme={null}
npm install @servicetitan/anvil2-ext-charts @amcharts/amcharts5
```
## Import
All themes are available from the `@servicetitan/anvil2-ext-charts/am5` entry point.
```tsx theme={null}
import {
ThemeCategorical,
ThemeMonochrome,
ThemeSemantic,
} from "@servicetitan/anvil2-ext-charts/am5";
```
## Themes
The library provides three themes, each suited to different data visualization scenarios.
| Theme | Use case | Color order |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Monochrome** | Data with a natural order or progression (e.g., volume, intensity, time). Default for charts with 4 or fewer variables. | Shades of a single hue |
| **Categorical** | Displaying 5–9 distinct, unrelated categories. Group additional categories into "Other". See [Categorical Color Palette](/docs/web/data-visualization/categorical-color-palette) for details. | Distinct hues for maximum differentiation |
| **Semantic** | Status-based data showing positive, negative, and neutral values (e.g., deviation from a target). The [design guidelines](/docs/web/data-visualization) recommend this palette primarily for bar charts. | Success, Neutral, Warning, Danger |
Each theme configures the following for bar and donut charts:
* **Color palettes** derived from Anvil2 design tokens
* **Fill patterns**
* **Stroke colors** for contrast
* **Tooltips**
* **Rounded corners**
* **Hover dimming**
* **Pointer cursor** on interactive elements
* **Nunito Sans** font family
## Supported chart types
The themes include specific styling rules for these amCharts 5 chart types:
* [Bar charts](/docs/kits/charts/bar-charts) (vertical, horizontal, grouped, stacked)
* [Donut charts](/docs/kits/charts/donut-charts) (pie/donut with legend)
Additional amCharts 5 chart types are planned for future releases.
## Dependencies
* **@amcharts/amcharts5** ^5 - charting library (peer dependency)
* **@servicetitan/hammer-token** - design tokens for color palettes
* **React** ^18 || ^19
# Field state
Source: https://anvil.servicetitan.com/docs/kits/common/form-fields/field-state
A2InputFieldState extends formstate's FieldState with warnings, disablers, and update-mode support.
`A2InputFieldState
` extends [`formstate`](https://formstate.github.io/)'s `FieldState` and adds non-blocking warnings, dynamic disablers, soft validation, and value-confirmation helpers. It is the field state every `Form.*` wrapper binds to.
## Creating a field state
Pass an initial value to the constructor, then attach validators:
```tsx theme={null}
import { A2InputFieldState } from "@servicetitan/anvil2-ext-common";
const email = new A2InputFieldState("");
email.validators((value) => !value && "Email is required");
```
## Inherited from formstate
`A2InputFieldState` keeps the full `FieldState` surface:
| Member | Description |
| --------------- | --------------------------------------------------------------- |
| `$` | The validated value. |
| `dirty` | `true` once the value changes from its initial value. |
| `error` | The current error message, or `undefined`. |
| `hasError` | `true` when the last validation found an error. |
| `onChange` | Sets a new value (no-op while the field is `disabled`). |
| `reset(value?)` | Restores the initial value (or `value`) and clears the warning. |
| `validate()` | Runs validators and resolves with the result. |
| `validators()` | Registers the field's validators. |
| `value` | The current value bound to the input. |
## Added behavior
`A2InputFieldState` adds the following on top of `FieldState`:
| Member | Description |
| ------------------------------ | --------------------------------------------------------------------------------------------------- |
| `addValidator(key, validator)` | Adds a validator once per `key`, guarding against duplicate registration. |
| `disabled` | `true` when a disabler matched the current value. |
| `disablers(...validators)` | Registers disabler validators. A truthy result disables the field. |
| `disableUpdateMode()` | Removes the `updateMode` sub-field. |
| `enableUpdateMode(mode?)` | Creates the `updateMode` sub-field (defaults to `UpdateMode.Keep`). |
| `hardConfirmValue()` | Marks the current value as the initial value and clears `dirty`. |
| `hardSetValue(value)` | Sets the value, bypassing the `disabled` guard, and confirms it as the new initial value. |
| `hasValueChanged()` | Returns `true` when the value differs from its initial value. |
| `initValue` | The field's initial value, for comparing against the current value. |
| `onChangeHandler` | A `@servicetitan/form`-compatible handler: `(event, { value })`. |
| `onChangeNativeHandler` | A `@servicetitan/form`-compatible handler that reads `event.currentTarget.value`. |
| `onReset(handler)` | Registers a handler that runs after `reset()`. Returns the field state for chaining. |
| `seedValue(value)` | Silently sets a clean baseline value (no `onChange`/`onDidChange`); for hydrating from loaded data. |
| `softValidate()` | Runs disablers and warnings without committing a full validation. |
| `updateMode` | An optional `FieldState` sub-field for value-update controls. |
| `warning` | The current non-blocking warning message, or `undefined`. |
| `warnings(...validators)` | Registers warning validators. Each returns a message string or a falsy value. |
Warnings and disablers are evaluated during `validate()` and `softValidate()`:
```tsx theme={null}
const quantity = new A2InputFieldState(null);
quantity.validators((value) => value == null && "Quantity is required");
quantity.warnings((value) => (value ?? 0) > 100 && "That is a large quantity");
quantity.disablers((value) => value === 0 && "Zero is not allowed");
await quantity.validate();
// quantity.hasError, quantity.warning, and quantity.disabled now reflect the value.
```
## Selection field states
`SelectableOptionsFieldState` (single) and `SelectableOptionsArrayFieldState` (multiple) extend `A2InputFieldState` for option-based selection, such as `Form.RadioGroup` and `Form.CheckboxGroup`. Both hold an `options` list and validate the selected value against option-derived rules.
```tsx theme={null}
import { SelectableOptionsFieldState } from "@servicetitan/anvil2-ext-common";
type Option = { id: number; label: string; active: boolean };
const role = new SelectableOptionsFieldState(undefined as never, {
validationRules: [
{ shouldTrack: (option) => !option.active, errorCode: "Role is inactive" },
],
});
role.setOptions([
{ id: 1, label: "Admin", active: true },
{ id: 2, label: "Legacy", active: false },
]);
```
Call `setOptions` whenever the available options change. When the selected value matches a tracked option, `validate()` reports the rule's `errorCode` as the error.
# Form fields
Source: https://anvil.servicetitan.com/docs/kits/common/form-fields/index
Bind Anvil2 input fields to formstate with the Form.* wrapper components.
The `Form.*` components bind Anvil2 input fields to a [`formstate`](https://formstate.github.io/) field state, removing the boilerplate of wiring value, change, error, and warning props by hand. Each wrapper is a MobX `observer`, so it re-renders when its bound `A2InputFieldState` changes.
## Installation
Form fields rely on `formstate`, `mobx`, and `mobx-react` as peer dependencies. Install them alongside the package:
```shell theme={null}
npm install @servicetitan/anvil2-ext-common formstate mobx mobx-react
```
## Usage
Create an `A2InputFieldState`, then pass it to the matching `Form.*` component. The wrapper reads `value`, reports validation `error`/`warning`, and writes changes back to the field state.
```tsx theme={null}
import { Form, A2InputFieldState } from "@servicetitan/anvil2-ext-common";
const name = new A2InputFieldState("");
name.validators((value) => !value && "Name is required");
function NameField() {
return ;
}
```
The field state is the source of truth. Read `name.value` to get the current value, call `name.validate()` to run validation, and check `name.hasError` / `name.error` to inspect the result. See [Field state](/docs/kits/common/form-fields/field-state) for the full API.
## Available wrappers
Each wrapper accepts a `fieldState` prop plus the underlying Anvil2 component's props. The field state's value type matches the field:
| Component | `fieldState` value type |
| ----------------------------- | -------------------------------- |
| `Form.DateFieldRange` | `{ startDate, endDate } \| null` |
| `Form.DateFieldSingle` | `string \| null` |
| `Form.DateFieldYearless` | `YearlessDate \| null` |
| `Form.DateFieldYearlessRange` | `{ startDate, endDate }` |
| `Form.NumberField` | `number \| null` |
| `Form.Textarea` | `string` |
| `Form.TextField` | `string` |
| `Form.TimeField` | `string \| null` |
## Selection fields
`Form.Select`, `Form.MultiSelect`, and `Form.TreeSelect` wrap the beta Anvil2 select components and ship from the package root alongside the rest of the namespace. Provide a `loadOptions` function and bind the selected option(s) to the field state:
```tsx theme={null}
import { Form, A2InputFieldState } from "@servicetitan/anvil2-ext-common";
import type { SelectFieldOption } from "@servicetitan/anvil2/beta";
const fruit = new A2InputFieldState(null);
const loadOptions = async (search: string) =>
[
{ id: 1, label: "Apple" },
{ id: 2, label: "Banana" },
].filter((option) => option.label.toLowerCase().includes(search.toLowerCase()));
function FruitSelect() {
return ;
}
```
`Form.Select` and `Form.MultiSelect` support every loader mode (eager, page-lazy, offset-lazy, and group-lazy) — pass the `loadOptions` function that matches your strategy.
## Group fields
Checkbox and radio inputs are provided as the group wrappers `Form.RadioGroup` and `Form.CheckboxGroup`. These render an Anvil2 group with a `FieldMessage`, so validation errors and warnings surface as text. Pass an `items` array; the field state holds the selected `id` (`Form.RadioGroup`) or `id`s (`Form.CheckboxGroup`).
```tsx theme={null}
import {
Form,
SelectableOptionsArrayFieldState,
} from "@servicetitan/anvil2-ext-common";
const items = [
{ id: 1, label: "Email" },
{ id: 2, label: "SMS" },
];
const channels = new SelectableOptionsArrayFieldState(
[],
);
channels.validators((value) => value.length === 0 && "Select at least one channel");
function ChannelGroup() {
return (
);
}
```
## Limitations
* Single checkbox and radio inputs have no standalone wrapper. Use `Form.CheckboxGroup` or `Form.RadioGroup` — a bare Anvil2 `Checkbox`/`Radio` exposes a boolean error (styling only) with no message area, so the group is the form pattern that renders validation messages.
* The following Anvil2 input types have no `Form.*` wrapper: `SearchField`, `InputMask`, `Switch`, `ButtonToggle`, `SelectCard`, `Combobox`, `SegmentedControl`, and `RichTextEditor`.
# Hooks
Source: https://anvil.servicetitan.com/docs/kits/common/hooks/index
React hooks that extend Anvil2 for common ServiceTitan app interactions.
Hooks in the Extended Common library package up interaction logic that is awkward to get right by hand — debouncing, rate-limiting, and event coordination — behind a small, declarative API. Each hook ships from the package root.
## Installation
```shell theme={null}
npm install @servicetitan/anvil2-ext-common
```
Individual hooks may add their own peer dependencies — see each hook's documentation.
## Available hooks
* **[useInputGuards](/docs/kits/common/hooks/use-input-guards)** — debounces a text input and rate-limits an expensive `onCommit` callback across typing, paste, Enter, and clear.
# useInputGuards
Source: https://anvil.servicetitan.com/docs/kits/common/hooks/use-input-guards
Debounce a text input and rate-limit an expensive onCommit callback across typing, paste, Enter, and clear.
`useInputGuards` sits between a text input's raw events and an expensive `onCommit` callback — a network request, store update, or analytics event you don't want firing on every keystroke. The visible value stays fully responsive while the hook decides *when* a value is worth committing. The "guards" in the name are the protections it layers over that callback:
* **Debounce** — a burst of typing coalesces into a single commit with the final value.
* **Commit guard** — commits are rate-limited to at most one per `delay` ms, across *every* input method.
* **Held-key suppression** — repeated `keydown` from a held hotkey is dropped, so holding Enter or a paste shortcut doesn't fire repeatedly.
* **Deduplication** — the same value never commits twice in a row.
* **External sync** — when the `value` prop changes from outside, the input re-syncs and any stale in-flight commit is cancelled.
By default all four user actions (typing, Enter, paste, clear) can commit; the [`triggers`](#choosing-which-actions-commit) option lets you opt out of any of them.
## Installation
```shell theme={null}
npm install @servicetitan/anvil2-ext-common
```
## Usage
Call the hook with the controlled `value`, a `delay`, and your `onCommit` handler, then spread the returned `inputProps` onto the input. The hook owns the visible value and the commit timing; your component just reacts to commits.
```tsx theme={null}
import { useInputGuards } from "@servicetitan/anvil2-ext-common";
function SearchBox({ query, onSearch }: { query: string; onSearch: (value: string) => void }) {
const { inputProps } = useInputGuards({
value: query,
delay: 300,
onCommit: onSearch,
});
return ;
}
```
`inputProps` carries `value`, `onChange`, `onPaste`, and `onKeyDown`. Spread all of them — the paste and keydown handlers are what make the guards work; dropping them falls back to plain debounced typing.
## Configuration
| Prop | Type | Description |
| ---------- | ------------------------- | ---------------------------------------------------------------------------------------------------- |
| `value` | `string` | The controlled value. Changing it from outside re-syncs the visible input and cancels stale commits. |
| `delay` | `number` | Debounce and commit-guard window, in milliseconds. |
| `onCommit` | `(value: string) => void` | Called with a value worth committing — debounced, rate-limited, and deduplicated. |
| `triggers` | `CommitTrigger[]` | Which actions may commit. Omit (or pass empty) to enable all. |
## Choosing which actions commit
Pass `triggers` to restrict which user actions fire a commit. When omitted, all are active.
```ts theme={null}
enum CommitTrigger {
Typing = "typing", // debounced auto-commit after inactivity
Enter = "enter", // immediate commit on Enter
Paste = "paste", // immediate commit on paste
Clear = "clear", // immediate commit when cleared to ""
}
```
For example, to commit only on Enter, paste, and clear — never on a typing pause:
```tsx theme={null}
const { inputProps } = useInputGuards({
value,
delay: 300,
onCommit: handleCommit,
triggers: [CommitTrigger.Enter, CommitTrigger.Paste, CommitTrigger.Clear],
});
```
## Behavior reference
| Action / scenario | What happens |
| ----------------------- | ------------------------------------------------------------------------------------------ |
| Typing | Visible value updates instantly; `onCommit` fires after `delay` ms of inactivity. |
| Paste | Inserted at the cursor (or over the selection); commits immediately through the guard. |
| Enter | Commits immediately if the guard is idle, otherwise defers until the guard window expires. |
| Clear to empty | `onCommit('')` fires immediately, bypassing the guard, and the guard is deactivated. |
| Rapid commits | First commits immediately; the next is deferred until `delay` ms have elapsed. |
| Held hotkeys | Repeated `keydown` is suppressed, so a held Enter or paste shortcut only fires once. |
| Held arrow keys | `ArrowLeft` / `ArrowRight` are exempt, so the cursor keeps moving as expected. |
| Repeat value | A value identical to the last committed one is never committed again. |
| External `value` change | Visible value re-syncs, stale in-flight commits are cancelled, and the guard resets. |
| Unmount | All pending timers are cancelled; no trailing commit fires. |
# Common
Source: https://anvil.servicetitan.com/docs/kits/common/index
Components and utilities for building common use cases in the ServiceTitan app monolith and MFEs.
The Extended Common library is an open contribution area for components, hooks, and utilities that extend Anvil2 for the ServiceTitan app. It is maintained by the wider ServiceTitan engineering organization with support from the Design System team.
## Installation
```shell theme={null}
npm install @servicetitan/anvil2-ext-common
```
The package requires `@servicetitan/anvil2` as a peer dependency. Individual features may add their own peer dependencies — see each feature's documentation.
## What's available
* **[Form fields](/docs/kits/common/form-fields)** — `Form.*` wrappers that bind Anvil2 input fields to `formstate`, plus the `A2InputFieldState` field state.
* **[Hooks](/docs/kits/common/hooks)** — React hooks for common interactions, like `useInputGuards` for debounced, rate-limited input commits.
# Kits
Source: https://anvil.servicetitan.com/docs/kits/index
Product teams can use these kits to build a wide range of experiences that extend the core Anvil2 library.
## Browse kits
Design guidance and building blocks for building agentic AI experiences with the Anvil2 AI Kit.
Components and utilities for building common use cases in the ServiceTitan app monolith and MFEs.
Coming soon
Themes and utilities for building data visualization experiences with amCharts 5.
***
## Connect with us
Ran into a bug in the code or design? We use Jira to track outstanding
issues with Anvil.
Have an idea for a component, pattern, or documentation? Submit your idea to
Jira.
You can reach the Anvil team in the #ask-designsystem Slack channel.
View what is coming up this year for the design system.
Interested in contributing to Anvil? Read our contribution guide to learn
more!
# Library Architecture
Source: https://anvil.servicetitan.com/docs/library-architecture
Anvil2 includes a set of core and extended libraries that are designed to be used together to create a consistent and cohesive user experience across different products and platforms.
Anvil2 is broken down into a few packages that are built for different use cases, product areas, and platforms. All of the source code for Anvil2 is available in the [Hammer](https://github.com/servicetitan/hammer) monorepo.
## Core Web Library
React components and utilities for building ServiceTitan web experiences.
Figma components for building ServiceTitan web experiences, which match the React components in `@servicetitan/anvil2`.
The core web library is the main library for building ServiceTitan web experiences. It includes all of the commonly-used components and utilities for building web experiences using React.
Components in the core web library have been heavily tested and validated by the Anvil team to solve common use cases for ServiceTitan web experiences. Novel, experimental, or niche use cases may be included in the extended libraries, or built directly in the product area's codebase.
See the [Web section](/docs/web/index) of the documentation for more information about the core web library and its various assets.
## Extended Libraries
Extended libraries are libraries that extend the core web library with additional components and utilities for specific use cases. These libraries are owned by both product teams and the Anvil team. Each library has a dedicated purpose, usually tied to a specific product area, and it's own set of documentation.
}
href="/docs/kits/atlas"
>
Components and utilities for building the Atlas chat experience.
Components and utilities for building common use cases in the ServiceTitan app monolith and MFEs.
Coming soon
Themes and utilities for building data visualization experiences with amCharts 5.
### Extended libraries FAQ
#### What belongs in an extended library?
* Components built as composites of Anvil2 core components to cover specific use cases.
* Components and utilities that are specific to a product area, either due to tailored use cases or integrations.
* Components and utilities that could be migrated to the core web library, but still need further validation or testing, or only have one known use case.
* Potential contributions to the core web library. Starting with the extended library lowers the barrier to entry for contributing.
#### Can components be migrated from an extended library to the core web library?
In some cases, a component or utility may be migrated from an extended library to the core web library. This is typically done if multiple use cases arise, or if the component or utility is used in multiple product areas. The Anvil team will monitor usage and look for opportunities to migrate components to the core web library when it makes sense.
#### Why should you contribute to an extended library, rather than building it in your product area's codebase?
Oftentimes, it may seem easier or faster to build directly in your product area's codebase. Ultimately it is up to you and your team to decide, but here are the advantages of contributing to an extended library:
* The extended libraries directly consume the Anvil2 core web library, and always stay up-to-date.
* The Anvil team manages all of the tooling and infrastructure for the extended libraries, so you can focus on building your product.
* The Anvil team has better visibility into what is being built in the extended libraries, and can help with feedback, guidance, and connecting teams with similar use cases.
* The extended libraries have Storybook and Chromatic built-in for isolated development and automated visual regression testing.
* The more active our community is, the more likely we are to build components and utilities that are useful to other teams.
#### Where are the Figma designs for the extended libraries?
The Figma libraries for each extended library generally have shared ownership between the Anvil team and the product team. Head to the docs for each extended library to find out more about the Figma assets for that library.
# Anvil2 1.0 to 2.0
Source: https://anvil.servicetitan.com/docs/resources/migration-guides/1.0-to-2.0
## Migrating from Anvil2 1.x to 2.0
Anvil2 2.0 is a **major release** that removes all previously deprecated APIs, standardizes component composition, and updates several required props. Because deprecated APIs have been **fully removed**, upgrading from v1.x will surface **build-time errors** that must be resolved before your application can compile.
This guide walks through the required changes and common migration patterns.
* Deprecated components and props have been **removed** and will now throw build errors
* Several internal sub-exports that were accidentally surfaced are **no longer exported**; use the existing compound/component namespace API instead
* Several previously optional props are now **required**
* Overlay components no longer have a **fallback** for browsers without HTML Popover support.
* Some Icon filenames have been renamed for consistency
## Component Behavior Changes
### Chip
When using `size="small"`, `Chip` now allows **only one** of the following props:
* `onChange`
* `onClose`
```tsx theme={null}
// ❌ Invalid
// ✅ Valid
```
***
## Overlay Components
The following components using the **HTML Popover API** no longer have a fallback:
* `Tooltip`
* `Popover`
* `Menu`
* `Combobox`
No API changes are required, but this change requires that consumers of Anvil2 use a browser that is [compatible with the HTML Popover API](https://caniuse.com/?search=popover).
***
## Removed Exports → Compound Component APIs
Several components previously exposed internal sub-exports at the package surface. Those internal exports are now removed from the public surface. Use the existing compound / namespaced APIs instead.
### Dialog
```tsx theme={null}
// ❌ Removed exports
import {
DialogContent,
DialogHeader,
DialogFooter,
DialogCancelButton,
} from "@servicetitan/anvil2";
// ✅ Use the compound API
```
***
### SideNav
```tsx theme={null}
// ❌ Removed exports
import { SideNavLink, SideNavGroup } from "@servicetitan/anvil2";
// ✅ Use the compound API
```
***
### Other Components Using the Same Pattern
| Component | Removed Export(s) | Replacement API |
| ----------- | ------------------------------------------------------------------- | -------------------------------------------------- |
| Breadcrumbs | `BreadcrumbsElement` | `Breadcrumbs` |
| Checkbox | `CheckboxGroup` | `Checkbox.Group` |
| Details | `DetailsSummary`, `DetailsContent` | `Details.Summary`, `.Content` |
| Popover | `PopoverTrigger`, `PopoverContent`, `PopoverClose`, `PopoverButton` | `Popover.Trigger`, `.Content`, `.Close`, `.Button` |
| Radio | `RadioGroup` | `Radio.Group` |
| SelectCard | `SelectCardElement`, `SelectCardGroup` | `SelectCard`, `SelectCard.Group` |
| Toolbar | `ToolbarElement`, `ToolbarButton`, `ToolbarSelect` | `Toolbar`, `Toolbar.Button`, `.Select` |
***
## Removed Components
### DateField
The deprecated `DateField` component has been **removed**.
```tsx theme={null}
// ✅ Replace with one of:
```
***
## Removed / Replaced Props
All deprecated props have been removed in 2.0 and will now cause **build errors**.
## Dialog
* Removed:
* `onOpen`
* `onOpenChange`
* Use instead:
* `onOpenAnimationStart`
* `onOpenAnimationComplete`
* `onClose`
***
### Popover
* Removed:
* `onOpenChange`
* `onOutsidePress`
* `disableFocusLock`
* `root`
* Replacements:
* `onClickOutside`
* `modal`
```tsx theme={null}
// ❌ Removed
// ✅ Updated
```
* `Popover.Trigger`
* `data-state` has been replaced with `aria-expanded`
***
### Drawer
* Removed: `onOpen`
* Use:
* `onOpenAnimationStart`
* `onOpenAnimationComplete`
***
### Calendar
```tsx theme={null}
// ❌ Removed
unavailableDates
// ✅ New API
unavailable={{ dates: [...] }}
```
***
### Flex
`flexShrink` has been replaced by `shrink`.
```tsx theme={null}
// ❌
// ✅
```
***
### Textarea
* `errorAriaLive` no longer accepts a boolean
```tsx theme={null}
// ❌
errorAriaLive={true}
// ✅
errorAriaLive="assertive" // "polite" | "off"
```
***
### Toaster
* Removed:
* `duration`
* `forceRender`
Toast duration must now be set on **individual toasts**.
***
### Field-Level `openMoreInfo`
Removed from the following components:
* `FieldLabel`
* `Combobox.Trigger`
* `SelectTrigger`
* `Textarea`
```tsx theme={null}
// ❌
openMoreInfo
// ✅
moreInfoOpen
```
***
### Link
```tsx theme={null}
// ❌
// ✅
```
***
## Newly Required Props
The following props are now **required** and must be provided to avoid build errors:
| Component | Required Prop |
| ---------------- | ------------- |
| `RadioGroup` | `legend` |
| `Toolbar.Button` | `onClick` |
***
## Icon Renames
Several icons were renamed for consistency. Update any imports or asset references accordingly:
| Old Name | New Name |
| -------------------------------- | --------------------------------- |
| `gnav_dial_pad_filled.svg` | `gnav_dial_pad_active.svg` |
| `gnav_dial_pad_inactive_outline` | `gnav_dial_pad_disabled.svg` |
| `gnav_dial_pad_outline.svg` | `gnav_dial_pad_inactive.svg` |
| `gnav_home_filled.svg` | `gnav_home_active.svg` |
| `gnav_home_outline.svg` | `gnav_home_inactive.svg` |
| `gnav_legacy_search_filled.svg` | `gnav_legacy_search_active.svg` |
| `gnav_legacy_search_outline.svg` | `gnav_legacy_search_inactive.svg` |
***
## Optional: Anvil2 v2 Migration Claude Skill
To help teams prepare for the Anvil2 v2 migration, an optional Claude skill is available that scans the codebase for **deprecated Anvil2 patterns** and applies safe, mechanical fixes where possible.
> **Thanks to Kiryl Chetyrbak** for creating and sharing this Claude skill.
Script location:\
[https://github.com/servicetitan/ai-codegen-tools/blob/master/plugins/skills/preparing-anvil2-migration/SKILL.md](https://github.com/servicetitan/ai-codegen-tools/blob/master/plugins/skills/preparing-anvil2-migration/SKILL.md)
### What the script does
The script performs a structured migration pass with the following workflow:
1. Scans the codebase for known Anvil2 v2 deprecations
2. Applies **automatic fixes** for clear, unambiguous replacements
3. Flags items that require **manual decisions or review**, including file locations
4. Produces a summary of changes made and remaining action items
### Types of changes it handles
The script covers a broad set of Anvil2 v2 migration rules, including:
* **Renamed exports and components**
* e.g. `BreadcrumbsElement → Breadcrumbs`
* **Pascal-case subcomponents migrated to dot notation**
* e.g. `DialogContent → Dialog.Content`
* **Icon naming changes**
* e.g. `*_filled → *_active`
* **Prop renames and removals**
* e.g. `quiet → ghost`, removed `Menu.open`
* **Event handler changes**
* e.g. `onOpen → onOpenAnimationStart / End`
* **Component replacements**
* e.g. `DateField → DateFieldSingle / DateFieldRange`
* **Import path updates**
* e.g. `TooltipContext` moved to `@servicetitan/anvil2/base`
* **Context-based fixes**
* Infers missing `RadioGroup` legends
* Chooses `DateFieldSingle` vs `DateFieldRange` based on usage
### Decisions that require manual input
Some migrations cannot be resolved automatically. In these cases, the script will stop and ask for guidance, for example:
* `Chip size="small"` with both `onClick` and `onClose`
* `Toolbar.Button` without an `onClick` handler
### What this script does *not* do
* It does **not** guarantee full Anvil2 v2 compatibility
* It does **not** replace manual review or testing
* It should not be treated as a one-click migration
### When to use it
We recommend running this script **early in the migration process** to:
* Reduce manual discovery of deprecated patterns
* Apply consistent, mechanical fixes automatically
* Surface migration decisions before deeper refactors begin
This script is a convenience tool, not a required or officially supported migration step. Teams remain responsible for validating behavior, accessibility, and visual correctness after migration.
# Anvil2 2.0 to 3.0
Source: https://anvil.servicetitan.com/docs/resources/migration-guides/2.0-to-3.0
## Anvil2 3.0 Release
Anvil2 3.0 is a **CSS-only** breaking release. We've made some foundational changes to our CSS structure to address critical issues we've faced in the first two versions of Anvil2, and to provide a more robust tokens and colors system. This page will describe the changes introduced in 3.0, as well as the things that product teams need to test.
The scope of this change also requires that **all monolith and MFE projects will need to upgrade to 3.0** in the `78Vega` release. Anvil2 3.0 will not be compatible with earlier versions. Due to this requirement, we've decided to hold off on any non-CSS breaking changes in this version.
***
## Anvil2 v3.0 migration guide for designers
Anvil2 v3.0 is available now for designers working on 78Vega. This guide covers what's changing with tokens and how to set up your Figma workspace. The full release is targeted for June 6, 2026.
If you're not working on 78Vega, wait for the official release.
***
## What's new in v3.0
### Tokens
Anvil2 v3.0 introduces changes across all three token tiers.
#### Tier 3 — New
Tier 3 tokens are a new addition to the system. They represent component-level semantic decisions — things like surface color, border, and interactive states. You won't use tier 3 tokens directly in your designs. Components in the Anvil2 v3.0 library are already built with them, so the work is handled automatically when you use updated components.
#### Tier 2 — Name changes
Some tier 2 tokens have minor name updates for consistency. If you reference tier 2 tokens directly in your designs, review any custom components or local styles that use them and update as needed.
##### Overlay → Transparent Background
The entire `overlay` token category was removed and replaced with `background.color.transparent.*`.
| 2.x | 3.0 |
| ------------------------------ | --------------------------------------------- |
| `overlay.color.hover.default` | `background.color.transparent.default-hover` |
| `overlay.color.active.default` | `background.color.transparent.default-active` |
| `overlay.color.hover.primary` | `background.color.transparent.primary-hover` |
| `overlay.color.active.primary` | `background.color.transparent.primary-active` |
| `overlay.color.hover.danger` | `background.color.transparent.danger-hover` |
| `overlay.color.active.danger` | `background.color.transparent.danger-active` |
##### `$root` flattening (structural rename)
Interactive state tokens were restructured from nested `$root` objects to flat hyphenated siblings. This pattern applies across all semantic token groups — `foreground`, `background`, `border`, etc. The `-hover` and `-active` suffixes are now flat siblings instead of nested children.
**2.x:**
```text theme={null}
foreground.color.primary (was $root)
foreground.color.primary.hover
foreground.color.primary.active
```
**3.0:**
```text theme={null}
foreground.color.primary
foreground.color.primary-hover
foreground.color.primary-active
```
#### Tier 1 — Expanded color ramp
The tier 1 color ramp has been expanded with new values. Update adds coverage at the edges of the scale.
### Components
Components in Anvil2 v3.0 have been rebuilt to match code props as closely as possible. This alignment reduces the gap between design and engineering, making handoff more predictable and consistent.
Anvil2 v3.0 components also now support Figma Slots, allowing you to nest components inside other components while preserving their properties and behavior.
***
## Set up your Figma libraries
The new Anvil2 libraries are not enabled by default. Follow these steps to enable them manually.
### Turn on the new libraries
1. Open your Figma file.
2. Go to **Main menu → Libraries**.
3. Search for and enable:
* **Anvil2 v3** *(replaces Anvil2)*
* **Anvil2 Design Token**
### Turn off the old library
4. In the same Libraries panel, locate **Anvil2** and toggle it off.
Disabling the old library may cause components that haven't been swapped yet to show as unresolved. Complete the library swap before sharing files with others or handing off for review.
***
## Introducing Figma Slots
Figma Slots let you define flexible areas within a component where other components can be inserted without detaching or overriding the parent's structure. Instead of hard-coding every possible content variation, you designate a region as a slot — any component dropped into that slot inherits the parent's layout constraints while keeping its own properties and variants intact.
Slots make design system components more composable and reduce the need for one-off variants. Anvil2 v3.0 components are built with slot support, so you can nest and swap content freely within the updated library.
**Learn more:**
* [Use slots to build flexible components in Figma](https://help.figma.com/hc/en-us/articles/38231200344599-Use-slots-to-build-flexible-components-in-Figma)
* [Supercharge your design system with slots](https://www.figma.com/blog/supercharge-your-design-system-with-slots/)
### Known limitation: Slots and variable properties
Figma has a current limitation with slots and component variables. When a component is placed inside a slot, its layers become detached from the original component structure. As a result, component properties — including variant-based changes — may not update as expected when switching states.
This is a known Figma issue the team is actively working to clarify. In the meantime, if a component inside a slot isn't responding to property changes, swap it out manually rather than relying on the variable to update it.
[Read the Figma forum thread for full details.](https://forum.figma.com/report-a-problem-6/figma-slots-not-adapting-variable-properties-51771?tid=51771\&fid=6)
***
## Release timeline
| Milestone | Date |
| ------------------------------- | ----------------- |
| Anvil2 v3.0 release | End of April 2026 |
| Schema freeze | June 1, 2026 |
| Feature complete / full release | June 6, 2026 |
***
## Next steps
The Anvil team will enable Anvil2 v3.0 for all designers by default with the 78Vega release on June 6. When this happens, the old Anvil2 library will be switched off automatically — but not deleted. Existing spaces that aren't upgrading yet will retain access to the old library.
Questions or issues? Reach out in [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) or file an issue in the Anvil backlog.
## Migrating from Anvil2 2.x to 3.0
**Anvil2 3.0 is scheduled for official release on April 13, 2026.** It will be included in the 78Vega branch opening on April 17, 2026. This guide is provided ahead of release for teams who want to prepare early.
Anvil2 3.0 makes significant changes to how component styles are structured. If your application has custom CSS that targets or overrides Anvil2 component styles, this guide explains what changed, why, and what you may need to update.
Use the **`a2-v2-to-v3-migration` Claude skill** (via `/plugin install for-code@design-platform`) to get inline migration help as you work. It covers token renaming, specificity changes, color ramp remapping, and more. See the [Design Platform code plugin](https://github.com/servicetitan/hammer/tree/main/plugins/claude/code).
***
## Why this changed
Before the 3.0 release, Anvil2 organized component styles inside CSS cascade layers (`@layer reset`, `@layer base`, `@layer state`). Cascade layers are a powerful tool for managing style precedence within a controlled environment — but they come with a critical constraint: **unlayered CSS always wins over layered CSS, regardless of source order or selector specificity.**
The ServiceTitan monolith contains a large amount of CSS — including Bootstrap — that is not inside any layer, and realistically cannot be moved into one due to its age and the scope of that change. As a result, Bootstrap and other unlayered styles were overriding Anvil2's layered component styles in applications using the monolith.
To work around this, Anvil2 shipped a `revert-layer` bugfix file that attempted to counteract the unlayered CSS. This file introduced its own problems — it was unreliable in MFEs where load order wasn't guaranteed — and teams continued to encounter broken styles.
The root cause was a structural mismatch: Anvil2 was in layers, the rest of the CSS environment was not. The path we had to take was to remove layers from Anvil2 entirely.
**In 3.0, all Anvil2 component styles are flat, unlayered CSS.** This puts Anvil2 on equal footing with Bootstrap and other global styles, eliminates the need for the `revert-layer` bugfix file, and makes the specificity model predictable and consistent across all environments.
***
## What changed
### CSS cascade layers removed
All `@layer reset`, `@layer base`, and `@layer state` blocks have been removed from Anvil2 component styles. Component CSS is now standard flat CSS with no layer involvement.
### Component styles are scoped to `.anvil2`
All Anvil2 component styles are automatically scoped under a `.anvil2` ancestor class at build time. This means every component style rule starts with `.anvil2` in the compiled output. This scoping gives Anvil2 styles one class's worth of extra specificity over unscoped global styles, such as Bootstrap, providing a natural boundary for overrides.
The `.anvil2` class is applied by `AnvilProvider`. Any Anvil2 component rendered inside an `AnvilProvider` will be inside this scope.
### Published CSS Utils are scoped to `.anvil2`
CSS utility classes are designed to be used **inside `AnvilProvider`**, which automatically adds the `anvil2` class to the page. Because of this, all `a2-*` rules in the published stylesheets are scoped — they only apply to elements inside an `anvil2` container.
If you are currently using CSS utility classes outside of an `AnvilProvider`, either wrap it in an `AnvilProvider`, or add the `.anvil2` class name to an ancestor.
### The `revert-layer` bugfix file is removed
Anvil2 previously shipped a CSS file that applied `revert-layer` fixes to counteract unlayered styles in the monolith overriding Anvil2's layered component styles. This file is no longer needed in 3.0 and has been removed from Anvil2.
**If you are updating an MFE to Anvil2 3.0**, check whether your app is manually importing this file and remove it. Keeping it will have no positive effect and may cause unexpected style behavior.
**For the monolith**, the file will be removed as part of a future platform update 78Vega.
### CSS custom properties and Tier 3 tokens
Anvil2 3.0 introduces a new tier of design tokens — tier 3 (T3) component tokens — that sit between the semantic (tier 2) tokens and component styles. Rather than components referencing semantic tokens directly in their SCSS, each component now has its own dedicated token file (e.g., `button.tokens.json`, `checkbox.tokens.json`) that maps component-specific roles to semantic values. Component styles then reference these T3 variables via CSS custom properties (e.g., `--a2-mod-button-*`).
This change does not affect consumers using Anvil2 components out of the box. However, if you were overriding component styles using internal CSS custom properties (e.g., `--a2-button-*`, `--a2-calendar-*`), those variables have been renamed to follow the `--a2-mod-{component}-*` convention. Refer to each component's updated SCSS for the new variable names.
A full documentation page covering the Anvil2 design token system — including token tiers, naming conventions, and component token references — is coming soon.
### Color Ramps
The 3.0 migration expands Color Ramps from 15 stops on the neutral ramp and 6 on non-neutral ramps to 20 stops on the neutral ramp and 12 on non-neutral ramps. Existing color tokens map to their previous counterparts. See [Figma](https://www.figma.com/design/jgymIi7ydk9JE2Q5RM6uLz/%F0%9F%8C%88-Spike--Expanded-Color-Ramps-in-Anvil?node-id=1-14817\&t=v7OG0ccy5riUwd2A-1) for details, including [ramp mappings](https://www.figma.com/design/jgymIi7ydk9JE2Q5RM6uLz/%F0%9F%8C%88-Spike--Expanded-Color-Ramps-in-Anvil?node-id=101-31661\&t=v7OG0ccy5riUwd2A-1) and [token mappings](https://www.figma.com/design/jgymIi7ydk9JE2Q5RM6uLz/%F0%9F%8C%88-Spike--Expanded-Color-Ramps-in-Anvil?node-id=103-2433\&t=v7OG0ccy5riUwd2A-1).
***
## Updating custom style overrides
Any custom styles that target or override Anvil2 components must include `.anvil2` in their selector to achieve the necessary specificity. Because Anvil2 component styles are compiled with `.anvil2` as an ancestor, an override without it will not have enough specificity to win — even against the default Anvil2 styles, let alone Bootstrap.
`.anvil2` can be applied in a number of ways depending on your situation:
### Unlayered CSS
**Wrap an entire stylesheet** — useful when a whole file contains Anvil2 overrides:
```scss theme={null}
.anvil2 {
.my-button {
background-color: blue;
}
.my-card {
border-color: red;
}
}
```
**Specificity to a single selector** — useful for a one-off override:
```scss theme={null}
.anvil2 .my-component {
background-color: blue;
}
```
**Using CSS Modules?** CSS Modules treat class names as locally scoped by default, so `.anvil2` in a module file refers to a hashed local class — not the global `.anvil2` scope Anvil2 needs. Wrap your selector in `:global()` to target the real `.anvil2` class:
```scss theme={null}
:global(.anvil2) {
.my-component {
...styles
}
}
```
### `@layer ____ {}`
If your team wrapped Anvil2 overrides in `@layer` to take precedence over Anvil2's `@layer base` and `@layer state` blocks, that approach is no longer needed. Since layers no longer exist in 3.0, `@layer` has no effect on Anvil2 styles.
Remove the `@layer` wrapper and replace it with `.anvil2`:
```css theme={null}
/* 2.x */
@layer application {
.my-button {
background-color: blue;
}
... other Anvil2 style overrides ...
}
/* 3.0 */
.anvil2 {
.my-button {
background-color: blue;
}
... other Anvil2 style overrides ...
}
```
#### PostCSS plugin
If your team has entire files dedicated to Anvil2 overrides, you can automate the `.anvil2` wrapping using a PostCSS plugin rather than wrapping each file manually. This is the same approach Anvil2 itself uses internally to scope its component styles. This can also be adapted for replacing `@layer ____ {}` with `.anvil2 { }`.
Add the following plugin to your PostCSS config and apply it to the files that contain your Anvil2 overrides:
```js theme={null}
// postcss.config.js
const anvil2Wrapper = () => {
return {
postcssPlugin: "postcss-anvil2-wrapper",
Once(root, { postcss, result }) {
const inputFile = result.root.source?.input?.from;
// Scope this plugin to only the files you want wrapped.
// Update this condition to match your override file paths.
if (!inputFile || !inputFile.includes("anvil2-overrides")) {
return;
}
const nodes = [];
const keyframeNodes = [];
const globalNodes = [];
root.each((node) => {
// Keep @keyframes outside the wrapper
if (node.type === "atrule" && node.name === "keyframes") {
keyframeNodes.push(node.clone());
node.remove();
}
// Keep :global selectors outside the wrapper
else if (node.type === "rule" && node.selector.includes(":global")) {
globalNodes.push(node.clone());
node.remove();
} else {
nodes.push(node.clone());
node.remove();
}
});
if (nodes.length > 0) {
const wrapper = new postcss.Rule({ selector: ".anvil2" });
nodes.forEach((node) => wrapper.append(node));
root.append(wrapper);
}
keyframeNodes.forEach((node) => root.append(node));
globalNodes.forEach((node) => root.append(node));
},
};
};
anvil2Wrapper.postcss = true;
module.exports = {
plugins: [anvil2Wrapper()],
};
```
With this in place, a file like:
```scss theme={null}
// anvil2-overrides.scss
.my-button {
background-color: blue;
}
```
will compile to:
```css theme={null}
.anvil2 .my-button {
background-color: blue;
}
```
### CSS custom properties
#### Internal property names changed
Anvil2 component styles use internal CSS custom properties as part of their implementation. These are not part of the public API. If your codebase was setting properties like `--button-background-color` or similar bare names to override Anvil2 styles, those overrides will not work in 3.0 — the internal names have changed. Refer to the [Tier 3 tokens section above](#css-custom-properties-and-tier-3-tokens) for the new naming convention.
***
## Known Issues
### Anvil (A1) dialogs containing Anvil2 components
If you have an Anvil (A1) dialog that renders Anvil2 components inside it, those components will lose their styles in 3.0. This is because dialogs portal their content out of the DOM — the rendered output is attached directly to the document body, outside the normal component tree. This means the `.anvil2` class provided by your app's `ThemeProvider` is no longer an ancestor, and Anvil2 component styles will not apply.
This is only an issue when mixing A1 and A2. A2 dialogs handle this correctly on their own.
### Preferred: migrate to the Anvil2 Dialog
The recommended fix is to replace the A1 dialog with the [Anvil2 Dialog](/docs/web/components/dialog/code). The Anvil2 Dialog renders its portal content inside the `.anvil2` scope automatically, so no additional setup is needed.
When migrating to the Anvil2 Dialog, any A1 components inside the dialog that use popovers — such as A1 Select fields — must also be migrated to their Anvil2 equivalents (e.g., `SelectField`). The Anvil2 Dialog uses the browser's [top layer](https://developer.mozilla.org/en-US/docs/Glossary/Top_layer), which A1 components are not designed to work within. A1 popover-based components will render beneath the dialog rather than on top of it, making them unusable.
#### Alternative: wrap with ThemeProvider
If migrating to the A2 Dialog is not immediately feasible, wrap the Anvil2 content inside the A1 dialog with a `ThemeProvider`. This gives the portaled content its own `.anvil2` scope:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
function MyA1Dialog() {
return (
{/* Anvil2 components will now have the .anvil2 scope */}
Save
);
}
```
***
### Known Chrome DevTools performance issue with CSS custom properties
**This is a Chrome DevTools-only issue and does not affect your application's runtime performance.**
There is a [known bug in Chrome](https://issues.chromium.org/issues/457696384) where CSS custom properties cause extremely expensive style recalculations inside DevTools. When an element's styles are resolved through a chain of cascaded custom properties, the DevTools style panel can take significantly longer to update when inspecting elements. This manifests as slow style panel updates and sluggish element inspection — it does not affect how your app behaves or performs for end users.
This is relevant to Anvil2 3.0 because the refactor significantly increased the use of CSS custom properties across all components. If you notice the DevTools style panel feeling slow when inspecting Anvil2 components in Chrome after upgrading, this bug is the likely cause.
This is a Chrome engine bug, not an Anvil2 bug, and it only affects the Chrome DevTools experience — not application performance. Firefox and Safari are not affected. Chrome's current plan is to ship a production fix in version 149, slated for June 2, 2026. We will update this page when the fix is released.
***
## Summary
| Scenario | What to do |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Custom styles not overriding Anvil2 | Add `.anvil2` to the selector or file wrapper |
| Using CSS Modules | Use `:global(.anvil2)` as the wrapper to target the real `.anvil2` scope |
| Using `@layer` for Anvil2 overrides | Remove the layer wrapper; replace with `.anvil2 { }` |
| Overrides stopped working after upgrade | Add `.anvil2` to match Anvil2's specificity scope |
| Large files of Anvil2 overrides | Use the PostCSS plugin to automate `.anvil2` wrapping |
| Relying on internal CSS custom property names | Migrate to standard CSS overrides scoped to `.anvil2` or update to the new token/property system |
| MFE importing the `revert-layer` bugfix file | Remove the import — no longer needed in 3.0; monolith removal coming in 78Vega |
| A1 dialog containing A2 components | Migrate to A2 Dialog, or wrap A2 content with `ThemeProvider` |
| Chrome style performance regression | No action needed — Chrome fix expected in v149 (June 2, 2026) |
# Anvil to Anvil2
Source: https://anvil.servicetitan.com/docs/resources/migration-guides/anvil-to-anvil2
Resources to help migrate from Anvil to Anvil2.
This guide is for ServiceTitan product squads attempting to migrate from Anvil (referenced as Anvil1 going forward to avoid confusion) to Anvil2. More specifically,
* for **engineers**, this is the migration guide for `@servicetitan/design-system` to `@servicetitan/anvil2`, and
* for **designers**, this guide is for the switch from the Anvil Web to Anvil2 Figma library.
This document will cover the high-level changes to the design system, and the breaking changes to component APIs and implementation standards. Follow the links to specific component documentation for a deeper understanding.
## Migrating to Anvil2
As of the initial `1.0.0` release, there are a few components that are not included that we plan to make available soon. Check out the component mapping guidelines below for more information.
While we work on some of the unreleased components, there is a good chance that upgrading will require using both Anvil1 and Anvil2 for a period of time. Reach out to the Anvil team if you need any assistance or have questions!
### React Requirement
To use Anvil2, the consuming project needs to use React 18 or React 19. Read more about [upgrading to React 18](https://docs.st.dev/blog/#upgrading-to-react-18) in the Front-End Platform blog.
### Getting Started Documentation
See our [Getting Started page](/docs/getting-started) for more details about installing Anvil2 and the basic requirements for using it in an application.
### Swapping Libraries
* In Figma, enable the Anvil2 library to start using the new components See the [Figma docs to learn how to enable a library](https://help.figma.com/hc/en-us/articles/1500008731201-Enable-or-disable-a-library-in-a-design-file).
* In code, install the Anvil2 package:
```bash theme={null}
npm install @servicetitan/anvil2
```
* Once the Anvil2 Web package is installed, components can be imported as follows.
```typescript theme={null}
import { Button } from "@servicetitan/anvil2";
```
## CSS Updates
### CSS Utilities
CSS Utility classes have been trimmed down considerably in Anvil2. Many of the classes were rarely (if ever) used, or can be replaced with components like [Flex](/docs/web/components/flex/code) and [Grid](/docs/web/components/flex/code). At a high level, here is what has changed:
* Any utility classes from Anvil1 for flex, grid, display, position, alignment, height, width, z-indexing, border radius, line height, or white space have been removed.
* Spacing classes (margin and padding), now use inline and block properties rather than left/right/top/bottom. This enables easier internationalization using CSS but does require changing the class names. For example, `.m-l-1` is now `.m-inline-start-1`. [Learn more in the mdn docs](https://developer.mozilla.org/en-US/docs/Web/CSS/margin-block).
* Spacing values (`-1`, `-2`, etc.) now use `rem` units for better accessibility.
* Most classes use CSS variables, which are provided by the `ThemeProvider` or `AnvilProvider`. A fallback value is provided for use outside of a provider.
* Utility classes can be imported all at once, or by utility type:
```tsx theme={null}
// all utils
import "@servicetitan/anvil2/assets/css-utils/a2-utils.css";
// by type
import "@servicetitan/anvil2/assets/css-utils/a2-border.css";
import "@servicetitan/anvil2/assets/css-utils/a2-color.css";
import "@servicetitan/anvil2/assets/css-utils/a2-font.css";
import "@servicetitan/anvil2/assets/css-utils/a2-spacing.css";
```
See the [CSS Utilities reference](/docs/web/utilities/css-utilities) for the full class list, naming conventions, and setup instructions.
There are currently two versions of the CSS Utilities:
* `utils.css` - includes all utility classes
* `a2-utils.css` - includes all utility classes prefixed with `a2-`
In the future, we will deprecate the classes without the `a2-` prefix, so we recommend using the `a2-` versions going forward.
### Component CSS
One important change in Anvil2 is how component CSS is imported and bundled (or, *not* bundled in this case). In Anvil, a large CSS import that included bundled styles from all components was required.
In Anvil2, each component imports its own CSS. Ideally, this means that your application will only bundle the CSS it needs for the Anvil2 components that are included in the app. This also means that consuming applications will only need to directly import CSS from Anvil2 if they are using the CSS Utilities.
#### CSS Modules
Anvil2 also uses CSS Modules to prevent style conflicts and accidental overrides. In general, overriding styles should be avoided to maintain consistency, but in some cases it may still be necessary. To add custom styling to an Anvil2 component, use either a `className` or `style` object.
## Component Updates
We reviewed all of the Anvil1 components and made some significant changes for Anvil2. We trimmed down the library, renamed or repurposed some components, and added a few new ones. The information and links below should help when navigating these changes.
### Component Mapping
Some components from Anvil1 have been renamed or replaced in Anvil2. The list below includes changes in component names and notes about significant changes from Anvil1 to Anvil2.
The following table includes components that have been replaced in Anvil2 and will require some refactoring in consuming projects. Anvil1 components missing from this list have either been removed (see the "Components Omitted in Anvil2" section below) or are still available in Anvil2.
| Anvil1 Component | Anvil2 Component |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Action Menu | [Menu](/docs/web/components/menu) |
| BackLink | [Drilldown](/docs/web/components/drilldown) |
| Badge | [Chip](/docs/web/components/chip) (The Anvil2 Badge has been repurposed as a notification badge) |
| Banner | [Alert](/docs/web/components/alert) |
| Body Text | [Text](/docs/web/components/text) ("body" variant, default) |
| Button Toggle | [Segmented Control](/docs/web/components/segmented-control) (Reference doc to be sure your use case matches) |
| Color Picker | Color Field \* |
| Data List | [DataTable](/docs/web/components/data-table) |
| Date Picker | [Date Field Single](/docs/web/components/date-field-single), [Date Field Yearless](/docs/web/components/date-field-yearless) |
| Date Range Picker | [Date Field Range](/docs/web/components/date-field-range) |
| Eyebrow | [Text](/docs/web/components/text) ("eyebrow" variant) |
| Headline | [Text](/docs/web/components/text) ("headline" variant) |
| Input | [Text Field](/docs/web/components/text-field) |
| Modal | [Dialog](/docs/web/components/dialog) |
| Option List | [Listbox](/docs/web/components/listbox) or [List View](/docs/web/components/list-view) |
| Progress Tracker | [Stepper](/docs/web/components/stepper) |
| Select | [Combobox](/docs/web/components/combobox) |
| Table | [DataTable](/docs/web/components/data-table) |
| Tag | [Chip](/docs/web/components/chip) |
| Takeover | [Dialog](/docs/web/components/dialog) ("fullscreen" size) |
| Time Picker | [Time Field](/docs/web/components/time-field) |
| Toggle Switch | [Switch](/docs/web/components/switch) |
| Collapsible | [Details](/docs/web/components/details) |
| Flow Card | [Edit Card](/docs/web/components/edit-card) |
| Togglebox | [Select Card](/docs/web/components/select-card) |
| Spinner | [Spinner](/docs/web/components/spinner) |
\* Not available currently, but planned for a future release. See the [Roadmap](/docs/resources/roadmap) for more information.
### Date Picker values
Anvil1 Date Picker and Date Range Picker components used JavaScript `Date` objects for `value` and `onChange`. Anvil2 date fields use [ISO 8601 date strings](/docs/web/utilities/date-values) instead.
Convert existing `Date` values before passing them to Anvil2 date fields:
```tsx theme={null}
function toDateString(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
// Anvil1
// Anvil2
{
if (change.date) {
const [year, month, day] = change.date.split("-").map(Number);
setSelectedDate(new Date(year, month - 1, day));
} else {
setSelectedDate(null);
}
}}
/>
```
See [Date Values](/docs/web/utilities/date-values) for conversion patterns with Luxon, Moment, and JavaScript `Date` objects.
View the Anvil2 roadmap and upcoming features.
Have an idea for a component, pattern, or documentation? Fill out the Feature Request Form in Slack.
### Layout in Anvil2
Adjusting layouts has changed in Anvil2. The [Flex](/docs/web/components/flex/code) and [Grid](/docs/web/components/grid/code) components can be used to build layouts using [Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) and [CSS grid](https://css-tricks.com/snippets/css/complete-guide-grid/) properties. These two components can be used in combination with each other along with component-specific layout props, which allows for more flexibility than the Stack and Layout components in the original Anvil1 library.
#### Component Layout Props
On the component-level, most Anvil2 components accept [Layout props](/docs/web/utilities/layout-props), which includes several Flex and Grid CSS shortcuts, as well as responsive breakpoint props.
**Components that do not use Layout props:**
* Dialog
* Drawer
* Label
* Menu
* Popover
* Toast
* Tooltip
Create layouts using CSS Flexbox.
Create layouts using CSS Grid.
Apply CSS Flexbox and Grid properties to components.
### Other Notable Breaking Changes
#### Code
The following components may require some refactoring to use in Anvil2.
* [Announcement](/docs/web/components/announcement)
* The `actionName` and `onActionClick` props have been removed. Instead, add action buttons as `children` of the `Announcement`.
* ~~Badge/Tag~~ → [Chip](/docs/web/components/chip)
* Separate component for `Badge` rather than variation of the `Tag`.
* Use `label` instead of `children`.
* ~~Banner~~ → [Alert](/docs/web/components/alert)
* Replace action button props with action buttons in the `children` of the `Alert`.
* ~~Pickers~~ → Fields
* ***Coming soon!*** All picker components (Date Picker, File Picker, etc.) will be replaced with *field* components.
* ~~Modal~~ → [Dialog](/docs/web/components/dialog)
* The `title` and `footer` props have been replaced with the `Dialog.Header` and `Dialog.Footer` components, which can be passed as `children` of the `Dialog`.
* Dialog should not be conditionally rendered - use the `open` prop to show or hide. Read more about this [here](/docs/web/components/dialog/code#conditional-rendering).
* Drawer
* Drawer should not be conditionally rendered - use the `open` prop to show or hide. Read more about this [here](/docs/web/components/drawer/code#conditional-rendering).
* ~~Option List~~ → [List View](/docs/web/components/list-view) or [Listbox](/docs/web/components/listbox)
* List Views include checkboxes.
* Listboxes are generally used within confined spaces, such as a dropdown or side bar.
* Both components use `children` rather than the `options` prop to render items.
* ~~Progress Tracker~~ → [Stepper](/docs/web/components/stepper)
* The `Stepper.Step` sub-components should be children of a Stepper.List.
* The `Stepper.List`, `Stepper.Panel`, `Stepper.NextButton`, `Stepper.PrevButton`, and `Stepper.FinalPanel` sub-components can be used as `children` of the `Stepper`.
* ~~Select~~ → [Combobox](/docs/web/components/combobox)
* Combobox allows users to search for one or multiple options within an overlay dropdown.
* Combobox now uses a component-based API to render the list of items. Refer to the Combobox docs for implementation details.
* ~~Table~~ → [Data Table (beta)](/docs/web/components/data-table)
* Data Table has been completely rewritten in Anvil2. Refer to the [Data Table docs](/docs/web/components/data-table/code) for more information.
* [Tab](/docs/web/components/tab)
* The Anvil2 Tab uses a few sub-components to construct the buttons and panels, and is an uncontrolled component.
* In Anvil2, the `controls` and `id` props of the `Tab.Panel` and `Tab.Button` are used to determine which panel is opened by which button.
* [Toast](/docs/web/components/toast)
* The Anvil2 Toast implementation has undergone a complete overhaul. Rather than using state to control when a toast message is displayed, a set of `toast` methods are available to display, hide, and update toast messages can be used. See the [Toast docs](/docs/web/components/toast/code) to learn how.
* The Anvil team and the Front-End Platform team established a holistic solution to avoid issues with using both Anvil1 and Anvil2.
#### Design
* The Figma library now uses [Figma variables](https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma) to manage styles. This allows for easier text styling usage in addition to native dark mode support.
* Usage of [instance swapping](https://help.figma.com/hc/en-us/articles/360039150413-Swap-components-and-instances) in several components, such as Dialog and Drawer, allowing for components to make better use of autolayouts.
* Improved support for suggested content in a component, such as options provided in the Combobox Popover and multiple Tabs in a single instance.
* More consistent application of visual states (e.g., Hover, Active, Focus).
### Components Omitted in Anvil2
The following Anvil1 components are no longer available in Anvil2:
* Backdrop
* Collapsible
* Mask
* More details
* Select
* Sidebar (integrated into Page)
* Snackbar
#### Components not currently available
The following components are not currently available in Anvil2, but are under consideration or planned to be released later. The following list is updated quarterly:
* Color Picker \*\*
* File Picker \*\*
* Form \*
* Inline Edit \* Q1 FY27
* Status Light \*
\* Under consideration for Anvil2, depending on priority and feedback.
\*\* Work is already planned as noted.
### Migrate gradually as we release more components
As we work on these components, we expect that consuming projects will need to continue to use Anvil1 components that are not yet released in Anvil2. There should be no issues using Anvil1 alongside Anvil2 until the full catalog of components are available, but please reach out to the [Anvil team on Slack](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) if you need assistance.
Any net-new designs at ServiceTitan should use Anvil2 going forward. Anvil1 components can be used sparingly when necessary. If an Anvil1 component must be used within a net-new design, plan for a swap via [UX Debt](https://servicetitan.atlassian.net/wiki/spaces/DES/pages/2461206098/UX+Debt+Documentation).
Refer to the [Anvil1 docs](https://anvil.servicetitan.com/) when using these components. We will keep this migration guide updated as more components become available in Anvil2!
# Office Hours
Source: https://anvil.servicetitan.com/docs/resources/office-hours
# Accessibility
Source: https://anvil.servicetitan.com/docs/resources/playbook/accessibility
Accessibility helps us create usable, reliable experiences for all our customers, playing a key role in product usability, quality and consistency.
## Why accessibility matters
Accessibility ensures every user can interact with our products, regardless of ability, device, or context.
### Usage Guidance
Accessibility is a core requirement of Anvil2. Teams building consumer-facing products should be aware that accessibility directly affects usability, inclusivity, and long-term product stability. Anvil2 follows [WCAG 2.2](https://www.w3.org/TR/WCAG22/) accessibility guidance to ensure our components, patterns, and foundations meet consistent standards across the platform.
Use system components whenever possible, as they include built-in accessibility support, including keyboard interaction, screen-reader semantics, focus management, and color contrast. When designing or building new UI, consider accessibility early and test interactions using a keyboard and assistive technologies.
## How accessibility applies in Anvil2
* [WCAG 2.2 AA](https://www.w3.org/TR/WCAG22/) is the minimum requirement.
* All components must support keyboard navigation.
* All text and surfaces must meet contrast requirements.
* Motion must respect reduced-motion settings.
* Components must include correct ARIA roles and semantics.
## Related
* [Accessibility Foundations](/docs/accessibility)
* [#accessibility-champions](https://servicetitan.enterprise.slack.com/archives/C0A1QKT20LU)
# Content Guidance
Source: https://anvil.servicetitan.com/docs/resources/playbook/content-guidance
Content guidance ensures product language is clear, consistent, and helpful.
## Usage Guidance
Anvil2 partners closely with the [Product Cop team](https://servicetitan.atlassian.net/wiki/spaces/DTW) to create guidance for UI language across components and patterns. Follow shared guidelines for naming, labels, helper text, and error messages. Use plain language, keep content concise, and align with ServiceTitan voice and tone.
### Extension & Exceptions
If a product requires language outside standard guidance, consult both the Product Copy team and the Anvil team before diverging. If you have a specific request for Copy support, follow guidance [here](https://servicetitan.atlassian.net/wiki/spaces/DTW/pages/2833023629/Submit+a+request+for+in-app+copy+review+Mar-2024#In-app-Product-Copy%3A-Review-requests).
### Contribution & Maintenance
Content guidance is co-owned by Anvil2 and the Product Copy team. Updates are based on research, feedback, and evolving product language.
## Related
* [Content Intro](/docs/web/content)
* [Content Guidance](/docs/web/foundations/content)
* [Request assistance](https://servicetitan.atlassian.net/wiki/spaces/DTW/pages/2833023629/In-app+Copy+Transition+Plan+Mar-2024)
# Foundations
Source: https://anvil.servicetitan.com/docs/resources/playbook/foundations
Foundations define the core visual and interaction rules that ensure Anvil2-based interfaces remain cohesive across products.
## Anvil2 Foundations
Foundations include color, typography, spacing, duration, and more. Learn more about each by visiting the [Foundations](/docs/web/foundations) section.
### How to use Foundations
* Use tokens through Figma styles and code imports.
* Do not hardcode hex values, spacing, or typography.
* Follow layout rules and spacing scale.
* Maintain accessible contrast, focus, and behavior.
* Keep designs linked to the library, avoiding overrides.
### Extend only when:
* A repeated need exists across multiple teams.
* The gap cannot be addressed through current system tools.
* The extension follows naming and accessibility rules.
### Related
* [Foundations](/docs/web/foundations)
# Governance
Source: https://anvil.servicetitan.com/docs/resources/playbook/governance
Governance defines how Anvil2 is managed, how decisions are made, and how teams work together to maintain the consistency, quality, and long-term health of the system.
## Usage Guidance
Anvil2’s governance model guides how updates are reviewed, how contributions are evaluated, and how the system evolves. Use this section to understand how decisions are made, what to expect when submitting changes, and how the Anvil and Product teams collaborate.
### How governance works
Governance ensures that changes to Anvil2 support product-wide consistency, accessibility, and maintainability. This includes:
* Reviewing new component or pattern proposals
* Evaluating updates to foundations
* Assessing requests against system principles
* Aligning changes across design, engineering, and product teams
* Maintaining documentation, tokens, and code in sync
* Overseeing versioning, deprecations, and migration guidance
## Design System Scope
* Shared foundations (tokens, color, type, spacing, motion, accessibility)
* Core UI components and supported variants
* Reusable interaction patterns and workflows
* Content guidance (with Product Copy)
* Design & engineering tooling for the system
* Documentation, release notes, and support communications
If you aren't sure if a request is within Anvil's scope, reach out! If it benefits multiple teams or recurs, we’ll consider it for the system; if not, we’ll advise on next steps.
## Roles within governance
### Design System Team (Core Owners)
* Maintains foundations, components, tokens, patterns, and documentation
* Reviews and guides contributions
* Ensures accessibility standards are met within Anvil2
* Leads cross-team alignment on decisions
* Manages versioning, releases, and deprecations
### Product Designers & Engineers (Contributors)
* [Propose and contribute](https://calendly.com/anvil-servicetitan/office-hours) new components, patterns, or enhancements
* Identify system gaps for real product work
* Partner with the system team during testing and ideation
* Follow established guidance and standards
* Guide evolution through active conversation
### Cross-Functional Partners (Research, PMs)
* Provide input during reviews
* Help validate proposed changes against user needs
* Ensure solutions support customer-facing and internal workflows
* Guide evolution through surfacing user needs
## Contribution
We welcome and encourage contributions to Anvil2. The system grows stronger when teams share their needs, ideas, and improvements. Any team can propose improvements to Anvil2, and we’re here to support you through the process.
Anyone can [submit a feature request](https://slack.com/shortcuts/Ft07EXNJSPS7/f598de9eda7177f477aaddbbc5701e15), reviewed weekly, as new needs and features arise.
We also have detailed guidance for Design and Engineering contributors:
* [Code contribution](https://github.com/servicetitan/hammer/blob/main/README.md#contributing)
* [Design contribution](https://servicetitan.atlassian.net/wiki/spaces/ADS/pages/2054129026)
## Related
* [Anvil Blog: Engineering Support and Contribution](/blog/posts/2025-04-15-dev-feedback)
# How We Work
Source: https://anvil.servicetitan.com/docs/resources/playbook/how-we-work
This section explains how the Anvil2 team operates, including how we support product teams, manage requests, and organize our work across needs at ServiceTitan.
## Usage Guidance
Use this section to understand what support is available, how to engage with us, and how we prioritize our time and resources to keep Anvil2 healthy and effective.
### Weekly On-call Support
We run a weekly on-call rotation to provide fast, reliable support to product teams.
Each week, 2 designated Anvil team members are responsible for:
* Responding promptly to questions in Slack ([#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS)
* Hosting office hours
* Assisting with implementation questions
* Capturing new requests or system gaps
* The on-call partner escalates complex issues to the relevant team member as needed to unblock consumers.
### Weekly Office Hours
We host several scheduled [office-hour slots](https://calendly.com/anvil-servicetitan/office-hours) each week where consumers can meet with the Anvil2 team. Teams often use these sessions to:
* Walk through designs, implementations, or bugs
* Walk through feature requests
* Get feedback on patterns or components
* Check accessibility behavior
* Review early concepts
* Resolve visual or interaction issues
* Support code implementations
Office hours are open to anyone working on product UI and are designed to enable quick, collaborative problem solving.
Book Office Hours [here](https://calendly.com/anvil-servicetitan/office-hours)
### Quarterly design resource rotation
We rotate design resources on a quarterly basis to focus on priority workflows and provide deep, consistent support to the areas that impacts ServiceTitan’s core priorities.
Each quarter, specific Anvil2 Design Technologists are assigned to priority workflow groups, enabling:
* Deeper partnership with product teams
* Hands-on involvement in real product design
* Stronger pattern identification and reuse
* More informed system improvements
* Exposure to different workflow needs over time
Some examples of personas and teams we pair with:
* Dispatch
* CSR
* Technician
* End-to-end teams: TI and Global Search, Navigation
* Memberships and Service Agreements
* SalesPro
* Toolbelt
This rotation keeps the system grounded in real-world usage and spreads system knowledge across the design organization. It also ensures that the team is prioritizing the highest impact areas of the business.
### How we prioritize work
We prioritize work based on:
* Impact to the user
* Alignment with manifesto
* Impact across multiple product teams
* Accessibility or usability concerns
* Duplicated effort or system friction
* Long-term maintainability
* Contribution volume and urgency
* Repeated support requests
This prioritization ensures the design system evolves deliberately and supports key product outcomes.
### How we collaborate with teams
We support consuming teams by:
* Joining team reviews, supporting implementation
* Teaming up on bugs
* Providing async guidance in Slack
* Running office hours
* Participating in discovery or planning discussions
* Reviewing component and pattern usage
We aim to be a partner by working collaboratively and offering clear, practical guidance.
### How we support contributions
Any team can propose new features, enhancements, or improvements to the design system.
Our contribution workflow includes:
* Understanding the problem
* Checking alignment with system principles
* Reviewing system-wide impact
* Exploring options collaboratively
* Validating accessibility and engineering feasibility
* Collaborating with user research, as needed
* Documenting the final approach
* Releasing it through the system
We also have detailed guidance for Design and Engineering contributors:
* [Code contribution](https://github.com/servicetitan/hammer/blob/main/README.md#contributing)
* [Design contribution](https://servicetitan.atlassian.net/wiki/spaces/ADS/pages/2054129026)
Learn more [here](https://servicetitan.atlassian.net/wiki/spaces/ADS/pages/4214098906/Governance#Contribution)
### How we improve the system
We continuously strengthen Anvil2 through:
* Regular audits
* Accessibility reviews
* Recurring feedback from office hours
* Contribution requests
* Release retrospectives
* Usage metrics across design and code
* Quarterly workflow immersion
These activities ensure Anvil2 evolves alongside product needs and customer expectations.
# Overview
Source: https://anvil.servicetitan.com/docs/resources/playbook/overview
Anvil2 is ServiceTitan’s design system. It provides the building blocks, rules, and guidance needed to design and develop consistent, accessible, and reliable product experiences.
## What Anvil2 includes
* Foundations
* Components
* Patterns
* Content guidance
* Accessibility guidance
* Design tooling
* Coded libraries
* Governance and contribution processes
* Versioning and release notes
## How to use this Playbook
This playbook explains how to use, contribute to, and maintain Anvil2 inside ServiceTitan.
Use it to understand:
* How decisions are made
* How teams should apply system rules
* How contributions and requests are handled
* How to work with assets and guidance
* What governance looks like
* How we work across teams
* What responsibilities teams have when using or extending Anvil2
# Principles
Source: https://anvil.servicetitan.com/docs/resources/playbook/principles
Our principles guide how we design, build, and maintain Anvil2 to support how we create and deliver experiences at ServiceTitan.
## Understanding our principles
Each principle shapes the way we approach design, documentation, and development. They help us stay grounded in real product needs while ensuring the system remains scalable and maintainable. These principles should be referenced when deciding whether to add new components, update existing ones, or introduce new patterns and features.
* **Consistency that scales:** Build solutions that work across roles and product areas while keeping experiences unified.
* **Efficient and sustainable:** Prioritize durability, simplicity, and iteration over one-off solutions.
* **Built for everyone:** Make accessibility and inclusivity core requirements rather than afterthoughts.
* **Quality you can trust:** Validate, test, document, and support every addition to the system.
* **Provide and curate:** Offer reliable solutions and guide contributions from product teams.
* **Anchor the core, enable the rest:** Maintain a strong system foundation while allowing teams to extend responsibly.
## Related
* [Core Principles](/docs/core-principles)
# Versioning and Releases
Source: https://anvil.servicetitan.com/docs/resources/playbook/versioning-releases
Versioning and releases ensure teams stay aligned with updates to Anvil2 and can adopt changes consistently across products.
## How Versioning Works
Anvil2 uses semantic versioning, following the standard three-part version string:
`MAJOR.MINOR.PATCH`
### Major releases
Indicate breaking changes that may require design or engineering updates. Breaking changes to stable components and features are batched into major releases.
#### Examples
* Large UI changes affecting multiple components
* Significant updates to foundational tokens
* Removing previously deprecated components or features
* Major changes to API or behavior
Major releases are planned on a **quarterly cadence**. Announcements are shared approximately three months ahead of the target release window, giving teams time to plan for migration and testing. For major breaking changes, the Anvil team will provide migration guidance at time of release.
***
### Minor releases
Add new features while maintaining backwards compatibility. Minor releases ship **as needed** when feature work is ready.
#### Examples
* New components or variants
* Updated interactions
* Enhancements to patterns
* Additions to documentation
* Minor accessibility improvements
Teams can adopt minor releases immediately, as they do not break existing implementations.
***
### Patch releases
Contain small, safe, incremental updates. Patch releases ship **as needed**.
#### Examples
* Bug fixes
* Visual polish
* Icon additions
* Documentation corrections
* Minor accessibility fixes
* Internal code improvements
Teams should adopt patch releases as soon as possible to ensure quality and consistency.
Patch releases may also include breaking changes to **beta** features. See [Beta features](#beta-features) below.
***
## Communications
### Per-release announcements
Every Anvil2 release triggers an announcement in [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) so teams can track what changed and when.
### Weekly changelog summary
A weekly batch summary of all Anvil2 package releases from the prior week is posted to [#dev](https://servicetitan.enterprise.slack.com/archives/CAXMG2939). This makes it easy for engineering teams to catch up on recent changes in one place.
### Major release announcements
Major release announcements are shared approximately **three months ahead** of the target release window. Initial communication goes to the Platform team for coordination, then is shared broadly in [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) and [#dev](https://servicetitan.enterprise.slack.com/archives/CAXMG2939).
***
## Beta features
Some components and features are released as **beta** before being stabilized. Beta releases allow teams to try new features in real products and provide feedback while we continue to iterate.
### What to expect from beta
* Beta features may change, including breaking API changes, **within patch releases**.
* Beta features are clearly marked in the changelog (prefixed with **BETA**) and in documentation.
* Stable APIs are never broken in patch releases — only APIs explicitly marked as beta may change incompatibly.
### When beta features graduate
Once a beta feature has enough production usage and feedback, it will be stabilized and follow standard semver guarantees from that point forward.
A deep dive into how beta features work, what the beta label means, and what to expect when adopting them.
***
## Adopting Updates
To stay aligned with the system, teams should:
* Review [release notes](https://github.com/servicetitan/hammer/blob/main/packages/anvil2/CHANGELOG.md) when a new version is released
* Integrate minor and patch updates promptly
* Plan for major release migrations ahead of the release window
* Remove deprecated usage as part of ongoing maintenance
* Test UI behavior after upgrading the code package
* Report issues or ask questions in [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS)
## Related
Full history of Anvil2 package releases and changes.
See what the Anvil team is working on and what's coming next.
# Visit Anvil1
Source: https://anvil.servicetitan.com/docs/visit-anvil1
# AI Mark – Code
Source: https://anvil.servicetitan.com/docs/web/components/ai-mark/code
AiMark displays the ServiceTitan AI mark with optional animated hover states and support for tooltip or popover overlays.
```tsx lines expandable theme={null}
import { AiMark, Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
Created by Atlas
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { AiMark } from "@servicetitan/anvil2";
function ExampleComponent() {
return (
);
}
```
`AiMark` displays the ServiceTitan AI mark. When paired with a tooltip or popover via `popoverOrTooltipConfig`, the icon becomes interactive and animates on hover and focus. Without a configuration, it renders as a static inline icon.
The component automatically respects the user's `prefers-reduced-motion` system preference — animations are disabled when reduced motion is preferred.
### With Tooltip
Use the tooltip variant to show a brief text label when users hover or focus the icon.
```tsx lines expandable theme={null}
import { AiMark, Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
Call & job notes summary
);
}
export default App;
```
### With Popover
Use the popover variant to show richer content — such as a title and description — when users click the icon.
```tsx lines expandable theme={null}
import { AiMark, Button, Flex, Text, Textarea } from "@servicetitan/anvil2";
function App() {
return (
Recommended estimate lines
{}}>
Submit
),
}}
/>
);
}
export default App;
```
### Force Animated
Use `forceAnimate` to control the pulsing animation from a parent component. This is intended for cases where `AiMark` is embedded inside an interactive element and the animation should respond to the parent's hover or focus state rather than its own.
```tsx theme={null}
import { useState } from "react";
import { AiMark, Flex, Text } from "@servicetitan/anvil2";
function ParentComponent() {
const [isHovered, setIsHovered] = useState(false);
return (
setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
AI-powered suggestions
);
}
```
### Plain Icon
Without `popoverOrTooltipConfig`, `AiMark` renders as a non-interactive icon. Use this when you only need the visual indicator without any overlay.
```tsx lines expandable theme={null}
import { AiMark, Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
Created by Atlas
);
}
export default App;
```
## React Accessibility
* When `popoverOrTooltipConfig` is provided, the icon renders as a ghost `Button` with an `aria-label` set from `triggerLabel`. This ensures screen readers announce a meaningful label rather than just "button".
* Pass a descriptive `triggerLabel` that communicates the purpose of the overlay — for example, `"Learn about AI-powered suggestions"` rather than `"AI icon"`.
* Animations are automatically suppressed when the user has `prefers-reduced-motion: reduce` set in their system preferences.
* The popover variant uses the internal beta `Popover` with `role="dialog"`; focus moves into the popover when it opens.
* The popover variant calls `stopPropagation` on click to prevent unintended event bubbling when the icon is nested inside other interactive elements.
```tsx theme={null}
```
## `AiMark` Props
`AiMark` accepts all props from `Icon` except `svg`, plus the following:
Collapses layout padding on the chosen axes so the mark’s box matches a plain icon on those axes. Interactive tooltip and popover variants keep the same hit target; non-interactive variants add padding on the non-collapsed axes.
Forces the pulsing morph animation to play.
Configuration for an optional tooltip or popover overlay. When provided, the icon renders as an interactive button that animates on hover and focus. See `AiMarkWithTooltipOrPopoverConfig` below.
Size of the icon.
Visual style of the AI mark. `"default"` inherits the current text color. `"gradient"` applies a blue gradient fill.
## `AiMarkWithTooltipOrPopoverConfig`
A discriminated union — use `type` to select either the tooltip or popover variant.
### Tooltip variant
Accessible label for the trigger button, announced to screen readers.
Text content displayed inside the tooltip. When omitted, defaults to `AI can make mistakes.`
`}>
Optional props forwarded to the `Tooltip` component.
Discriminant for the tooltip variant.
### Popover variant
Accessible label for the trigger button, announced to screen readers.
Content rendered inside the popover. When omitted, defaults to the standard AI disclaimer message and a Learn more link to the privacy policy.
`}>
Optional props forwarded to `Popover.Content`.
`}>
Optional props forwarded to the internal beta `Popover` root.
Discriminant for the popover variant.
# AI Mark – Design
Source: https://anvil.servicetitan.com/docs/web/components/ai-mark/design
The AI mark is the standard visual indicator for AI-powered experiences in ServiceTitan web applications.
Embedded AI is a rapidly developing area at ServiceTitan. For how the AI Mark fits into broader embedded AI and Atlas usage, see [Embedded AI](/docs/web/patterns/embedded-ai).
The AI Mark signals that a control, label, or surface was touched by AI. It replaces legacy indicators so AI branding stays consistent across the product.
## Anatomy
The AI Mark consists of a single element: the double-star shape that identifies AI-assisted or AI-generated content in product UI.
1. **Double-star shape** — AI Mark
## Options
The AI Mark supports the following configurations for color, size, and related presentation.
### Color
* **Gradient**: A blue gradient, used as the primary option for denoting something is AI.
* **Default**: A `currentColor` option, used against non-default backgrounds, such as within buttons.
### Sizing
The mark comes in four sizes aligned with Anvil icon sizing.
* **small**: 12×12
* **medium**: 16×16
* **large**: 24×24
* **xlarge**: 32×32
## Behavior
### Animation
The AI Mark has two animation modes:
* **Pulse**: The default hover treatment for the mark.
* **Spin**: Used exclusively in situations that perform an action (such as buttons), also on hover.
### Tooltips and popovers
The AI Mark responds to hover and focus, and it supports an optional tooltip or popover disclosure when users need more context than the icon alone.
Pair the AI Mark with a tooltip or popover when short inline copy is not enough and users should be able to read more about how AI is used.
* **Tooltip**: Use for a single line or short string of explanatory text on hover or keyboard focus. Reserve tooltips for lightweight context (for example, what “AI-assisted” means in that spot).
* **Popover**: Use when the disclosure needs richer layout or actions—longer explanations, multiple lines, or controls such as feedback. Popover opens on activation (for example, click), not only on hover.
With either pattern, the mark uses an interactive treatment (including animation on hover and focus) and a larger hit target than the plain icon.
For configuration options and examples, see [AI Mark – Code](/docs/web/components/ai-mark/code).
### Contrast-driven style choice
Use the gradient mark by default. Switch to the monochrome mark when the primary treatment fails contrast requirements—for example, primary Button and similar cases where the gradient does not meet contrast against the background.
### Editable states
The AI Mark responds to edits. When a user starts editing a cell, remove the AI Mark immediately. After editing begins, the value is no longer purely AI-generated, and keeping the mark would misrepresent the content.
## Usage Guidelines
Use the AI Mark when you need a clear, consistent visual that an experience or control is AI-related outside of the dedicated Atlas conversational UI.
### When to use
Use the AI Mark when you need to:
* Label AI-assisted actions or fields in embedded product UI
* Pair with component-level AI affordances built into Anvil2 patterns
* Replace inconsistent legacy AI icons or emoji with the approved double-star mark
### Placement on fields
The AI Mark can sit in the field label or in the body or description area. When a field has a visible label, prefer the label. When there is no label—for example, a Data Table cell—place the mark to the right of what AI has generated. If a group of items was generated with AI (for example, a Card), place the mark to the right of the group title.
### Avoid duplicate marks
If the AI Mark already appears on a triggering element (for example, a button that starts an AI action), do not show it again in the output that element produces. One clear association per action keeps the UI honest and avoids noisy repetition.
### When not to use
Avoid using the AI Mark for:
* **Non-AI features** — Use neutral icons or no AI indicator so users are not misled
* **Atlas-only flows** — Within the centralized conversational Atlas experience, follow Atlas-specific guidance; see [Embedded AI vs Atlas](/docs/web/patterns/embedded-ai#embedded-ai-vs-atlas)
* **Repeated context** — If the mark already appears on the control that caused the content to appear, omit it from the generated content; see [Avoid duplicate marks](#avoid-duplicate-marks)
### Do and don't
## Content
Content within or next to the AI Mark should keep AI disclosure clear and scannable. Prefer plain language that states what the AI does or what the user should expect.
* Pair the mark with labels or headings that describe the AI-assisted outcome, not the technology
* In complex workflows, add short explanatory text or follow-up disclosure as needed in the UI or via tooltip or popover when the mark alone does not carry enough context
* When both a label and description exist, align the mark with [Placement on fields](#placement-on-fields)
## Keyboard Interaction
Users can navigate the AI Mark using the same keyboard patterns as the surrounding control or container (for example, the button or field label that includes the mark).
Read more about embedded AI and how to use the AI Mark within larger workflows in the product.
# Alert – Code
Source: https://anvil.servicetitan.com/docs/web/components/alert/code
Alerts are notifications that inform a user about something.
```tsx lines expandable theme={null}
import { Alert } from "@servicetitan/anvil2";
function App() {
return (
Warning message
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { Alert } from "@servicetitan/anvil2";
function ExampleComponent() {
return (
This is a warning message.
);
}
```
### Alert statuses
Use the `status` prop to set the color and icon of the `Alert`.
```tsx lines expandable theme={null}
import { Alert, Flex } from "@servicetitan/anvil2";
function App() {
return (
console.log("info onClose")} />
console.log("success onClose")}
/>
console.log("warning onClose")}
/>
console.log("danger onClose")}
/>
);
}
export default App;
```
### Closable alerts
Make an alert closable by providing a value to the `onClose` prop.
```tsx lines expandable theme={null}
import { Alert, Flex, Button } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [isOpen, setIsOpen] = useState(true);
return (
{isOpen ? (
setIsOpen(false)} />
) : (
setIsOpen(true)}
style={{ width: "100%" }}
>
Open Alert
)}
);
}
export default App;
```
### Alerts with actions
It is common to use Anvil2 [Buttons](/docs/web/components/button/design) within alerts to create actions. These can easily be arranged using the [Flex](/docs/web/components/flex/code) component.
```tsx lines expandable theme={null}
import { Alert, Text, Flex, Button } from "@servicetitan/anvil2";
function App() {
return (
There was an error loading the page data. Please refresh the page.
Refresh Page
Message Support
);
}
export default App;
```
### AI Mark
Add an [AI Mark](/docs/web/components/ai-mark/code) inline after the alert title by passing `aiMark`. Use `true` for the sparkle only, or pass tooltip or popover configuration.
```tsx lines expandable theme={null}
import { Alert } from "@servicetitan/anvil2";
function App() {
return (
Customer asked about indoor air quality—consider adding a media filter
upgrade and UV option to this HVAC maintenance estimate.
);
}
export default App;
```
```tsx theme={null}
console.log("Alert closed")}
>
This is a warning message.
```
## `Alert` Props
When set, displays the AI mark inline after the title. Use `true` for the sparkle only, or pass tooltip or popover configuration. See [AI Mark](/docs/web/components/ai-mark/code).
The component is closeable if this prop is provided.
# Alert – Design
Source: https://anvil.servicetitan.com/docs/web/components/alert/design
Alerts are notifications that informs a user about something.
## Anatomy
The Alert consists of four primary elements that work together to communicate important information to users.
1. Status icon
2. Title
3. Close action (Optional)
4. Body content (presented as text and actions) (Optional)
## Options
The Alert supports four status types and flexible content configurations to accommodate various messaging scenarios.
### Status
```tsx lines theme={null}
import { Alert, Flex } from "@servicetitan/anvil2";
function App() {
return (
console.log("info onClose")} />
console.log("success onClose")}
/>
console.log("warning onClose")}
/>
console.log("danger onClose")}
/>
);
}
export default App;
```
| Status type | Description |
| ----------- | ----------------------------------------------------------------------- |
| Info | Used for neutral types of notification to users. |
| Success | Used for positive-type confirmations to users. |
| Warning | Used to convey important information that may require user action soon. |
| Danger | Used to convey critical errors that require user action. |
### Content Area
The Alert allows for any type of content inside below the title and icon. This is typically represented as text and 0–2 actions (as [Secondary Buttons](/docs/web/components/button/design#appearances)).
```tsx lines theme={null}
import { Alert, Flex, Button } from "@servicetitan/anvil2";
function App() {
return (
console.log("onClose")} />
console.log("onClose")}>
Body text to the alert.
console.log("onClose")}>
Body text to the alert.
Button
Button
);
}
export default App;
```
### Close
```tsx lines theme={null}
import { Alert, Flex, Button } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [isOpen, setIsOpen] = useState(true);
return (
{isOpen ? (
setIsOpen(false)} />
) : (
setIsOpen(true)}
style={{ width: "100%" }}
>
Open Alert
)}
);
}
export default App;
```
Preserve the close action by default. Remove it only when an alert must persist, such as in warning or danger scenarios.
### AI Mark
```tsx lines theme={null}
import { Alert } from "@servicetitan/anvil2";
function App() {
return (
Customer asked about indoor air quality—consider adding a media filter
upgrade and UV option to this HVAC maintenance estimate.
);
}
export default App;
```
Include an [AI Mark](/docs/web/components/ai-mark/code) on an Alert when the title should indicate AI-generated content.
## Behavior
The Alert responds to content overflow by expanding vertically to accommodate longer messages while maintaining its visual structure.
### Overflow handling
```tsx lines theme={null}
import { Alert, Flex, Button } from "@servicetitan/anvil2";
function App() {
return (
Content inside the Alert will overflow depending on how it is implemented,
with the most typical being wrapping.
Button
Button
);
}
export default App;
```
## Usage Guidelines
### When to Use
Use alerts to keep users informed of important information, such as updates, status, and errors. Alerts work across multiple layouts and scopes of notification.
### When not to use
Alerts should not be used for system-wide notifications.
Avoid overusing Alerts. Overuse can lead to an overwhelming interface.
### Alternatives
#### Alert vs Announcement
Announcements are used to convey app-wide information. They are placed as a global element on the page. Alerts are contextual to an area of an app, and are more flexible in scope and layout position.
#### Alert vs Dialog
Dialogs are interruptive by design, while an Alert is non-disruptive. Prefer using an Alert when information needs to be conveyed. Use a Dialog only when there is a clear need to interrupt a flow, such as a confirmation of destructive or irreversible actions.
#### Alert vs Toast
Alerts are used to communicate information about either part of a page or a whole page. Alerts can be used to summarize information, such as a summary of errors.
Toasts are brief pieces of information and usually appear as the result of user action.
### How to Use
#### As part of a form
Pair alerts with forms, typically as part of an error pattern.
#### Layout positions
Position alerts toward the top of their related context. This includes UIs such as Pages, Drawers, and Dialogs.
## Content
Content within the Alert should communicate clearly and concisely, helping users understand the situation and take appropriate action.
### Use a title that identifies the issue
Titles identify the issue clearly. Write titles in sentence case.
```tsx lines theme={null}
import { Alert } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
**Do**
```tsx lines theme={null}
import { Alert } from "@servicetitan/anvil2";
function App() {
return ;
}
export default App;
```
**Don't**
### Focus on a single message
The body content should focus on a single, simple message or action to avoid overwhelming the user with too much information. Avoid repeating the information in the title.
Do not use periods in short phrases or single sentences.
An exception to this rule is for a summary of errors, typically found with forms.
```tsx lines theme={null}
import { Alert } from "@servicetitan/anvil2";
function App() {
return (
Delete unused themes to add more
);
}
export default App;
```
**Do**
```tsx lines theme={null}
import { Alert } from "@servicetitan/anvil2";
function App() {
return (
We were unable to process your request.
The document template has a maximum of 20 themes.
Your document title is limited to 100 characters.
);
}
export default App;
```
**Caution**
### Write concise, action-oriented body content
```tsx lines theme={null}
import { Alert } from "@servicetitan/anvil2";
function App() {
return (
Delete unused themes to add more
);
}
export default App;
```
**Do**
```tsx lines theme={null}
import { Alert } from "@servicetitan/anvil2";
function App() {
return (
You have reached your theme limit. Your document template has reached its
maximum of 20 themes. To add more themes, delete themes you’re no longer
using.
);
}
export default App;
```
**Don't**
## Keyboard Interaction
Users can navigate the Alert using standard keyboard controls.
| Key | Description |
| --- | --------------------------------------------------------------- |
| Tab | Move to the next tab-able element within the Alert or the page. |
# Tokens
Source: https://anvil.servicetitan.com/docs/web/components/alert/tokens
Token reference for the Alert component.
Alert tokens control the appearance of the Alert component in its info, success, warning, danger, and close-button variants.
## Tokens
## Nested component tokens
Alert includes internal components that bring their own tokens. Override these on the Alert `/tokens` page or globally via the semantic layer.
### Status Icon
## How to override
Use `ThemeProvider` to override component tokens for a subtree:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
{/* children */}
;
```
The `-hover`, `-active`, and `-disabled` variants of any token can be overridden the same way. To theme every component at once, override tier-2 semantic tokens instead — see [Design Tokens](/docs/web/design-tokens).
## Interactive states
Interactive state tokens follow a `-hover` / `-active` / `-disabled` suffix on the base token name. For example, `alert.primary.background.color` has siblings `alert.primary.background.color-hover`, `...color-active`, and `...color-disabled`. The table above surfaces these via the **State** column.
## Related
* [Alert Design](/docs/web/components/alert/design)
* [Alert Code](/docs/web/components/alert/code)
* [Design Tokens](/docs/web/design-tokens)
# Announcement – Code
Source: https://anvil.servicetitan.com/docs/web/components/announcement/code
Announcement notifications convey app-wide information about an event.
```tsx lines expandable theme={null}
import { Announcement } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { Announcement } from "@servicetitan/anvil2";
function ExampleComponent() {
return (
);
}
```
### Announcement Status
Use the `status` prop to update the colors and icon of the `Announcement`.
```tsx lines expandable theme={null}
import { Announcement, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Action Buttons
Pass in a [Button](/docs/web/components/button/design) as the `children` of the `Announcement` to add an action. Multiple actions are allowed, but discouraged.
```tsx lines expandable theme={null}
import { Announcement, Flex, Button } from "@servicetitan/anvil2";
function App() {
return (
Button
Button
Button
);
}
export default App;
```
### Closing Announcements
Add the `onClose` callback prop to the `Announcement` to include a close button.
```tsx lines expandable theme={null}
import { Announcement, Flex, Button } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [isOpen, setIsOpen] = useState(true);
return (
{isOpen ? (
setIsOpen(false)}
/>
) : (
setIsOpen(true)}
style={{ width: "100%" }}
>
Open Announcement
)}
);
}
export default App;
```
```tsx theme={null}
console.log("Announcement closed")}
>
Learn More
```
## `Announcement` Props
Providing this prop will render a close button on the announcement.
# Announcement – Design
Source: https://anvil.servicetitan.com/docs/web/components/announcement/design
Announcement notifications convey app-wide information about an event.
## Anatomy
The Announcement consists of five primary elements that work together to communicate app-wide information to users.
1. Status background
2. Status icon
3. Title
4. Action (Optional)
5. Close action (Optional)
## Options
The Announcement supports three status types and flexible action configurations to accommodate various app-wide messaging scenarios.
### Status
```tsx lines theme={null}
import { Announcement, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
| Status type | Description |
| ----------- | --------------------------------------------------------------------- |
| Info | Used for neutral or positive app-wide information. |
| Warning | Used for important message to users, typically requiring action soon. |
| Danger | Used to convey a major, blocking app-wide issue. |
### Close
```tsx lines theme={null}
import { Announcement, Flex, Button } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [isOpen, setIsOpen] = useState(true);
return (
{isOpen ? (
setIsOpen(false)}
/>
) : (
setIsOpen(true)}
style={{ width: "100%" }}
>
Open Announcement
)}
);
}
export default App;
```
Preserve the close action by default. Remove it only when an announcement must persist, such as in warning or danger scenarios.
### Action
```tsx lines theme={null}
import { Announcement, Flex, Button } from "@servicetitan/anvil2";
function App() {
return (
Button
Button
Button
);
}
export default App;
```
An optional, single action can be added to the Announcement. All action Buttons should use the `"default"` or `"secondary"` appearance to maintain visual hierarchy within the Announcement.
## Behavior
The Announcement responds to content overflow by truncating text while maintaining its fixed height.
### Overflow handling
```tsx lines theme={null}
import { Announcement, Flex, Button } from "@servicetitan/anvil2";
function App() {
return (
Button
);
}
export default App;
```
When space is unavailable, text truncates. The Announcement height remains constant.
## Usage Guidelines
### When to Use
Announcements convey app-wide information. Examples include feature releases, maintenance messaging, and sandbox status.
### When not to use
Announcements should not be used for specific sections of an app, or to convey normal error messaging.
### Alternatives
#### Announcement vs Alert
Announcements are used to convey app-wide information. They are placed as a global element on the page. Alerts are contextual to an area of an app, and are more flexible in scope and layout position.
#### Announcement vs Toast
Announcements are used to convey app-wide messages, such as a new release being available, application maintenance, or sandbox information.
Toasts are short pieces of information usually prompted by user action.
### How to Use
#### Layout positions
Position announcements above all other elements with sticky or fixed positioning. If top placement is not possible, bottom placement with fixed positioning is an alternative, though not recommended. Do not place announcements in other locations.
**Do**
**Caution**
Place announcements at the bottom only when top placement is not possible.
**Don't**
Don't place Announcements inside normal content.
## Content
Content within the Announcement should communicate clearly and concisely, helping users understand app-wide situations and take appropriate action.
### Make clear and concise announcements that are easy to read and scan
Keep to 1 to 2 sentences where possible. Put the most critical information first. When action is required, explain the main task and why the user should do it.
Don't use periods in short phrases or single sentences, and use sentence case. This helps make the content easier to read and more scannable.
```tsx lines theme={null}
import { Announcement } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
```tsx lines theme={null}
import { Announcement } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Make announcements action-oriented
Provide an action when possible to allow users to resolve the issue described in the announcement.
Follow content guidelines for [Buttons](/docs/web/components/button/design#content-guidelines).
```tsx lines theme={null}
import { Announcement, Button } from "@servicetitan/anvil2";
function App() {
return (
Learn More
);
}
export default App;
```
### Buttons should be clear, predictable, and action-oriented
Users should be able to predict what will happen when they click a button.
Lead with a strong verb that encourages action. Use a verb/noun pair on actions in most cases. Common actions like Save, Close, or Cancel do not require an accompanying noun.
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
function App() {
return (
Create Order
Print Label
);
}
export default App;
```
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
function App() {
return (
New invoice
Schedule
);
}
export default App;
```
## Keyboard Interaction
Users can navigate the Announcement using standard keyboard controls.
If an action exists in the Announcement (the close or optional action), it will use the [Button](/docs/web/components/button/design)'s keyboard interactions, otherwise the Announcement has no keyboard interactions.
# Tokens
Source: https://anvil.servicetitan.com/docs/web/components/announcement/tokens
Token reference for the Announcement component.
Announcement tokens control the appearance of the Announcement component in its info, success, warning, and danger variants.
## Tokens
## How to override
Use `ThemeProvider` to override component tokens for a subtree:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
{/* children */}
;
```
The `-hover`, `-active`, and `-disabled` variants of any token can be overridden the same way. To theme every component at once, override tier-2 semantic tokens instead — see [Design Tokens](/docs/web/design-tokens).
## Interactive states
Interactive state tokens follow a `-hover` / `-active` / `-disabled` suffix on the base token name. For example, `announcement.primary.background.color` has siblings `announcement.primary.background.color-hover`, `...color-active`, and `...color-disabled`. The table above surfaces these via the **State** column.
## Related
* [Announcement Design](/docs/web/components/announcement/design)
* [Announcement Code](/docs/web/components/announcement/code)
* [Design Tokens](/docs/web/design-tokens)
# Avatar – Code
Source: https://anvil.servicetitan.com/docs/web/components/avatar/code
Avatars visually represent an entity, typically a person or business.
```tsx lines expandable theme={null}
import { Avatar } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { Avatar } from "@servicetitan/anvil2";
const ExampleComponent = () => {
return ;
};
```
### Changing the avatar colors
Avatar colors can be changed using the `color` prop. Internally, the color could be adjusted in order to ensure low vision and colorblind users can read the text.
```tsx lines expandable theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Showing online status on avatars
Set the `status` prop to `"online"` or `"offline"` to add an indicator to the avatar.
```tsx lines expandable theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Avatar image and name
The content within an avatar will change based on the `name` and `image` props provided. If an `image` is provided, and does not fail to load, it will be displayed.
If no `image` is provided, or it fails to load, two characters of the `name` value will show instead:
* If `name` is a single word, the first two characters will be used
* If `name` is multiple words, the first characters of the first and last words are used
### Avatar with image and name
```tsx lines theme={null}
import { Avatar } from "@servicetitan/anvil2";
import Logo_block from "../assets/Logo_block.jpg";
function App() {
return ;
}
export default App;
```
### Avatar with name only
```tsx lines theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Stacking avatars in a group
Avatars can be grouped using the `Avatar.Group` component. They will overlap slightly, with the last `Avatar` component child on top.
```tsx lines expandable theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Avatar as a button
To make an avatar interactive, wrap it with a `ButtonCompound` with `shape="circular"`. This will add a `button` element around the avatar which can be used for tab focus and interactions.
```tsx lines expandable theme={null}
import { Avatar, ButtonCompound } from "@servicetitan/anvil2";
function App() {
return (
alert("Clicked!")}>
);
}
export default App;
```
```tsx theme={null}
```
## `Avatar` Props
This string will be used for alt text and screenreaders even if it is not
visible.
```tsx theme={null}
```
## `AvatarGroup` Props
The group will truncate after showing this maximum number of avatars, and will
display the number of additional avatars (e.g. "+3").
# Avatar – Design
Source: https://anvil.servicetitan.com/docs/web/components/avatar/design
Avatars visually represent an entity, typically a person or business.
## Anatomy
The Avatar consists of three primary elements that work together to visually represent an entity.
1. Avatar body
2. Status indicator (Optional)
3. Avatar Group
## Options
The Avatar supports multiple content types, sizes, and grouping configurations to accommodate various representation scenarios.
### Image
```tsx lines theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
import dog01 from "../assets/dog-01.png";
import dog02 from "../assets/dog-02.png";
function App() {
return (
);
}
export default App;
```
### Initials
When an image is not used, the avatar displays initials derived from the name, calculated based on specific rules.
```tsx lines theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
When two or more words are used, the first letter of the first two words is used.
```tsx lines theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
When only one word is provided in the name, the first two letters are used.
```tsx lines theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
On 'small' Avatars, only the first letter is used regardless of what name is used.
### Custom background colors
```tsx lines theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
Avatars provide one color by default. They accept any color or Token value. Text color auto-calculates to achieve the highest available contrast. Use a Token value that supports theming.
### Sizes
```tsx lines theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
| Type | Dimensions |
| ------ | ------------- |
| Small | 24px / 1.5rem |
| Medium | 32px / 2rem |
| Large | 48px / 3rem |
### Status
Avatars can also convey an online or offline status.
```tsx lines theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Grouping
```tsx lines theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
Avatars can be grouped together in a condensed form. The specific spacing depends on the size of individual Avatars.
Grouping also allows for a max count of Avatars, where a +X will be displayed in place of additional Avatars. This cutoff is configurable to any number.
```tsx lines theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Status in groups
```tsx lines theme={null}
import { Avatar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
## Usage Guidelines
### When to Use
Avatars are appropriate when referencing a person. They also represent other entities such as businesses. Follow local standards for business entity representation.
### When not to use
Avatars should be avoided to display icons or emojis, and an element such as a Button may be more appropriate.
### How to Use
#### Optionally add text labels to Avatars
Text labels are not provided directly with an Avatar, but are recommended when space allows. Customize labels as needed.
#### Adding interactivity to an Avatar
Combine an avatar with a [Button Compound](/docs/web/components/button/design#button-compound) to add interaction. This provides interactive states and accessibility.
#### Only use a Tooltip when also adding interactivity
Since an Avatar is non-interactive by default, use a Tooltip when the Avatar is focusable by adding `tabIndex={0}`. A text label next to the Avatar is still the preferred option.
**Caution**
#### Don't make isolated changes to an Avatar's shape
Shape is consistent when representing a category of entities. Deviating from this confuses users.
**Don't**
## Content
Avatar will always display initials when no image is provided.
## Keyboard Interaction
The default Avatar and Avatar Group, do not have any keyboard interactions.
# Tokens
Source: https://anvil.servicetitan.com/docs/web/components/avatar/tokens
Token reference for the Avatar component.
Avatar tokens control the appearance of the Avatar component in its background, foreground, border, and indicator variants.
## Tokens
## How to override
Use `ThemeProvider` to override component tokens for a subtree:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
{/* children */}
;
```
The `-hover`, `-active`, and `-disabled` variants of any token can be overridden the same way. To theme every component at once, override tier-2 semantic tokens instead — see [Design Tokens](/docs/web/design-tokens).
## Interactive states
Interactive state tokens follow a `-hover` / `-active` / `-disabled` suffix on the base token name. For example, `avatar.primary.background.color` has siblings `avatar.primary.background.color-hover`, `...color-active`, and `...color-disabled`. The table above surfaces these via the **State** column.
## Related
* [Avatar Design](/docs/web/components/avatar/design)
* [Avatar Code](/docs/web/components/avatar/code)
* [Design Tokens](/docs/web/design-tokens)
# Badge – Code
Source: https://anvil.servicetitan.com/docs/web/components/badge/code
Badges are used to indicate that something requires a user's attention on an interactive element.
```tsx lines expandable theme={null}
import { Badge, Flex, Button } from "@servicetitan/anvil2";
import Warning from "@servicetitan/anvil2/assets/icons/material/round/warning.svg";
function App() {
return (
4
What's new?
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { Badge, Button } from "@servicetitan/anvil2";
const ExampleComponent = () => {
return (
Alerts
12
);
};
```
### Use relative positioning on badge parent
In order for a badge to properly position itself, the parent of the `Badge` component that the badge should be placed on needs to have `position: relative`.
```tsx theme={null}
Content
10
```
### Adjusting badge position
When used with square or rectangular elements, the badge should position itself nicely on the corner of the element. Sometimes, some fine-tuning may be required if the element has a different shape, such as text. Use the `offset` prop to supply CSS sizing values to the `x` and `y` parameters.
```tsx lines expandable theme={null}
import { Badge, Link } from "@servicetitan/anvil2";
function App() {
return (
What's new?
12
);
}
export default App;
```
### Badges without children
If no `children` element is provided for the `Badge` component, only a red dot will appear.
```tsx lines expandable theme={null}
import { Badge, Button } from "@servicetitan/anvil2";
function App() {
return (
Notifications
);
}
export default App;
```
## React Accessibility
### Using aria-label
```tsx theme={null}
12
```
**Do**
Use the aria-label prop to provide better context for the number displayed within the badge.
```tsx theme={null}
12
```
**Don't**
Provide only the children for screen readers to use.
For more guidance on accessible labels and ARIA, see [labels and CTA best practices](/docs/accessibility/labels-and-ctas).
```tsx theme={null}
12
```
## `Badge` Props
In addition to the props listed below, the `Badge` component can accept any valid HTML `sup` props.
Fine-tune the badge position with CSS sizing values.
# Badge – Design
Source: https://anvil.servicetitan.com/docs/web/components/badge/design
Badges are used to indicate that something requires a user's attention on an interactive element.
## Anatomy
The Badge consists of two primary elements that work together to indicate attention requirements on interactive elements.
1. Parent element
2. Badge
## Options
The Badge supports default and value-less configurations to accommodate various notification scenarios.
### Default
```tsx lines theme={null}
import { Badge, Button } from "@servicetitan/anvil2";
function App() {
return (
Button
6
);
}
export default App;
```
By default, show the number of alerts or notifications on the Badge so users see them without diving deeper.
### Without value
```tsx lines theme={null}
import { Badge, Button } from "@servicetitan/anvil2";
function App() {
return (
Button
);
}
export default App;
```
When the number of alerts or notifications is not available, use an empty Badge.
## Behavior
The Badge responds to value limits and positioning requirements while maintaining consistent visual indication.
### Max value
```tsx lines theme={null}
import { Badge, Button } from "@servicetitan/anvil2";
function App() {
return (
Button
99+
);
}
export default App;
```
**Do**
Set a max value for Badges. Use 99 in most cases. When values of 100+ are frequent, use 999.
```tsx lines theme={null}
import { Badge, Button } from "@servicetitan/anvil2";
function App() {
return (
Button
999999999
);
}
export default App;
```
**Don't**
### Offset
When using Badge with components like [Link](/docs/web/components/link/design), overlap may not be ideal. Set offset to resolve this issue.
```tsx lines theme={null}
import { Badge, Link } from "@servicetitan/anvil2";
function App() {
return (
What's new?
12
);
}
export default App;
```
**Do**
```tsx lines theme={null}
import { Badge, Link } from "@servicetitan/anvil2";
function App() {
return (
What's new?
12
);
}
export default App;
```
**Don't**
## Usage Guidelines
### When to Use
Use a Badge to indicate to users that something requires attention.
### When not to use
Badges should not be used to label or organize information, instead use a [Chip](/docs/web/components/chip/design). Badges should also not be used to indicate the count of items within (e.g., the number of items inside of a Tab or folder). Use a Chip for these cases.
### Alternatives
#### Badge vs Chip
Chips are used to categorize, label, and add context to items whereas Badges are used to indicate something requires attention, such as a notification. Additionally, Chips can be interactive while Badges are not.
### How to Use
#### Add Badge to interactive elements
```tsx lines theme={null}
import { Badge, Button } from "@servicetitan/anvil2";
function App() {
return (
Button
6
);
}
export default App;
```
Attach Badges to interactive elements, such as icon buttons, to indicate a count (e.g., the number of notifications).
#### Without a count
```tsx lines theme={null}
import { Badge, Button } from "@servicetitan/anvil2";
function App() {
return (
Button
);
}
export default App;
```
Use Badges without a value to denote an unread notification or message.
#### Do not change the color of the Badge
**Don't**
#### Only use numbers inside of a Badge
```tsx lines theme={null}
import { Badge, Button } from "@servicetitan/anvil2";
function App() {
return (
Button
New Item
);
}
export default App;
```
**Don't**
Badges should only contain numerical values.
## Content
Content within the Badge should clearly indicate the count or presence of items requiring attention through numerical values.
## Keyboard Interaction
Users can navigate the Badge using standard keyboard controls.
For accessibility, `aria-label` should be defined to match the context.
With the example above, there are 2 methods:
* Adding `aria-label="Cases with 6 items to review"` to the Button
* This will read as "Cases with 6 items to review, button"
* Adding `aria-label="with 6 items to review"` to the Badge only
* This will read as "Cases, button, with 6 items to review"
For more guidance on accessible labels and ARIA, see [labels and CTA best practices](/docs/accessibility/labels-and-ctas).
# Tokens
Source: https://anvil.servicetitan.com/docs/web/components/badge/tokens
Token reference for the Badge component.
Badge tokens control the appearance of the Badge component in its background and foreground variants.
## Tokens
## How to override
Use `ThemeProvider` to override component tokens for a subtree:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
{/* children */}
;
```
The `-hover`, `-active`, and `-disabled` variants of any token can be overridden the same way. To theme every component at once, override tier-2 semantic tokens instead — see [Design Tokens](/docs/web/design-tokens).
## Interactive states
Interactive state tokens follow a `-hover` / `-active` / `-disabled` suffix on the base token name. For example, `badge.primary.background.color` has siblings `badge.primary.background.color-hover`, `...color-active`, and `...color-disabled`. The table above surfaces these via the **State** column.
## Related
* [Badge Design](/docs/web/components/badge/design)
* [Badge Code](/docs/web/components/badge/code)
* [Design Tokens](/docs/web/design-tokens)
# Breadcrumbs – Code
Source: https://anvil.servicetitan.com/docs/web/components/breadcrumbs/code
Breadcrumbs represent the hierarchy of an app that users can navigate through.
```tsx lines expandable theme={null}
import { Breadcrumbs } from "@servicetitan/anvil2";
function App() {
return (
Home Page
Sub Page
Current Page
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { Breadcrumbs } from "@servicetitan/anvil2";
function ExampleComponent() {
return (
Home
Page
);
}
```
### Building breadcrumbs from data
Breadcrumbs are slim wrappers around an HTML `nav` element with some `a` links and a `span` for the current page.
To create dynamic breadcrumbs on a page, collect the page names and URLs in an array and map it to `Breadcrumbs.Link` components.
**Note:** the final breadcrumb will be treated as the current page, and will render as plain text instead of a link.
```tsx theme={null}
const sampleData = [
{
name: "Home",
url: "https://some.url/",
},
{
name: "Reports",
url: "https://some.url/reports",
},
{
name: "Weekly Report",
// this url will be ignored as the current page
url: "https://some.url/reports/weekly-report",
},
];
export const SampleComponent = () => {
return (
{sampleData.map((link) => (
{link.name}
))}
);
};
```
```tsx theme={null}
Home
Products
```
## `Breadcrumbs` Props
The `Breadcrumbs` component can accept any valid HTML `nav` props.
```tsx theme={null}
Home
```
## `Breadcrumbs.Link` Props
The `Breadcrumbs.Link` component can accept any valid HTML `a` props.
# Breadcrumbs – Design
Source: https://anvil.servicetitan.com/docs/web/components/breadcrumbs/design
Breadcrumbs represent the hierarchy of an app that users can navigate through.
## Anatomy
The Breadcrumbs consists of three primary elements that work together to represent navigation hierarchy.
1. Breadcrumb item link
2. Separator
3. Current page
## Options
The Breadcrumbs supports one configuration that shows each Breadcrumb Item as a link, with the last item representing the current page.
### Default
```tsx lines theme={null}
import { Breadcrumbs } from "@servicetitan/anvil2";
function App() {
return (
Home Page
Sub Page
Current Page
);
}
export default App;
```
Breadcrumbs have one configuration. Each Breadcrumb Item displays as a link, and the last Breadcrumb Item represents the current page.
## Behavior
The Breadcrumbs responds to navigation interactions and overflow scenarios while maintaining clear hierarchy representation.
### Visual States
```tsx lines theme={null}
import { Breadcrumbs, Flex } from "@servicetitan/anvil2";
function App() {
return (
Default Breadcrumb
Current Page
Hover Breadcrumb
Current Page
Active Breadcrumb
Current Page
Focus Visible Breadcrumb
Current Page
);
}
export default App;
```
Note that the last Breadcrumb item, the current page, is not interactive, and has no additional visual states.
### Interactions
All Breadcrumb items except the last item are links. Do not place items that cannot be represented as navigational elements in the Breadcrumbs.
### Breadcrumb Group Overflow
```tsx lines theme={null}
import { Breadcrumbs, Flex } from "@servicetitan/anvil2";
function App() {
return (
Item 1
Item 2
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
);
}
export default App;
```
### Breadcrumb Item Overflow
```tsx lines theme={null}
import { Breadcrumbs } from "@servicetitan/anvil2";
function App() {
return (
Item 1
Longer Item
This is a very long item to show what happens when one individual
Breadcrumb item is incredibly long for some reason
Item 4
Item 5
);
}
export default App;
```
## Usage Guidelines
### When to Use
Use Breadcrumbs when the product area has a multi-level hierarchy that allows users to understand and navigate the product hierarchy.
### When not to use
Do not use Breadcrumbs when:
* There is no hierarchical relation to display
* Users cannot navigate through the hierarchy displayed in a Breadcrumb
* A user is in a linear flow. Use a [Stepper](/docs/web/components/stepper/design) instead.
### How to Use
#### Only use Breadcrumbs to represent the site hierarchy
Use Breadcrumbs only to represent the hierarchy of the site or product area. This is sometimes known as location-based Breadcrumbs.
Do not use Breadcrumbs to denote path-based behavior, where Breadcrumb content is dynamically generated based on the path users took to get to a page.
```tsx lines theme={null}
import { Breadcrumbs } from "@servicetitan/anvil2";
function App() {
return (
Web
Components
Breadcrumbs
);
}
export default App;
```
**Do** Example using this doc site to show the site hierarchy.
```tsx lines theme={null}
import { Breadcrumbs } from "@servicetitan/anvil2";
function App() {
return (
Button
Forms
Breadcrumbs
);
}
export default App;
```
**Don't** Example of a hypothetical user session arriving to the same page. Breadcrumbs should not be used this way.
#### Always show the current page in the Breadcrumbs
When using Breadcrumbs, always represent the current page in the Breadcrumbs, even if the page title shows the same content.
```tsx lines theme={null}
import { Breadcrumbs } from "@servicetitan/anvil2";
function App() {
return (
Item 1
Item 2
Current page
);
}
export default App;
```
**Do**
```tsx lines theme={null}
import { Breadcrumbs } from "@servicetitan/anvil2";
function App() {
return (
Item 1
Item 2
);
}
export default App;
```
**Don't**
#### Always provide navigation within Breadcrumbs
Breadcrumb items always have a link to navigate. If part of the hierarchy lacks a link, do not include it in the Breadcrumb trail. The trailing Breadcrumb item, the current page, does not display its own link.
#### Skip the Breadcrumbs for the top-level item
The top-most part of the Breadcrumb hierarchy does not need to display the Breadcrumbs directly. For example, if the first Breadcrumb item was a "Settings" page, the actual Settings page would not show the Breadcrumb.
## Content
Content within the Breadcrumbs should clearly represent the navigation hierarchy through descriptive labels.
## Keyboard Interaction
Users can navigate the Breadcrumbs using standard keyboard controls.
| Key | Interaction |
| ----- | --------------------------------------------------- |
| Enter | Engages the link and moves focus to the link target |
| Tab | Navigates linked Breadcrumbs in normal Tab order |
# Tokens
Source: https://anvil.servicetitan.com/docs/web/components/breadcrumbs/tokens
Token reference for the Breadcrumbs component.
Breadcrumbs tokens control the appearance of the Breadcrumbs component in its link variant.
## Tokens
## How to override
Use `ThemeProvider` to override component tokens for a subtree:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
{/* children */}
;
```
The `-hover`, `-active`, and `-disabled` variants of any token can be overridden the same way. To theme every component at once, override tier-2 semantic tokens instead — see [Design Tokens](/docs/web/design-tokens).
## Interactive states
Interactive state tokens follow a `-hover` / `-active` / `-disabled` suffix on the base token name. For example, `breadcrumbs.primary.background.color` has siblings `breadcrumbs.primary.background.color-hover`, `...color-active`, and `...color-disabled`. The table above surfaces these via the **State** column.
## Related
* [Breadcrumbs Design](/docs/web/components/breadcrumbs/design)
* [Breadcrumbs Code](/docs/web/components/breadcrumbs/code)
* [Design Tokens](/docs/web/design-tokens)
# Button Toggle – Code
Source: https://anvil.servicetitan.com/docs/web/components/button-toggle/code
Button Toggles let users alternate between two states with a single click.
```tsx lines expandable theme={null}
import { ButtonToggle, Flex } from "@servicetitan/anvil2";
import Star from "@servicetitan/anvil2/assets/icons/material/round/star.svg";
import Favorite from "@servicetitan/anvil2/assets/icons/material/round/favorite.svg";
import Bookmark from "@servicetitan/anvil2/assets/icons/material/round/bookmark.svg";
function App() {
return (
Star
Heart
Bookmark
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { ButtonToggle } from "@servicetitan/anvil2";
function ExampleComponent() {
return Click to toggle ;
}
```
### Button toggle sizes
Use the `size` prop to adjust the text size and padding of the button.
```tsx lines expandable theme={null}
import { ButtonToggle, Flex } from "@servicetitan/anvil2";
function App() {
return (
Large
Medium(Default)
Small
);
}
export default App;
```
### Controlling button toggles
```tsx lines expandable theme={null}
import { ButtonToggle } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [isChecked, setIsChecked] = useState(true);
return (
setIsChecked((prev) => !prev)}
>
Click to Toggle
);
}
export default App;
```
#### Uncontrolled button toggles
By default, button toggles are uncontrolled. The checked state changes when the button is clicked. To make the button toggle checked by default, use the `defaultChecked` prop.
```tsx lines expandable theme={null}
import { ButtonToggle } from "@servicetitan/anvil2";
function App() {
return Click to Toggle ;
}
export default App;
```
#### Controlled button toggles
To programmatically set the checked state of a button toggle, use the `checked` prop.
### Handling state changes
The `onChange` prop accepts a function that passes the change event and current state object. This can be used to trigger other actions depending on the changed state.
```tsx theme={null}
() => {
doSomething(state: ButtonToggleState) {
console.log(`${state.value} is pressed: ${state.pressed}`)
}
return (
doSomething(state)}>
Toggle
)
}
```
## React Accessibility
### Icon-only button toggles
```tsx theme={null}
```
**Do**
Use aria-label or aria-labelledby for a button without any text.
```tsx theme={null}
```
**Don't**
Provide only an icon for screen readers to read.
For more guidance on button labels and accessibility, see [button accessibility best practices](/docs/accessibility/labels-and-ctas#buttons-and-links).
```tsx theme={null}
console.log(state)}
>
Click to toggle
```
## `ButtonToggle` Props
The SVGR library imports `*.svg` files as React components, so `icon` can
accept either a React component, or an object with `before` and/or `after`
keys to specify the placement of the icon.
# Button Toggle – Design
Source: https://anvil.servicetitan.com/docs/web/components/button-toggle/design
Button Toggles let users alternate between two states with a single click.
## Anatomy
The Button Toggle consists of three primary elements that work together to let users alternate between two states with a single click.
1. Container
2. Text label
3. Icon
## Options
The Button Toggle supports multiple size configurations to accommodate various toggle scenarios.
#### Sizes
| Size | Height |
| ------ | ------ |
| Large | 48px |
| Medium | 40px |
| Small | 32px |
## Behavior
The Button Toggle responds to user interaction with distinct visual states and flexible layout behaviors.
### Visual States
#### Default
#### Checked
#### Text wrap
Button Toggles fill all available space, wrapping only when no space remains. Text is center-aligned when wrapping.
#### Widths
In general, Button Toggles should match the width of their text. Container width works well when aligning with specific layout elements. Button Toggle Icon width always equals its height.
## Usage Guidelines
Use the Button Toggle to control the binary state of a Button.
### When to Use
Button Toggles control the binary state of a Button. They can contain text, text with an icon, or just an icon.
### How to Use
Button Toggles work well when selecting multiple options from a set of available options.
```tsx lines theme={null}
import { ButtonToggle, Flex } from "@servicetitan/anvil2";
import Star from "@servicetitan/anvil2/assets/icons/material/round/star.svg";
import Favorite from "@servicetitan/anvil2/assets/icons/material/round/favorite.svg";
import Bookmark from "@servicetitan/anvil2/assets/icons/material/round/bookmark.svg";
function App() {
return (
Star
Heart
Bookmark
);
}
export default App;
```
**Do**
## Content
Content within the Button Toggle should clearly communicate the toggle state through labels and optional icons.
## Keyboard Interaction
Users can navigate the Button Toggle using standard keyboard controls.
| Key | Interaction |
| -------------- | --------------------- |
| Space or Enter | Toggles checked state |
Button Toggles using an href will also use this keyboard combination.
### Accessibility
Anvil provides most of the accessibility needs for Button Toggle out of the box, and only need some additional considerations for library users.
#### Provide labels for Icon Buttons
Regardless of whether or not a label is visible on the page, a label should be provided to describe the action for both screen readers and for users who focus on the element.
For more guidance on button labels and accessibility, see [button accessibility best practices](/docs/accessibility/labels-and-ctas#buttons-and-links).
# Tokens
Source: https://anvil.servicetitan.com/docs/web/components/button-toggle/tokens
Token reference for the Button Toggle component.
Button Toggle tokens control the appearance of the Button Toggle component in its unpressed and pressed variants.
## Tokens
## How to override
Use `ThemeProvider` to override component tokens for a subtree:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
{/* children */}
;
```
The `-hover`, `-active`, and `-disabled` variants of any token can be overridden the same way. To theme every component at once, override tier-2 semantic tokens instead — see [Design Tokens](/docs/web/design-tokens).
## Interactive states
Interactive state tokens follow a `-hover` / `-active` / `-disabled` suffix on the base token name. For example, `button-toggle.primary.background.color` has siblings `button-toggle.primary.background.color-hover`, `...color-active`, and `...color-disabled`. The table above surfaces these via the **State** column.
## Related
* [Button Toggle Design](/docs/web/components/button-toggle/design)
* [Button Toggle Code](/docs/web/components/button-toggle/code)
* [Design Tokens](/docs/web/design-tokens)
# Button – Code
Source: https://anvil.servicetitan.com/docs/web/components/button/code
Buttons are clickable UI elements that trigger an action or event.
```tsx lines expandable theme={null}
import { Button } from "@servicetitan/anvil2";
import Star from "@servicetitan/anvil2/assets/icons/material/round/star.svg";
function App() {
return (
Click me
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { Button, ButtonCompound, ButtonLink, Card } from "@servicetitan/anvil2";
function ExampleComponent() {
return (
<>
Click me
Click me
Click me
>
);
}
```
The `Button` component can be used in the same way that you would use an HTML `button` element, while the `ButtonLink` component can be used in the same way that you would use an HTML `a` element.
### Form Submit
The `type` prop can be used to alter the default functionality of the button. A common use case is to use `type="submit"` in order to submit the content of a form.
```tsx theme={null}
```
### Button links
In general, [Links](/docs/web/components/link/design) should be used for navigating to other pages or website. To create a link that is styled like a button, use the `ButtonLink` component.
```tsx theme={null}
A project label can be attached to expense items like timesheet activities
to provide a visual representation of expense spending.
Learn more
```
### Using button compounds to make components interactive
To add interactivity to components that are usually static, such as avatars, use the `ButtonCompound` component.
```tsx theme={null}
```
```tsx lines expandable theme={null}
import { ButtonCompound, Avatar } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
#### Interactive cards
To make cards clickable, use the [`InteractiveCard`](/docs/web/components/interactive-card/code) component. It supports nested interactive elements without accessibility violations.
```tsx lines expandable theme={null}
import { ButtonCompound, Card, Text } from "@servicetitan/anvil2";
function App() {
return (
Interactive Card
You can click this card!
);
}
export default App;
```
### AI Mark
Use the `aiMark` prop to display an AI indicator. `aiMark` accepts a `boolean`, or one of:
* `"mark"`
* `"chat"`
* `"edit"`
* `"form"`
* `"mic"`
* `"search"`
The `aiMark` prop cannot be used together with a before icon (a single `icon` Svg or `icon={{ before: Svg }}`).
| Button usage | `aiMark` | `icon` allowed? |
| ------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------ |
| With `children` | `true` or an AI icon name | Only after: use `icon={{ after: Svg }}` or omit `icon`. You cannot use a before icon or a single `icon` Svg. |
| Icon-only (no `children`) | `true` or an AI icon name | No. Do not pass `icon`. Only the AI icon is shown. Always provide `aria-label`. |
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
import ArrowForwardIcon from "@servicetitan/anvil2/assets/icons/material/round/arrow_forward.svg";
function App() {
return (
Summarize Visit
Autofill Work Order
);
}
export default App;
```
**With children:**
```tsx theme={null}
Generate
Report
```
**Icon-only AI:**
```tsx theme={null}
```
### Minimum width for buttons
If a `Button` component has any `children`, it will have a `5rem` minimum width. This is not applied in icon-only buttons using the `icon` prop. In some cases, such as when adding a `Badge` as a child of a `Button` component without any text, this can lead to unexpected width changes.
Either override the `width` and `minWidth` of the `Button` component, or make the `Badge` a sibling of the `Button` inside of a `div` with relative positioning.
```tsx theme={null}
// option 1
2
// option 2
2
```
## React Accessibility
### Icon-only buttons
```tsx theme={null}
```
**Do**
Use aria-label or aria-labelledby for a button without any text.
```tsx theme={null}
```
**Don't**
Provide only an icon for screen readers to read.
For more guidance on button labels and accessibility, see [button accessibility best practices](/docs/accessibility/labels-and-ctas#buttons-and-links).
```tsx theme={null}
console.log("Clicked")}
>
Click me
```
## `Button` Props
Shows an AI indicator. When set with button text, a leading `icon` (plain Svg or `icon.before`) is not rendered—the AI icon is used instead. On an icon-only button, `icon` is not rendered. Use `icon={{ after: Svg }}` for a trailing icon with text.
The SVGR library imports `*.svg` files as React components, so `icon` can
accept either a React component, or an object with `before` or `after` key
to specify the placement of the icon.
Setting this to `true` will put the button into a loading state.
```tsx theme={null}
```
## `ButtonCompound` Props
```tsx theme={null}
Click me
```
## `ButtonLink` Props
The SVGR library imports `*.svg` files as React components, so `icon` can
accept either a React component, or an object with `before` or `after` key
to specify the placement of the icon.
Setting this to `true` will put the button into a loading state.
# Button – Design
Source: https://anvil.servicetitan.com/docs/web/components/button/design
Buttons are clickable UI elements that trigger an action or event.
## Anatomy
The Button consists of three primary elements that work together to trigger actions or events.
1. Container
2. Text label
3. Icon
## Options
The Button supports multiple appearances, sizes, and icon configurations to accommodate various action scenarios.
### Appearances
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
function App() {
return (
Primary
Secondary
Ghost
Danger
Danger Secondary
);
}
export default App;
```
| Appearance | When to use |
| ---------- | ---------------------------------------------------------------------------------------------------------------- |
| Primary | Reserved for important, essential actions on the page. Avoid using more than 3 primary actions in the same view. |
| Secondary | Used as the most common style of actions. |
| Ghost | For less prominent actions, and a good fit for icon-only actions. |
### Sizes
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
function App() {
return (
Extra Small
Small
Medium
Large
);
}
export default App;
```
| Size | Height |
| ----------- | ------ |
| Extra Small | 28px |
| Small | 32px |
| Medium | 40px |
| Large | 48px |
Extra small buttons should only be used on desktop devices, as they do not meet the 44x44px tap target required for mobile.
### Icons
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
import FileUpload from "@servicetitan/anvil2/assets/icons/material/round/file_upload.svg";
import KeyboardArrowDown from "@servicetitan/anvil2/assets/icons/material/round/keyboard_arrow_down.svg";
import Edit from "@servicetitan/anvil2/assets/icons/material/round/edit.svg";
function App() {
return (
Prefix Icon
Suffix Icon
);
}
export default App;
```
Icons can be placed both before and after the Button's text label, and can even replace the text label as an Icon Button.
See the [Button Icon usage guidelines](#icons-in-buttons) on when to use icons, and accessibility considerations when using icons.
### Button Compound
A Button Compound is a standalone component that provides button interactivity to another component. This can be useful for components such as anAvatar, which by themselves lack interactivity.
#### Button Compound Option Circular
```tsx lines theme={null}
import { ButtonCompound, Avatar } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
The shape of the Button Compound can be changed to conform to the shape of the component it's overlaying, such as an Avatar.
### Button Link
```tsx lines theme={null}
import { ButtonLink, Flex } from "@servicetitan/anvil2";
function App() {
return (
Link to Web
);
}
export default App;
```
The **Button Link** is a specialized Button component that uses an `` tag under the hood. It gives you the visual style of a button with the correct HTML semantics of a link, making it ideal for actions that navigate to a new page.
The [**Link Button**](/docs/web/components/link/design) is its reverse: a Button component styled to look like a simple text link. Use it when an action doesn't involve navigation but you need a link's visual appearance, such as for triggering a modal.
## Behavior
The Button responds to user interaction with distinct visual states and flexible layout behaviors.
### Visual States
#### Primary
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
function App() {
return (
Rest
Hover
Active
Focus Visible
Disabled
);
}
export default App;
```
#### Danger Secondary
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
function App() {
return (
Rest
Hover
Active
Focus Visible
Disabled
);
}
export default App;
```
#### Secondary
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
function App() {
return (
Rest
Hover
Active
Focus Visible
Disabled
);
}
export default App;
```
#### Ghost
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
function App() {
return (
Rest
Hover
Active
Focus Visible
Disabled
);
}
export default App;
```
#### Danger Primary
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
function App() {
return (
Rest
Hover
Active
Focus Visible
Disabled
);
}
export default App;
```
#### Disabled
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
function App() {
return (
Primary
Primary
Secondary
Secondary
Ghost
Ghost
Danger
Danger
Danger Secondary
Danger Secondary
);
}
export default App;
```
The disabled style is consistent across each button variant. This style is useful when preventing user clicks when an action isn't available or when a submission of some kind is taking place.
### Text wrap
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
function App() {
return (
Text Does Not Have A Max Width
Text Will Wrap To A Second Line In Code If It Runs Out Of Space
);
}
export default App;
```
Buttons fill all available space, wrapping only when no space remains. Text is center-aligned when wrapping.
### Widths
```tsx lines theme={null}
import { Button, Grid } from "@servicetitan/anvil2";
import Edit from "@servicetitan/anvil2/assets/icons/material/round/edit.svg";
function App() {
return (
Text Width
Set Width
Container Width
);
}
export default App;
```
In general, Buttons should match the width of their text. Container width works well when aligning with specific layout elements. Button Icon width always equals its height.
### Left-aligned Text
```tsx lines theme={null}
import { Button, Flex } from "@servicetitan/anvil2";
function App() {
return (
Button
A slightly longer Button
Another Button
);
}
export default App;
```
Button text is typically centered. Use left-aligned text when aligning with specific layout elements or when stacking multiple buttons vertically.
## Usage Guidelines
Use the Button to denote most forms of actions on the page.
### When not to use
In general, buttons should not be used for navigation. See the Link comparison below for more details related to Links. For other navigational contexts, consider the Tab or Side Nav.
### Alternatives
#### Button vs Link
In general, Buttons are used to denote an action, while a Link is used to denote navigation. This distinction in practice can be blurry.
##### When navigating
In general, Links are the preferred choice for navigating. There are a few scenarios when a Button may be used however:
* Use a Button when emphasis is needed. Sometimes a page's call to action is navigating somewhere else.
* Use a Button when navigation is mixed with actions.
##### With Actions
In general, Buttons are the preferred choice for actions. A Link however may be used when the action priority is low and space is tight.
##### With triggering overlays
Triggering an overlay UI can be treated like navigation.
### How to Use
#### Button Pairing
There are 3 levels of emphasis in our Buttons: primary, secondary, and ghost. This corresponds to an emphasis scheme of high, medium, and low emphasis.
##### Recommended pairings
```tsx lines theme={null}
import { Button, Grid, Flex } from "@servicetitan/anvil2";
function App() {
return (
Primary
Secondary
Primary
Ghost
Secondary
Secondary
Secondary
Ghost
Ghost
Ghost
);
}
export default App;
```
**Do**
Most combinations of Primary, Secondary, and Ghost are allowed. In general, use the hierarchy that best represents its relative importance on the page.
* Be consistent within a product area.
* There should never be more than 1 Primary action. There is no upper limit to how many Secondary or Ghost actions could exist.
* Some components, such as [Dialogs](/docs/web/components/dialog/design), [Drawers](/docs/web/components/drawer/design), and [Alerts](/docs/web/components/alert/design) have Button pairing standards already, and those should be used when possible.
* Icon-only actions are frequently represented as Ghost actions.
* A Cancel action is generally a Secondary action.
### Caution when pairing
```tsx lines theme={null}
import { Button, Grid, Flex } from "@servicetitan/anvil2";
function App() {
return (
Primary
Secondary
Ghost
Primary
Secondary
Danger
);
}
export default App;
```
**Caution**
While these pairings are allowed, caution should be used with them. Three levels of Button hierarchy in one grouping is usually excessive, consider simplifying it to two levels.
#### Don't use these pairings
```tsx lines theme={null}
import { Button, Grid, Flex } from "@servicetitan/anvil2";
function App() {
return (
Primary
Primary
Danger
Danger
);
}
export default App;
```
**Don't**
These pairings should be avoided. Primary actions are meant to only occur once in a context, multiple primaries should not happen.
#### Icons in Buttons
In general, icons should only be used in Buttons when a user can easily associate the icon with the action.
##### Use caution with Icon-only Buttons
While icon-only buttons can save space on the page, the meaning behind icons is not universally understood. Often, this requires users to explore and later recall what each icon does to understand the action.
##### Always include a Tooltip with Icon-only Buttons
All Icon-only buttons should include a Tooltip that appears on hover and focus, describing what the Button would do.
#### Button Alignment
Refer to the [Button alignment guidance from the Form pattern](/docs/web/patterns/forms#button-alignment).
##### Left-aligned Buttons
In scenarios where you have several Buttons stacked vertically, use the Secondary variant and left-align the text. Each button should have the same width.
```tsx lines theme={null}
import { Button, Flex } from "@servicetitan/anvil2";
function App() {
return (
Button
A slightly longer Button
Another Button
);
}
export default App;
```
**Do**
```tsx lines theme={null}
import { Button, Flex } from "@servicetitan/anvil2";
function App() {
return (
Button
A slightly longer Button
Another Button
);
}
export default App;
```
**Don't**
## Content
Content within the Button should clearly communicate the action it will perform.
### Button name should describe what it does
A user should be able to use the button name to predict what will happen when they click it.
Buttons should be action-oriented, pairing a verb and a supporting noun, and be 2 to 3 words long. Common actions like "Save", "Close", "Cancel," or "OK" don't require a supporting noun.
```tsx lines theme={null}
import { Button, Flex } from "@servicetitan/anvil2";
function App() {
return (
Merge Accounts
Save
View Settings
);
}
export default App;
```
**Do**
```tsx lines theme={null}
import { Button, Flex } from "@servicetitan/anvil2";
function App() {
return (
Account Merge
Save to Database
Settings
);
}
export default App;
```
**Don't**
### Be concise and consistent
Avoid unnecessary words and articles such as the, an, or a. Never include punctuation in button text and avoid button text that requires punctuation. Always write button text in title case. Capitalize the first word, the last word, and all major words in between. Never use emoji or exclamation points.
```tsx lines theme={null}
import { Button, Flex } from "@servicetitan/anvil2";
function App() {
return (
Add Menu Item
Done
);
}
export default App;
```
**Do**
```tsx lines theme={null}
import { Button, Flex } from "@servicetitan/anvil2";
function App() {
return (
Add a menu item
Yes, I‘m Done!
);
}
export default App;
```
**Don't**
### Button name should match destination
When a button opens a modal or takeover, the destination's title should match the button text.
## Keyboard Interaction
Users can navigate the Button using standard keyboard controls.
| Key | Interaction |
| -------------- | ------------------------- |
| Space or Enter | Engages the button action |
Buttons using an href will also use this keyboard combination.
### Accessibility
Anvil provides most of the accessibility needs for buttons out of the box, and only needs some additional considerations for library users.
#### Provide labels for Icon Buttons
```tsx lines theme={null}
import { Button, Tooltip } from "@servicetitan/anvil2";
import Edit from "@servicetitan/anvil2/assets/icons/material/round/edit.svg";
function App() {
return (
Tooltip Content
);
}
export default App;
```
Regardless of whether or not a label is visible on the page, a label should be provided to describe the action for both screen readers and for users who focus on the element.
For more guidance on button labels and accessibility, see [button accessibility best practices](/docs/accessibility/labels-and-ctas#buttons-and-links).
# Tokens
Source: https://anvil.servicetitan.com/docs/web/components/button/tokens
Token reference for the Button component.
Button tokens control the appearance of the Button component in its primary, secondary, ghost, and danger variants.
## Tokens
## How to override
Use `ThemeProvider` to override component tokens for a subtree:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
{/* children */}
;
```
The `-hover`, `-active`, and `-disabled` variants of any token can be overridden the same way. To theme every component at once, override tier-2 semantic tokens instead — see [Design Tokens](/docs/web/design-tokens).
## Interactive states
Interactive state tokens follow a `-hover` / `-active` / `-disabled` suffix on the base token name. For example, `button.primary.background.color` has siblings `button.primary.background.color-hover`, `...color-active`, and `...color-disabled`. The table above surfaces these via the **State** column.
## Related
* [Button Design](/docs/web/components/button/design)
* [Button Code](/docs/web/components/button/code)
* [Design Tokens](/docs/web/design-tokens)
# Calendar – Code
Source: https://anvil.servicetitan.com/docs/web/components/calendar/code
Calendar allows a user to pick a specific date or date range.
## Live Component Playground
```tsx lines expandable theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [date, setDate] = useState();
return (
setDate(date.value)}
/>
{date && `Selected Date: ${date}`}
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { Calendar } from "@servicetitan/anvil2";
function ExampleComponent() {
return ;
}
```
### Date Format
The `Calendar` component accepts [ISO 8601 date strings](/docs/web/utilities/date-values) for `value` and `defaultValue`. Prefer date-only strings (for example, `"2025-07-02"`) for predictable calendar date selection.
`onSelection` returns a full ISO datetime string with timezone offset (for example, "2025-07-02T00:00:00.000-07:00"), or `undefined` when the selection is cleared.
Convert to a date-only string (for example, `const dateOnly = value?.split("T")[0];`) before passing the value to date fields or APIs that expect `YYYY-MM-DD`.
#### Example with new Date()
```tsx theme={null}
```
### Controlling calendar state
```tsx lines expandable theme={null}
import { Calendar, Flex, Button } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [date, setDate] = useState(
new Date().toISOString(),
);
return (
setDate(value)} />
setDate("2000-01-01")} size="small">
Go to millenium
);
}
export default App;
```
#### Uncontrolled calendars
By default, calendars handle their own state. Use the `defaultValue` prop to set the initial value of an uncontrolled calendar.
```tsx lines expandable theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
#### Controlled calendars
Use the `value` prop to manually control the state of a calendar.
### Selecting a date range
Calendar can also be used to select range of dates using `range` prop.
```tsx lines expandable theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Calendar focus
By default, the month of selected date or today is shown.
#### Overriding calendar focus
In the example below, `2024-07-07` is the selected date but June is showing on initial render because of `defaultFocusedDate="2024-06-06"`.
```tsx lines expandable theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Calendar Timezone
By default, local browser timezone is used. To override local system timezone, `defaultTimezone` can be used.
```tsx lines expandable theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [date, setDate] = useState();
return (
setDate(date.value)}
defaultTimeZone="America/New_York"
/>
{date && `Selected Date: ${date}`}
);
}
export default App;
```
### Scoping calendar dates
There are two different ways to limit the scope of selectable dates within a calendar.
```tsx lines expandable theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
#### Min and Max
The `minDate` and `maxDate` props can be used to scope the date range selection.
```tsx lines expandable theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
#### Unavailable dates
You can also pass an array of dates to the `unavailable.dates` prop to disable selecting specific dates.
```tsx theme={null}
```
## `Calendar` Props
Props marked as BETA are currently only available if you import from `@servicetitan/anvil2/beta`.
BETA Array of date metadata objects to display pips and provide context.
Uncontrolled focus date. Sets the initially visible month and year.
Uncontrolled date value. Object is only accepted when `range={true}`.
Use `defaultFocusedDate` instead. `focusedDate` does not reliably control the initial view and will be removed in a future major release.
The locale to use for the calendar.
BETA Callback when the visible month changes.
Callback when a date is selected.
Enable date range selection mode. When `true`, `value` and `defaultValue` accept objects with `start` and `end` properties.
The day of the week to start the calendar on.
The days of the week and specific dates that are unavailable.
Controlled date value. Object is only accepted when `range={true}`.
# Calendar – Design
Source: https://anvil.servicetitan.com/docs/web/components/calendar/design
Calendar allows users to view and select a specific date or date range.
## Anatomy
The Calendar consists of nine primary elements that work together to enable date viewing and selection.
1. **Header** - Navigation toolbar containing month and year controls
2. **Month button** - Opens month selection view
3. **Year button** - Opens year selection view
4. **Previous button** - Navigates to the previous month
5. **Next button** - Navigates to the next month
6. **Today button** - Navigates to today's date
7. **Month grid** - Table displaying days in a month
8. **Day cell** - A single cell in the month grid
9. **Selected day** - Selected date
## Options
The Calendar supports the following configurations to accommodate various date selection scenarios.
### Selection Mode
```tsx lines theme={null}
import { Calendar, Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
Single Date Selection
Date Range Selection
);
}
export default App;
```
Calendar supports single date selection and date range selection modes. Single date mode allows selecting one date, while range mode enables selecting a start and end date.
### Start Day
```tsx lines theme={null}
import { Calendar, Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
Sunday Start (Default)
Monday Start
);
}
export default App;
```
| Start day | Description |
| --------- | ---------------------------------------- |
| Sunday | Calendar week begins on Sunday (default) |
| Monday | Calendar week begins on Monday |
### Date Restrictions
```tsx lines theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
Calendar supports date restrictions through minimum and maximum date boundaries. Dates outside the specified range are disabled and cannot be selected.
### Unavailable Dates
```tsx lines theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
Mark specific dates or days of the week as unavailable when they cannot be selected. Unavailable dates are visually distinct and cannot be selected.
### Locale and Timezone
Calendar adapts to different locales for date formatting and week start days. Timezone configuration ensures dates are interpreted correctly across different regions.
## Behavior
The Calendar responds to user interaction with distinct visual states and navigation patterns.
### Selection States
Selected dates display with distinct visual styling. In range mode, the start date, end date, and dates within the range are visually indicated. Hover states provide feedback during date selection.
### Navigation
Users navigate between months using Previous and Next buttons. Month and Year buttons switch to selection views for faster navigation across longer time periods. The Today button quickly returns focus to the current date.
### View Transitions
Calendar transitions between day, month, and year selection views. The header controls adapt based on the current view, showing relevant navigation options for each context.
### Date Constraints
When date restrictions or unavailable dates are configured, disabled dates are visually distinct and cannot receive focus or selection. Navigation automatically skips unavailable dates when using keyboard controls.
## Usage Guidelines
### When to Use
Use Calendar when you need to:
* Allow users to select a single date for scheduling, filtering, or data entry
* Enable date range selection for filtering data or scheduling periods
* Display dates in a familiar calendar grid format
* Provide date selection within forms or date input fields
### When not to use
Avoid using Calendar for:
* **Simple date input** - Use [DateField](/docs/web/components/date-field-single/design) for text-based date entry when users prefer typing dates
* **Time selection** - Calendar focuses on dates only; use [TimeField](/docs/web/components/time-field/design) for time selection
* **Display-only dates** - Use text or other display components when dates are not selectable
### Alternatives
#### Calendar vs DateField
DateField provides text-based date input with calendar popover support. Use DateField when space is limited or users prefer typing dates. Use Calendar when you need a persistent calendar view or when date selection is the primary interaction.
### How to Use
#### Choose single or range selection based on user needs
Use single date selection when users need to pick one specific date, such as a deadline, appointment date, or event date. Single selection provides clear focus and reduces cognitive load for straightforward date choices.
Use date range selection when users need to define a period, such as vacation dates, report date ranges, or filtering time periods. Range selection helps users visualize the span and understand the relationship between start and end dates.
```tsx lines theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
Use single date selection for specific point-in-time selections. This pattern works well for scheduling, deadlines, and one-time events where users need clarity about a single date.
```tsx lines theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
Use date range selection when users need to define periods or spans. This pattern helps users understand duration and relationships between dates, making it ideal for filtering, reporting, and multi-day activities.
#### Set date boundaries to guide user choices
Use minimum and maximum date boundaries to prevent invalid selections and guide users toward appropriate date choices. Boundaries help users understand constraints before they attempt invalid selections, reducing errors and frustration.
```tsx lines theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
Set date boundaries when business rules or data availability create constraints. Clear boundaries help users understand what dates are valid, preventing errors and reducing support requests.
#### Mark unavailable dates to communicate constraints
Mark specific dates or recurring days as unavailable when they cannot be selected due to business rules, holidays, or system constraints. Visual distinction helps users understand why certain dates aren't selectable and prevents confusion.
```tsx lines theme={null}
import { Calendar, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
Mark unavailable dates clearly to communicate constraints to users. This helps users understand why certain dates aren't selectable and prevents frustration from attempting invalid selections.
## Content
Content within the Calendar should be clear and accessible, helping users understand date availability and navigate efficiently.
* Use appropriate locale settings to format dates according to user expectations
* Ensure date restrictions are clearly communicated through visual styling
* Provide clear visual feedback for selected dates and date ranges
* Maintain consistent date formatting throughout the calendar interface
## Keyboard Interaction
Users can navigate the Calendar using standard keyboard controls.
| Key | Description |
| ----------- | ------------------------------------------------ |
| Tab | Move focus between header controls |
| Arrow Left | Move focus to the previous day |
| Arrow Right | Move focus to the next day |
| Arrow Up | Move focus to the same day in the previous week |
| Arrow Down | Move focus to the same day in the next week |
| Home | Move focus to the first day of the current week |
| End | Move focus to the last day of the current week |
| Page Up | Move focus to the same day in the previous month |
| Page Down | Move focus to the same day in the next month |
| Enter | Select the focused date |
| Space | Select the focused date |
| Escape | Close month or year selection views |
# Tokens
Source: https://anvil.servicetitan.com/docs/web/components/calendar/tokens
Token reference for the Calendar component.
Calendar tokens control the appearance of the Calendar component in its cell and range-fill variants.
## Tokens
## How to override
Use `ThemeProvider` to override component tokens for a subtree:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
{/* children */}
;
```
The `-hover`, `-active`, and `-disabled` variants of any token can be overridden the same way. To theme every component at once, override tier-2 semantic tokens instead — see [Design Tokens](/docs/web/design-tokens).
## Interactive states
Interactive state tokens follow a `-hover` / `-active` / `-disabled` suffix on the base token name. For example, `calendar.primary.background.color` has siblings `calendar.primary.background.color-hover`, `...color-active`, and `...color-disabled`. The table above surfaces these via the **State** column.
## Related
* [Calendar Design](/docs/web/components/calendar/design)
* [Calendar Code](/docs/web/components/calendar/code)
* [Design Tokens](/docs/web/design-tokens)
# Card – Code
Source: https://anvil.servicetitan.com/docs/web/components/card/code
Cards are containers that group related content together.
```tsx lines expandable theme={null}
import { Card, Text } from "@servicetitan/anvil2";
function App() {
return (
Basic Card
This is the text within this example card. Make sure to always add{" "}
flexDirection="column" to the Card{" "}
to make the text stack vertically.
);
}
export default App;
```
## Common Examples
Cards are used to distinguish sections of content within a page, and can be interactive (clickable).
```tsx theme={null}
import { Card, Text } from "@servicetitan/anvil2";
function ExampleComponent() {
return (
Card Header
Card body text.
);
}
```
### Background Color
Use the `background` prop to control the background color of the card.
```tsx lines expandable theme={null}
import { Card, Flex } from "@servicetitan/anvil2";
function App() {
return (
Card with a
default background
Card with a
strong background
Card with a
stronger background
);
}
export default App;
```
### Padding
Anvil provides five options to pass to the `padding` prop to determine the spacing between the card border and content.
```tsx lines expandable theme={null}
import { Card, Flex } from "@servicetitan/anvil2";
function App() {
return (
This card has no padding applied to it.
This card has 4px padding applied to it.
This card has 8px padding applied to it.
This card has 16px padding applied to it.
This card has 24px padding applied to it.
);
}
export default App;
```
### Flex Direction
Use the `flexDirection` prop to determine which direction the `children` are displayed. The parent `div` of an Anvil `Card` uses `display: inline-flex`, so note that the orientation of the `children` could vary based on the user's locale and settings.
```tsx lines expandable theme={null}
import { Card } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
```tsx lines expandable theme={null}
import { Card } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Creating sections with Divider
To create sections within a `Card`, use a `padding` of `0` along with the `Divider`, then give each child element its own padding as needed.
```tsx lines expandable theme={null}
import { Card, Text, Divider } from "@servicetitan/anvil2";
function App() {
return (
Section 1. Lorem ipsum dolor sit amet.
Section 2. Lorem ipsum dolor sit amet.
);
}
export default App;
```
### AI Mark
Place the [AI Mark](/docs/web/components/ai-mark/code) inline with a card title by setting `aiMark` on [`Text`](/docs/web/components/text/code) with `variant="headline"`. Use `true` for the sparkle only, or pass tooltip or popover configuration for more detail.
When a card is wrapped in `ButtonCompound`, avoid interactive AI Mark configurations (tooltip or popover triggers) inside the card if that would nest interactive elements incorrectly. For clickable cards that need an interactive mark, use the [Interactive Card](/docs/web/components/interactive-card) pattern and validate with accessibility testing.
```tsx lines expandable theme={null}
import { Card, List, Text } from "@servicetitan/anvil2";
function App() {
return (
Daily Job Summary
You have four jobs on the board for the next two days: two drain
cleanouts (Oak St., Riverside Ave.), a water-heater diagnostic on Maple,
and a leak under a kitchen sink on Cedar.
High Priority Jobs
Drain Cleanout (Oak St.)
Leak Under a Kitchen Sink (Cedar)
);
}
export default App;
```
### Interactive Cards
If the card needs to have an action applied to it, please use the [InteractiveCard component](/docs/web/components/interactive-card) instead.
## React Accessibility
* Since the `Card` component can accept any valid HTML `div` props, all relevant `aria-*` props are supported.
* When creating an interactive card, the `ButtonCompound` component wraps the rendered card in a `button` element, and will use the content of the card to determine its label. If the card includes complex content, it may be helpful to use `aria-labelledby` or `aria-label`. Always test using a screenreader to ensure the label makes sense and doesn't repeat information.
For more guidance on creating accessible components, see [building accessible custom components best practices](/docs/accessibility/custom-components).
```tsx theme={null}
Card content
```
## `Card` Props
In addition to the props listed below, the `Card` component can accept any valid HTML `div` props and layout utility props (including `flexDirection`).
Uses the default app background color if no value is provided.
# Card – Design
Source: https://anvil.servicetitan.com/docs/web/components/card/design
Cards are containers that group related content together.
## Anatomy
The Card consists of two primary elements that work together to group related content together.
1. Container
2. Content
## Options
The Card supports flexible content, padding, and background configurations to accommodate various grouping scenarios.
### Content
```tsx lines theme={null}
import { Card, Flex, Checkbox, Text, Button } from "@servicetitan/anvil2";
function App() {
return (
This is a Card that just has text in it.
Headline text
Description of the lorem ipsum dolar emit molestiae reprehenderit
iste ipsam et velit ab. Eum est et corporis quia voluptatem.
Get started
);
}
export default App;
```
### Padding
```tsx lines theme={null}
import { Card, Flex } from "@servicetitan/anvil2";
function App() {
return (
This card has no padding applied to it.
This card has 4px padding applied to it.
This card has 8px padding applied to it.
This card has 16px padding applied to it.
This card has 24px padding applied to it.
);
}
export default App;
```
| Padding option | Value |
| ---------------- | ------------- |
| 0 | 0px / 0rem |
| xSmall | 4px / 0.25rem |
| Small | 8px / 0.5rem |
| Medium (default) | 16px / 1rem |
| Large | 24px / 1.5rem |
### AI Mark
```tsx lines theme={null}
import { Card, List, Text } from "@servicetitan/anvil2";
function App() {
return (
Daily Job Summary
You have four jobs on the board for the next two days: two drain
cleanouts (Oak St., Riverside Ave.), a water-heater diagnostic on Maple,
and a leak under a kitchen sink on Cedar.
High Priority Jobs
Drain Cleanout (Oak St.)
Leak Under a Kitchen Sink (Cedar)
);
}
export default App;
```
Use the [AI Mark](/docs/web/components/ai-mark/code) with [`Text`](/docs/web/components/text/design) `variant="headline"` when the card title should indicate AI-generated content.
### Backgrounds
```tsx lines theme={null}
import { Card, Flex } from "@servicetitan/anvil2";
function App() {
return (
Card with a
default background
Card with a
strong background
Card with a
stronger background
);
}
export default App;
```
## Behavior
The Card responds to interactivity requirements while maintaining consistent content grouping.
## Usage Guidelines
Use the Card when grouping related content and actions inside of a container.
### When to Use
Use a Card when grouping related content and actions inside of a container.
### Alternatives
#### Interactive Card vs Card
Use Interactive Card when the card itself needs an interaction. If no interaction is needed, or only an inner element needs an interaction, use Card instead.
### How to Use
#### Group related content
Group related content and actions within a single container. Don't add unrelated content to a card.
```tsx lines theme={null}
import { Card, Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
Contact methods
Phone
(813) 364-0534
Fax
(813) 364-0530
Email
email.address123@gmail.com
);
}
export default App;
```
**Do**
### Nesting cards within cards
Don't nest cards within cards. Use headings, body text, and actions to provide structure to your layout.
```tsx lines theme={null}
import { Card, Flex, Text } from "@servicetitan/anvil2";
function App() {
return (
Contact methods
Phone
(813) 364-0534
Fax
(813) 364-0530
Email
email.address123@gmail.com
Robert Smith
Founder
);
}
export default App;
```
**Don't**
#### Adding interactive elements to a Card
Inner content of a Card can add interactive elements. Do not make the Card itself interactive, use the [Interactive Card](/docs/web/components/interactive-card/design) component instead.
## Content
Content within the Card should be clearly organized and focused on a single purpose.
* Use a Heading to make a card's purpose clear. Following heading structure for the page.
* Avoid too many call-to-actions in a single card.
* Cards should have an intentional focus, avoid overloading a single card with too much or unrelated information.
* Cards of similar importance should be structured in the same way.
# Tokens
Source: https://anvil.servicetitan.com/docs/web/components/card/tokens
Token reference for the Card component.
Card tokens control the appearance of the Card component in its border, default, strong, stronger, and strongest variants.
## Tokens
## How to override
Use `ThemeProvider` to override component tokens for a subtree:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
{/* children */}
;
```
The `-hover`, `-active`, and `-disabled` variants of any token can be overridden the same way. To theme every component at once, override tier-2 semantic tokens instead — see [Design Tokens](/docs/web/design-tokens).
## Interactive states
Interactive state tokens follow a `-hover` / `-active` / `-disabled` suffix on the base token name. For example, `card.primary.background.color` has siblings `card.primary.background.color-hover`, `...color-active`, and `...color-disabled`. The table above surfaces these via the **State** column.
## Related
* [Card Design](/docs/web/components/card/design)
* [Card Code](/docs/web/components/card/code)
* [Design Tokens](/docs/web/design-tokens)
# Checkbox – Code
Source: https://anvil.servicetitan.com/docs/web/components/checkbox/code
Checkboxes are form elements that allow users to select one or more options.
```tsx lines expandable theme={null}
import { Checkbox } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { Checkbox } from "@servicetitan/anvil2";
function ExampleComponent() {
return (
);
}
```
### Basic checkbox inputs
The `Checkbox` component accepts many of the same props as an HTML `input` with `type="checkbox"`, such as `name`, `value`, `checked`, and `defaultChecked`.
```tsx lines expandable theme={null}
import { Checkbox } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Controlling checkbox state
Use the `checked` and `onClick` props to manually control the state of a checkbox.
```tsx lines expandable theme={null}
import { Checkbox, type CheckboxState } from "@servicetitan/anvil2";
import { useState } from "react";
function App() {
const [list, setList] = useState([
{
label: "Email",
checked: false,
},
{
label: "Text",
checked: true,
},
]);
const handleUpdate = (state: CheckboxState, index: number) => {
const newArr = [...list];
newArr[index].checked = !state.checked;
setList(newArr);
};
return (
{list.map((item, i) => {
return (
state && handleUpdate(state, i)}
/>
);
})}
);
}
export default App;
```
### Indeterminate checkboxes
The `indeterminate` prop should be used to represent a checkbox that has checkbox children which some, but not all are checked.
```tsx lines expandable theme={null}
import { Checkbox, Flex } from "@servicetitan/anvil2";
import { useState } from "react";
type ListItem = {
label: string;
checked: boolean;
indeterminate?: boolean;
childrenNodes?: ListItem[];
};
function App() {
const [list, setList] = useState([
{
label: "Income statement",
checked: false,
childrenNodes: [
{
label: "Income",
checked: false,
},
{
label: "Expense",
checked: false,
},
],
},
{
label: "Balance sheet",
checked: true,
indeterminate: true,
childrenNodes: [
{
label: "Asset",
checked: true,
},
{
label: "Liabilities",
checked: false,
},
],
},
]);
// Note: this only works with 2 level
const handleUpdate = (position: number, parent?: number) => {
const newArr = [...list];
if (typeof parent != "undefined") {
const childrenNodes = newArr[parent].childrenNodes;
if (
childrenNodes &&
Array.isArray(childrenNodes) &&
childrenNodes.length > 0
) {
childrenNodes[position].checked = !childrenNodes[position].checked;
newArr[parent].childrenNodes = childrenNodes;
// Check parent if all childrenNodes are checked
if (childrenNodes.every((item) => item.checked)) {
newArr[parent].indeterminate = false;
newArr[parent].checked = true;
}
// Indeterminate parent if childrenNodes are partially checked
else if (childrenNodes.some((item) => item.checked === true)) {
newArr[parent].indeterminate = true;
}
// Uncheck parent if all childrenNodes are unchecked
else if (childrenNodes.every((item) => item.checked === false)) {
newArr[parent].checked = false;
newArr[parent].indeterminate = false;
}
}
} else {
if (newArr[position].childrenNodes) {
const newState = !newArr[position].checked;
newArr[position].checked = newState;
newArr[position].indeterminate = false;
newArr[position].childrenNodes?.forEach(
(item) => (item.checked = newState),
);
} else {
newArr[position].checked = !newArr[position].checked;
}
}
setList(newArr);
};
const createNodes = (list: ListItem[], parent?: number) => {
return (
{list.map((item, i) => {
return (
handleUpdate(i, parent)}
/>
{item.childrenNodes &&
Array.isArray(item.childrenNodes) &&
item.childrenNodes.length > 0 &&
createNodes(item.childrenNodes, i)}
);
})}
);
};
return (
{createNodes(list)}
);
}
export default App;
```
## React Accessibility
* The `Checkbox` component's `label` prop allows for any `ReactElement` to be passed in, but the `label` should always be populated with some text content that is screen-reader accessible.
For more guidance on form field labels and context, see [input field context association best practices](/docs/accessibility/labels-and-ctas#input-field-context-association).
### Markdown in legends
The `Checkbox.Group` `legend` prop supports inline markdown: bold (`**text**`), italic (`*text*`), bold and italic (`***text***`), highlight (`==text==`), and code (`` `text` ``).
```tsx lines expandable theme={null}
import { Checkbox, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Hide the legend
Use `hideLabel` on `Checkbox.Group` to visually hide the legend. The `legend` string is kept as a screen-reader-only `` element so the group remains accessible — any inline markdown is stripped to plain text.
```tsx lines expandable theme={null}
import { Checkbox } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
```tsx theme={null}
{
console.log(e, state.checked, state.value);
}}
/>
```
## `Checkbox` Props
Controlled check state.
Uncontrolled check state.
Helper text description below the checkbox.
Overrides for default icons.
Sets the checkbox to an indeterminate state.
The label should have accessible string content.
Callback when the checkbox is changed.
Callback when the checkbox is clicked.
```tsx theme={null}
```
## `Checkbox.Group` Props
The legend for the checkbox group. Supports inline markdown formatting.
Passing `ReactElement` is deprecated — use a plain string with inline markdown instead.
The direction that the checkboxes are organized. This is inherited from LayoutUtilProps.
Visually hides the legend while keeping it accessible to screen readers.
`}>
Additional props to pass to the legend [FieldLabel](/docs/web/components/field-label/code) component. Supports the `aiMark` prop for displaying [AI-powered field indicators](/docs/web/utilities/ai-marks).
# Checkbox – Design
Source: https://anvil.servicetitan.com/docs/web/components/checkbox/design
Checkboxes are form elements that allow users to select one or more options.
## Anatomy
The Checkbox consists of two primary elements that work together to allow users to select one or more options.
1. Checkbox
2. Label
## Options
The Checkbox supports selection states and description configurations to accommodate various form scenarios.
### Selection
```tsx lines theme={null}
import { Checkbox } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
Checkboxes can be unselected, selected, or indeterminate. Indeterminate checkboxes represent selected and non-selected values.
### Description
```tsx lines theme={null}
import { Checkbox } from "@servicetitan/anvil2";
function App() {
return ;
}
export default App;
```
Descriptions provide information to help the user avoid errors.
## Behavior
The Checkbox responds to user interaction with distinct visual states and flexible overflow handling.
### Visual States
#### Default
```tsx lines theme={null}
import { Checkbox } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
#### Disabled
```tsx lines theme={null}
import { Checkbox } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
#### Error
```tsx lines theme={null}
import { Checkbox } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Overflow
```tsx lines theme={null}
import { Checkbox } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
When a Checkbox label is too long for the available space, it will wrap to a new line.
## Usage Guidelines
Use the Checkbox when allowing users to make zero, one, multiple, or all selections from a range of options.
### When to Use
Checkboxes give users a way to make zero, one, multiple, or all selections from a range of options.
### Alternatives
#### Checkbox vs Combobox
Checkbox groups and the Combobox both allow multi-selection of a set of items. Checkbox groups should be used when there are only a few options. For larger options, Checkbox groups can still be used when there is space available and discovery of options is important. Comboboxes are good for 8 or more options and also when conserving space.
#### Checkbox vs Switch
Switches are used for making a selection between two mutually exclusive options and have an immediate effect. Checkboxes allow users to make zero to many choices from available options and changes are not immediate.
#### Checkbox vs Radio vs Switch
Radio buttons are used to select a single option from a set of mutually exclusive options. Checkboxes are best used to make zero to many selections from available options.
| | Checkbox | Switch | Radio |
| ---------------------------------------------------------------------------- | ----------------------------------- | ------------------------- | ----------------------------------- |
| When is the selection executed? | After a user clicks a submit button | Immediately | After a user clicks a submit button |
| How many options are available? | 1 or more | 1 | 2 or more |
| Must an option be reducible to an on or off context? | No, it can be other things. | Yes | No, it can be other things. |
| If I had a group of this UI, what is the relation of each item in the group? | Independent of each other | Independent of each other | Mutually exclusive |
| Indeterminate state possible? | Yes | No | No |
### How to Use
#### Checkbox Groups
Sets of related Checkboxes should be grouped together in a Checkbox Group. In most cases, a Checkbox Group should have a label. Checkbox Groups allow users to see all available options that are related to each other.
## Content
Content within the Checkbox should clearly communicate the option being selected and provide necessary context.
### Give a descriptive, action-oriented label
The user relies on the label to provide context and identify what the checkbox controls. Use positive and active wording for checkbox labels. Include a verb to prompt the user to take action.
Use sentence case (capitalize only the first word and proper nouns) so control labels are easy to scan. Do not use periods for short phrases or single sentences.
Checkbox labels should follow the content guidelines for labels.
```tsx lines theme={null}
import { Checkbox, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
**Do**
```tsx lines theme={null}
import { Checkbox, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
**Don't**
### Give the user the information they need to make an informed decision
When the checkbox label doesn't provide enough information and context for users to make an informed decision, use inline help. Inline help works best because it requires no user interaction and is always visible. Keep inline help to 1-2 sentences.
Use the content guidelines for inline help.
Avoid using a tooltip to communicate this kind of information about an option. It's an additional step for users to mouse over the tooltip to reveal its information, and users can't refer to the tooltip content and take action at the same time.
```tsx lines theme={null}
import { Checkbox } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
**Do**
## Keyboard Interaction
Users can navigate the Checkbox using standard keyboard controls.
| Key | Interaction |
| ----- | ----------------------------- |
| Space | Enables the selected Checkbox |
| Tab | Navigate between Checkboxes |
### Accessibility
* Checkboxes should have a visible label.
* Checkbox groups must have a legend that describes the group.
* If a Checkbox has an error, Helper Text must be present to describe the nature of the error.
For more guidance on form field labels and context, see [input field context association best practices](/docs/accessibility/labels-and-ctas#input-field-context-association).
# Tokens
Source: https://anvil.servicetitan.com/docs/web/components/checkbox/tokens
Token reference for the Checkbox component.
Checkbox tokens control the appearance of the Checkbox component in its default, error, and border variants.
## Tokens
## How to override
Use `ThemeProvider` to override component tokens for a subtree:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
{/* children */}
;
```
The `-hover`, `-active`, and `-disabled` variants of any token can be overridden the same way. To theme every component at once, override tier-2 semantic tokens instead — see [Design Tokens](/docs/web/design-tokens).
## Interactive states
Interactive state tokens follow a `-hover` / `-active` / `-disabled` suffix on the base token name. For example, `checkbox.primary.background.color` has siblings `checkbox.primary.background.color-hover`, `...color-active`, and `...color-disabled`. The table above surfaces these via the **State** column.
## Related
* [Checkbox Design](/docs/web/components/checkbox/design)
* [Checkbox Code](/docs/web/components/checkbox/code)
* [Design Tokens](/docs/web/design-tokens)
# Chip – Code
Source: https://anvil.servicetitan.com/docs/web/components/chip/code
Chips visually label and organize statuses, metadata, and objects.
```tsx lines expandable theme={null}
import { Chip } from "@servicetitan/anvil2";
function App() {
return (
alert("chip click")}
onClose={() => alert("chip close")}
/>
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { Chip } from "@servicetitan/anvil2";
function ExampleComponent() {
return (
);
}
```
### Basic Chip
`Chip` components must at least have their `label` prop defined, which will define the text that is displayed within the `Chip`.
```tsx lines expandable theme={null}
import { Chip } from "@servicetitan/anvil2";
function App() {
return ;
}
export default App;
```
### Closeable Chip
Adding an `onClose` prop handler to the `Chip` component will render a clickable close button alongside the `Chip`.
```tsx lines expandable theme={null}
import { Chip } from "@servicetitan/anvil2";
function App() {
return alert("chip close")} />;
}
export default App;
```
### Clickable Chip
Adding an `onClick` prop handler to the `Chip` component will render the text inside the `Chip` as a clickable button.
```tsx lines expandable theme={null}
import { Chip } from "@servicetitan/anvil2";
function App() {
return alert("chip click")} />;
}
export default App;
```
### Chip with Avatar
Adding an `avatar` prop with a URL to an image will render an `Avatar` inside of the `Chip` as a prefix before the label. Note that the `avatar` prop cannot be used with the `icon` prop.
```tsx lines expandable theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
import dog01 from "../assets/dog-01.png";
import dog02 from "../assets/dog-02.png";
function App() {
return (
alert("chip close")}
color="#3e3e5f"
avatar={dog01}
textWrap
/>
alert("chip close")}
color="#3e3e5f"
avatar={dog02}
textWrap
/>
);
}
export default App;
```
### Chip with Icon
Adding an `icon` prop with an imported SVG will render an `Icon` inside of the `Chip` as a prefix before the label. Note that the `icon` prop cannot be used with the `avatar` prop.
```tsx lines expandable theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
import Info from "@servicetitan/anvil2/assets/icons/material/round/info.svg";
import Warning from "@servicetitan/anvil2/assets/icons/material/round/warning.svg";
function App() {
return (
alert("chip close")}
color="#3e3e5f"
icon={Info}
textWrap
/>
alert("chip close")}
color="#3e3e5f"
icon={Warning}
textWrap
/>
);
}
export default App;
```
### Chip with AI Mark
Adding the `aiMark` prop displays the `AiMark` icon inside the chip to indicate AI-generated content. On default chips (no color), a gradient AI mark is shown. On colored chips, the AI mark inherits the chip's text color.
```tsx lines expandable theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
#### AI Mark with Color
When a `color` prop is set alongside `aiMark`, the AI mark uses the default type, inheriting the chip's text color for visual cohesion.
```tsx lines expandable theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
#### AI Mark with Interactive Chips
When `aiMark` is used with `onClick`, the AI mark will animate when the chip is hovered or focused.
```tsx lines expandable theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
function App() {
return (
alert("chip click")}
/>
alert("chip close")}
/>
alert("chip click")}
onClose={() => alert("chip close")}
/>
);
}
export default App;
```
## React Accessibility
* The `Chip` component won't be focusable unless it has an `onClick` or `onClose` prop handler defined.
For more guidance on accessible interaction components, see [button accessibility best practices](/docs/accessibility/labels-and-ctas#buttons-and-links).
```tsx theme={null}
console.log("Clicked")}
onClose={(e) => console.log("Closed")}
/>
```
## `Chip` Props
Displays an AI mark icon in the chip to indicate AI-generated content. Uses gradient style on default chips and inherited color style on colored chips.
**Note:** Only works when `icon` is not set
Several color formats work, such as HEX, RGB, HSL, HSV, etc.
**Note:** Only works when `avatar` is not set
Setting this will make the chip clickable and focusable.
When `size="small"`, only one of `onClick` or `onClose` can be used, not both.
Setting this will render the chip's close button.
When `size="small"`, only one of `onClick` or `onClose` can be used, not both.
Wraps text when it overflows.
# Chip – Design
Source: https://anvil.servicetitan.com/docs/web/components/chip/design
Chips visually label and organize statuses, metadata, and objects.
## Anatomy
The Chip consists of three primary elements that work together to visually label and organize information.
1. Content
2. Close action
3. Custom background
## Options
The Chip supports clickable, closeable, and customizable configurations to accommodate various labeling scenarios.
#### Clickable
```tsx lines theme={null}
import { Chip } from "@servicetitan/anvil2";
function App() {
return alert("chip click")} />;
}
export default App;
```
By default, a Chip is a non-clickable label. Use a clickable Chip for navigational contexts.
#### Close
```tsx lines theme={null}
import { Chip } from "@servicetitan/anvil2";
function App() {
return alert("chip close")} />;
}
export default App;
```
Chips can also have a close action, allowing a user to manually remove the Chip. Clickable and close can also be used together in a single Chip, but only when the size is set to medium.
#### Custom background colors
```tsx lines theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
import { core } from "@servicetitan/anvil2/token";
function App() {
return (
);
}
export default App;
```
Chips provide one color by default. They can accept any color or Token value. Text color is auto-calculated to be the highest available contrast. It is suggested to use a Token value that supports theming.
#### AI Mark
Chips can display an AI mark icon to indicate AI-generated content.
```tsx lines theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
By default, the AI mark uses a gradient treatment. When a custom background color is applied, the mark adapts to match the chip's text color for visual consistency.
```tsx lines theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
When a Chip is clickable, the AI mark animates on hover to draw attention to the AI-generated content.
### Sizes
```tsx lines theme={null}
import { Chip, Grid } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
| Size | Height | Font size |
| ------ | ------ | --------- |
| Medium | 26px | 14px |
| Small | 18px | 12px |
Refer to the [choosing a chip size](#choosing-a-chip-size) section for more sizing recommendations.
## Behavior
The Chip responds to user interaction with distinct visual states and flexible overflow handling.
### Visual States
With custom colors, the hover background is lighter when the text is white, and darker when the text is black.
#### Without close
```tsx lines theme={null}
import { Chip, Grid } from "@servicetitan/anvil2";
function App() {
return (
alert("chip click")} />
alert("chip click")}
data-interactive="hover"
/>
alert("chip click")}
data-interactive="focus-visible"
/>
);
}
export default App;
```
#### With close
```tsx lines theme={null}
import { Chip, Grid } from "@servicetitan/anvil2";
function App() {
return (
alert("chip close")}
onClick={() => alert("chip click")}
/>
alert("chip close")}
onClick={() => alert("chip click")}
data-interactive="close-hover"
/>
alert("chip close")}
onClick={() => alert("chip click")}
data-interactive="focus-visible"
/>
);
}
export default App;
```
### Overflow handling
#### Truncation
```tsx lines theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
function App() {
return (
alert("chip close")}
/>
);
}
export default App;
```
By default, Chips will truncate, rather than wrap.
#### Wrapping
```tsx lines theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
function App() {
return (
alert("chip close")}
/>
);
}
export default App;
```
Chips can be configured to individually wrap when there is not enough space.
#### Suggested group overflow
```tsx lines theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
The Chip on its own does not provide an opinion on grouping. Wrap groups first, prioritizing group wrapping before wrapping individual Chips.
## Usage Guidelines
Use the Chip when visually labeling and organizing statuses, metadata, and objects.
### Alternatives
#### Chip vs Badge
Chips are used to categorize, label, and add context to items whereas Badges are used to indicate something requires attention, such as a notification. Additionally, Chips can be interactive while Badges are not.
### How to Use
#### Default
The default Chip visually highlights elements and provides user interaction.
```tsx lines theme={null}
import { Chip } from "@servicetitan/anvil2";
function App() {
return (
alert("chip click")}
onClose={() => alert("chip close")}
/>
);
}
export default App;
```
### Chip group
Chips are frequently paired together to form a Chip group.
```tsx lines theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
function App() {
return (
);
}
export default App;
```
### Color
When implementing Chips, it is important to carefully consider what color is being used so that meaning and relationships are not implied when there is none.
For example, green is often meant to indicate "success" and red is used to indicate "danger" or "failure". Consistent usage is essential for helping users efficiently understand the status of something.
```tsx lines theme={null}
import { Chip, Flex } from "@servicetitan/anvil2";
import { core } from "@servicetitan/anvil2/token";
function App() {
return (
);
}
export default App;
```
**Caution**
### Choosing a Chip size
Choose a Chip size based on its relative importance within the UI. Some general suggestions for Chips are provided below.
| | Medium Chip | Small Chip |
| ---------------------------------------------------------- | ----------- | ---------- |
| Part of a [Tab](/docs/web/components/tab/design) | ❌ | ✅ |
| Part of a [Side Nav](/docs/web/components/side-nav/design) | ❌ | ✅ |
| Part of a [Combobox](/docs/web/components/combobox/design) | ✅ | ❌ |
| Table Cell main content | ✅ | ⚠️ |
| Table Cell secondary content | ⚠️ | ✅ |
| Group of Chips on the page | ✅ | ⚠️ |
| Chip(s) in the page header | ✅ | ⚠️ |
| Needing an interactive Chip + close | ✅ | ❌ |
#### Legend
✅ recommended
⚠️ use with caution
❌ do not use
## Content
Content within the Chip should be short and descriptive, helping users understand context at a glance.
* Chips should be short and descriptive. Users should understand context at a glance.
* Use both color and text together to provide meaning so that color alone is not the only indicator of meaning.
* Use Chips in moderation as they add visual noise to the page.
* Chips may be used for both textual and numerical content.
## Keyboard Interaction
Users can navigate the Chip using standard keyboard controls.
#### With clickable Chip
| Key | Description |
| ------------------- | ------------------------------------------------------------- |
| Enter | Engages the Chip and moves focus to the click target. |
| Backspace or Delete | If the close action exists, removes the Chip from the screen. |
### Accessibility
The Chip provides all accessibility needs out of the box. With custom background colors, the Chip will automatically provide a text color that will be at least a 4.5:1 ratio, satisfying WCAG AA requirements.
For more guidance on accessible interaction components, see [button accessibility best practices](/docs/accessibility/labels-and-ctas#buttons-and-links).
# Tokens
Source: https://anvil.servicetitan.com/docs/web/components/chip/tokens
Token reference for the Chip component.
Chip tokens control the appearance of the Chip component in its background, border, foreground, focus-ring, and close variants.
## Tokens
## How to override
Use `ThemeProvider` to override component tokens for a subtree:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
{/* children */}
;
```
The `-hover`, `-active`, and `-disabled` variants of any token can be overridden the same way. To theme every component at once, override tier-2 semantic tokens instead — see [Design Tokens](/docs/web/design-tokens).
## Interactive states
Interactive state tokens follow a `-hover` / `-active` / `-disabled` suffix on the base token name. For example, `chip.primary.background.color` has siblings `chip.primary.background.color-hover`, `...color-active`, and `...color-disabled`. The table above surfaces these via the **State** column.
## Related
* [Chip Design](/docs/web/components/chip/design)
* [Chip Code](/docs/web/components/chip/code)
* [Design Tokens](/docs/web/design-tokens)
# Combobox – Code
Source: https://anvil.servicetitan.com/docs/web/components/combobox/code
Comboboxes are form elements that allow users to filter and select a value from a list.
Combobox is deprecated. Use [SelectField](/docs/web/components/select-field/code) for single selection or [MultiSelectField](/docs/web/components/multi-select-field/code) for multiple selection. Combobox will be removed in Anvil2 5.0.
```tsx lines expandable theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
{ id: 6, name: "Sixth item" },
{ id: 7, name: "Seventh item" },
{ id: 8, name: "Eighth item" },
{ id: 9, name: "Ninth item" },
{ id: 10, name: "Tenth item" },
];
function App() {
const [selectedItems, setSelectedItems] = useState- ([]);
return (
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
selectedItems={selectedItems ?? []}
onChange={setSelectedItems}
filterOptions={{ keys: ["name"] }}
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
## Common Examples
```tsx theme={null}
import { useState } from "react";
import { Combobox } from "@servicetitan/anvil2";
const items = [
{ id: 1, name: "First item" },
// ...more items...
];
const ExampleComponent = () => {
const [selectedItem, setSelectedItem] = useState();
return (
item ? item.name : ""}
itemToKey={(item) => item ? item.id : null}
selectedItem={selectedItem}
onChange={setSelectedItem}
filterOptions={{ keys: ["name"] }}
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
```
### Combobox components
The Anvil2 combobox is made up of a `Combobox` component and a few sub-components that can be used together to build simple or complex select drop-downs.
* `Combobox`: the parent component to configure comboboxes.
* `Combobox.SearchField`: used to create a searchable select input to use with a combobox list in a drop-down.
* `Combobox.Content`: popover that passes an `items` array to its `children` to construct combobox items.
* `Combobox.List`: organizes combobox items in the drop-down list.
* `Combobox.Item`: used to construct the items within the combobox drop-down.
* `Combobox.Empty`: optionally used to customize empty results message.
### Multi-selects
To allow the user to select multiple items, set the `multiple` prop of the `Combobox` to `true`. In addition, props such as `selectedItem` will be replaced with `selectedItems` (note the "s" at the end) and will expect an array.
```tsx lines expandable theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
{ id: 6, name: "Sixth item" },
{ id: 7, name: "Seventh item" },
{ id: 8, name: "Eighth item" },
{ id: 9, name: "Ninth item" },
{ id: 10, name: "Tenth item" },
];
function App() {
const [selectedItems, setSelectedItems] = useState- ([]);
return (
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
selectedItems={selectedItems ?? []}
onChange={setSelectedItems}
filterOptions={{ keys: ["name"] }}
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
### Select all for multi-selects
To add a "Select All" option at the top of a multi-select combobox dropdown, use the `selectAll` prop. This prop accepts an object with `label`, `onSelection`, and `isChecked` properties. The `onSelection` callback is called when the user clicks the Select All option, and `isChecked` controls whether the checkbox appears checked.
Note: The "Select All" option does not automatically select all items when the combobox is opened. It is up to the developer to handle the selection of all items when the "Select All" option is clicked. This means it is incompatible with using `Combobox` in an uncontrolled way.
```tsx lines expandable theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Blueberry" },
{ id: 4, name: "Grapefruit" },
{ id: 5, name: "Orange" },
{ id: 6, name: "Peach" },
{ id: 7, name: "Pomegranate" },
{ id: 8, name: "Raspberry" },
{ id: 9, name: "Strawberry" },
{ id: 10, name: "Watermelon" },
];
function App() {
const [selectedItems, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
selectAll={{
label: "Select All",
onSelection: () => {
setSelected(items);
},
isChecked: selectedItems?.length === items.length,
}}
selectedItems={selectedItems ?? undefined}
multiple
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
```tsx theme={null}
import { useState } from "react";
import { Combobox } from "@servicetitan/anvil2";
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
];
const ExampleComponent = () => {
const [selectedItems, setSelectedItems] = useState([]);
return (
item?.name ?? ""}
itemToKey={(item) => item?.id ?? null}
selectedItems={selectedItems}
onChange={setSelectedItems}
filterOptions={{ keys: ["name"] }}
selectAll={{
// Customize the label of the "Select All" option
label: "Select All",
// Callback function to handle the selection of all items
onSelection: () => {
// This is up to the developer to implement based on their use case
setSelectedItems((prev) =>
prev.length === items.length ? [] : items
);
},
// Control whether the "Select All" option appears checked
isChecked: selectedItems.length === items.length,
}}
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
};
```
### Building selects
The Anvil2 `Combobox` component is versatile due to its flexibility and robust set of sub-components. It can be used in place of the `Select` component from the original Anvil library to create a searchable select component.
```tsx lines expandable theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Casey Maxwell" },
{ id: 2, name: "Giovanni Saunders" },
{ id: 3, name: "Harper Gonzalez" },
{ id: 4, name: "Jane Doe" },
{ id: 5, name: "Meadow Hunter" },
];
function App() {
const [, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
#### Notes for building selects
* The `itemToString` prop passes an `item` object and expects a `string` return type. This is used to format the text of each combobox item within the search field input (after selecting an item) based on the provided `item` object type.
* The `itemToKey` prop passes an `item` object and expects something that can be used to identify the item, such as an `id`. This is used for identity comparisons, so if this prop is omitted, the `item`s will be compared by reference equality instead.
* The `filterOptions` prop is used to filter the items using the [match-sorter](https://github.com/kentcdodds/match-sorter?tab=readme-ov-file) library. See [their docs](https://github.com/kentcdodds/match-sorter?tab=readme-ov-file#advanced-options) for more info on the options that can be provided.
* The `selectedItem`/`selectedItems` props allow the `Combobox` to be used as a controlled component.
* For easily managing the selected item state, the `onChange` prop accepts a `setState` function, which is the second item in the array that a `useState` function returns:
```tsx theme={null}
const ExampleComponent = () => {
const [selected, setSelected] = useState();
return
}
```
#### Compare using `itemToKey` or reference equality
The `itemToKey` prop is used to compare `items` using a unique identifier. By default, the `Combobox` component does searches and comparisons using reference equality. That means that the `Combobox` may not behave how you expect if your code is generating new references for your `items` on every render:
```tsx theme={null}
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
];
const ExampleComponent = () => {
const [selected, setSelected] = useState();
// WARNING! This will create a new reference for every item on every render!
const newItems = items.map(item => { ...item, someChange: Math.random() });
return (
{...}
)
}
```
Some ways have stable references might be to perform the reference modifications outside of React's render cycle or to use `React.useMemo` to memoize computations on `items` and only update the references when something has meaningfully changed.
However, in cases like this, it's probably more likely you'd prefer to have your `items` compared by a specific key in your object rather than by reference equality. And so you can do that by passing in an `itemToKey` function that takes an item and returns a unique key:
```tsx theme={null}
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
];
const ExampleComponent = () => {
const [selected, setSelected] = useState();
const newItems = items.map(item => { ...item, someChange: Math.random() });
return (
item?.id}
>
{...}
)
}
```
#### Passing filtered items through the combobox components
The `items` array passed to the `Combobox` is automatically filtered based on the user typing in the search input and the `keys` in the `filterOption` prop. After filtering, it is passed through the `Combobox.Content` as a parameter used to generate the `children`.
```tsx theme={null}
// objects in items array can have any type
const items = [
{ id: 1, label: "First item", alias: "one" },
{ id: 2, label: "Second item", alias: "two" },
{ id: 3, label: "Third item", alias: "three" },
];
const ExampleComponent = () => {
const [selectedItems, setSelectedItems] = useState([]);
return (
item?.label ?? ""}
itemToKey={(item) => item?.id ?? null}
selectedItems={selectedItems}
onChange={setSelectedItems}
/* strings in keys array should match the
searchable fields in items objects */
filterOptions={{ keys: ["label", "alias"] }}
>
{/* matches items array, with filtering */}
{({ items }) => (
{items.map((item, index) => (
{item.label}
))}
)}
);
};
```
### Adding a new item when there is no exact match
To allow the user to add a new item when their search returns no exact matches, use the `Combobox.ItemAddNew` component. This component provides an `onSelection` callback, and that callback will need to be used to update both your list of items and your selected item.
Your `onSelection` callback will need to both update the list of items and update the `selectedItem`/`selectedItems`, so we recommend that you use a controlled `Combobox` when you need the ability to add a new item:
```tsx lines expandable theme={null}
import { Combobox, Text } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
function App() {
const [items, setItems] = useState- ([
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
{ id: 6, name: "Sixth item" },
{ id: 7, name: "Seventh item" },
{ id: 8, name: "Eighth item" },
{ id: 9, name: "Ninth item" },
{ id: 10, name: "Tenth item" },
]);
const [selectedItems, setSelectedItems] = useState
- ([]);
const handleAddNew = (inputValue: string) => {
const newItem = { id: items.length + 1, name: inputValue };
setItems((prevItems) => [...prevItems, newItem]);
setSelectedItems((prevItems) =>
prevItems != null ? [...prevItems, newItem] : [newItem],
);
};
return (
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
selectedItems={selectedItems ?? []}
onChange={setSelectedItems}
defaultIsOpen
defaultInputValue="New item"
>
{({ items, inputValue }) => (
{items.map((item, i) => (
{item.name}
))}
No match found
+ Add “{inputValue}”
)}
);
}
export default App;
```
### Select-only comboboxes
By default, comboboxes include a text input for searching and filtering items. To create a select-only combobox that acts similarly to an HTML select, use `ComboboxSelect` and `Combobox.SelectTrigger` instead of `Combobox` and `Combobox.SearchField`. `Combobox.Select` remains available as a deprecated alias for `ComboboxSelect`.
```tsx lines expandable theme={null}
import { Combobox, ComboboxSelect } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
{ id: 6, name: "Sixth item" },
{ id: 7, name: "Seventh item" },
{ id: 8, name: "Eighth item" },
{ id: 9, name: "Ninth item" },
{ id: 10, name: "Tenth item" },
];
function App() {
const [selectedItem, setSelectedItem] = useState- ();
return (
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
selectedItem={selectedItem}
onChange={setSelectedItem}
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
#### Disable clearing selection on select-only combobox
To remove the "x" button that clears a selection in a select-only combobox, use the `disableClearSelection` prop.
**Note**: this does not work when `multiple` is `true`, due to the nature of clearing options in multi-select comboboxes.
```tsx lines expandable theme={null}
import { Combobox, ComboboxSelect } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
{ id: 6, name: "Sixth item" },
{ id: 7, name: "Seventh item" },
{ id: 8, name: "Eighth item" },
{ id: 9, name: "Ninth item" },
{ id: 10, name: "Tenth item" },
];
function App() {
const [selectedItem, setSelectedItem] = useState- (items[1]);
return (
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
selectedItem={selectedItem}
onChange={setSelectedItem}
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
### Sorting combobox options
By default, Comboboxes sort options based on the order that they are provided via the `items` prop.
```tsx lines expandable theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
{ id: 6, name: "Sixth item" },
{ id: 7, name: "Seventh item" },
{ id: 8, name: "Eighth item" },
{ id: 9, name: "Ninth item" },
{ id: 10, name: "Tenth item" },
];
const reversedItems = items.reverse();
function App() {
const [selectedItems, setSelectedItems] = useState- ([]);
return (
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
selectedItems={selectedItems ?? []}
onChange={setSelectedItems}
filterOptions={{ keys: ["name"] }}
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
### Controlled combobox
Comboboxes can be used uncontrolled by default, or can be used in a fully-controlled way by setting the `selectedItem` prop (or by using the `selectedItems` prop when used with `multiple` select):
```tsx lines expandable theme={null}
import { Combobox, Flex, Button } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
{ id: 6, name: "Sixth item" },
{ id: 7, name: "Seventh item" },
{ id: 8, name: "Eighth item" },
{ id: 9, name: "Ninth item" },
{ id: 10, name: "Tenth item" },
];
function App() {
const [selectedItem, setSelectedItem] = useState- ();
return (
setSelectedItem(items[0])}>
Set First Item
setSelectedItem(items[1])}>
Set Second Item
setSelectedItem(items[2])}>
Set Third Item
setSelectedItem(items[3])}>
Set Fourth Item
setSelectedItem(items[4])}>
Set Fifth Item
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
selectedItem={selectedItem}
onChange={setSelectedItem}
filterOptions={{ keys: ["name"] }}
style={{ width: "50vw", maxWidth: "50%" }}
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
```tsx lines expandable theme={null}
import { Combobox, Flex, Button } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
{ id: 6, name: "Sixth item" },
{ id: 7, name: "Seventh item" },
{ id: 8, name: "Eighth item" },
{ id: 9, name: "Ninth item" },
{ id: 10, name: "Tenth item" },
];
function App() {
const [selectedItems, setSelectedItems] = useState- ([]);
return (
setSelectedItems([items[0]])}>
Set First Item
setSelectedItems([items[1], items[2]])}>
Set Second and Third Items
setSelectedItems([items[7], items[8], items[9]])}
>
Set Last Three Items
setSelectedItems([])}>Set Empty Array
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
selectedItems={selectedItems ?? []}
onChange={setSelectedItems}
filterOptions={{ keys: ["name"] }}
style={{ width: "50vw", maxWidth: "50%" }}
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
### Default combobox selection
When using a Combobox in an uncontrolled way, Comboboxes can set an item as a default by using the `defaultSelectedItem` prop (or by using the `defaultSelectedItems` prop when used with `multiple` select):
```tsx lines expandable theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Casey Maxwell" },
{ id: 2, name: "Giovanni Saunders" },
{ id: 3, name: "Harper Gonzalez" },
{ id: 4, name: "Jane Doe" },
{ id: 5, name: "Meadow Hunter" },
];
const defaultItem = items[0];
function App() {
const [, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
defaultSelectedItem={defaultItem}
style={{ minWidth: "260px" }}
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
```tsx lines expandable theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Casey Maxwell" },
{ id: 2, name: "Giovanni Saunders" },
{ id: 3, name: "Harper Gonzalez" },
{ id: 4, name: "Jane Doe" },
{ id: 5, name: "Meadow Hunter" },
];
const defaultItems = [items[1], items[4]];
function App() {
const [, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
multiple
defaultSelectedItems={defaultItems}
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
### Selected item customization
`Combobox`es with `multiple` select can customize the selected state of an item using the `selectedItemProps` prop on the `Combobox.SearchField`. This prop accepts a function that will pass the selected item as an parameter to allow for things like customizing the display color:
```tsx lines expandable theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
color: string;
};
const items = [
{ id: 1, name: "Red", color: "red" },
{ id: 2, name: "Purple", color: "purple" },
{ id: 3, name: "Black", color: "black" },
{ id: 4, name: "Gray", color: "gray" },
{ id: 5, name: "Gold", color: "gold" },
{ id: 6, name: "Cyan", color: "cyan" },
{ id: 7, name: "Pink", color: "pink" },
{ id: 8, name: "Blue", color: "blue" },
{ id: 9, name: "Green", color: "green" },
];
function App() {
const [, setSelectedItems] = useState- ([
items[4],
items[5],
items[6],
]);
return (
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
defaultSelectedItems={[items[4], items[5], items[6]]}
onChange={setSelectedItems}
filterOptions={{ keys: ["name"] }}
defaultIsOpen
>
({
color: item.color,
})}
/>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
### Disabling combobox popovers
Comboboxes can be used to create searchable select lists without a drop-down. If `disablePopover` is set to `true` on the `Combobox.Content`, the list will always render. This is especially useful if the combobox exists within a custom [popover](/docs/web/components/popover/design).
```tsx lines expandable theme={null}
import { Combobox, Popover } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: string;
name: string;
};
const technicians = [
{ id: "cs-0", name: "Cory Sparks" },
{ id: "jb-1", name: "Jessica Brees" },
{ id: "jc-2", name: "Johnny Carpenter" },
{ id: "rp-3", name: "Rusty Pipes" },
{ id: "sp-4", name: "Silvia Plomería" },
];
function App() {
const [selectedItems, setSelectedItems] = useState- ([]);
return (
Add technician(s)
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
selectedItems={selectedItems ?? []}
onChange={setSelectedItems}
filterOptions={{ keys: ["name"] }}
style={{ width: "15rem" }}
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
### Custom combobox empty state
By default, if no items exist in the filtered combobox results, a simple "No results found" message is displayed. To customize this, use the `Combobox.Empty` component.
```tsx lines expandable theme={null}
import { Combobox, Flex, Text } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
{ id: 6, name: "Sixth item" },
{ id: 7, name: "Seventh item" },
{ id: 8, name: "Eighth item" },
{ id: 9, name: "Ninth item" },
{ id: 10, name: "Tenth item" },
];
function App() {
const [selectedItem, setSelectedItem] = useState- ();
return (
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
selectedItem={selectedItem}
onChange={setSelectedItem}
filterOptions={{ keys: ["name"] }}
defaultIsOpen
defaultInputValue="XYZ"
>
{({ items }) => (
<>
😕 No matches
Please try again.
{items.map((item, i) => (
{item.name}
))}
>
)}
);
}
export default App;
```
### Combobox virtualization example
`Combobox.Content` renders a function that passes down the list of filtered items as children, allowing the `Combobox.List` component to be used with virtualization libraries, which often require an additional wrapper component around the items being virtualized. The following example uses `react-window`'s `FixedSizeList` component for virtualizing the list of items:
```tsx theme={null}
import { useState } from "react";
import { Combobox } from "@servicetitan/anvil2";
import { FixedSizeList as List } from "react-window";
const items = [
{ id: 1, name: "First item" },
// ...more items...
];
const ExampleComponent = () => {
const [selectedItem, setSelectedItem] = useState();
return (
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
onChange={setSelectedItem}
filterOptions={{ keys: ["name"] }}
>
{({ items }) => (
{({ index, style }) => (
{items[index].name}
)}
)}
);
};
```
## Testing
Interactive sub-elements carry a stable `data-anv-part` attribute so end-to-end tests can target them directly. The library owns the part names; you supply the instance identifier.
Attach your own `data-*` attribute (such as `data-cy` or `data-testid`) to `Combobox` / `Combobox.Select`. It is forwarded to the root wrapper, letting you scope queries to a single instance:
```tsx theme={null}
…
```
```ts theme={null}
// Target the input within this specific combobox
cy.get('[data-cy="fruit-picker"] [data-anv-part="input"]').type("Ban");
```
| `data-anv-part` | Element |
| --------------- | --------------------------------------------------- |
| `input` | Search field input (`Combobox.SearchField`) |
| `trigger` | Select trigger surface (`Combobox.SelectTrigger`) |
| `clear-button` | Button that clears the selection |
| `toggle-button` | Button that opens and closes the menu (search mode) |
| `chip` | Removable chip for a selected value (multi-select) |
| `menu` | Option list container |
| `option` | An individual option row |
| `popover` | Menu container in popover display mode |
Options also carry `data-id` from `itemToKey(item)` so you can pick one among many: `[data-anv-part="option"][data-id="1"]`.
The menu renders in the browser's top layer (`menu`, `option`, and `popover` parts), outside the Combobox wrapper. Descendant queries scoped to your wrapper attribute will not match these parts. Target them by their part attribute directly. Only one menu is open at a time.
```tsx theme={null}
item?.name ?? ""}
itemToKey={(item) => item?.id ?? null}
multiple={false}
onChange={setSelectedItem}
filterOptions={{ keys: ["name"] }}
/>
```
## `Combobox` and `ComboboxSelect` Props
In addition to the props listed below, the `Combobox` and `ComboboxSelect` components can accept any valid HTML `div` props.
* The default `Combobox` also accepts the props listed in the [downshift UseComboboxProps type](https://github.com/downshift-js/downshift/blob/v8.5.0/typings/index.d.ts#L537).
* The `ComboboxSelect` component (also available as the deprecated `Combobox.Select` alias) extends the [downshift UseSelectProps type](https://github.com/downshift-js/downshift/blob/v8.5.0/typings/index.d.ts#L340). Note: Props related to the input are not included for ComboboxSelect, since there is no search field. (`defaultInputValue, inputId`, etc.)
The default selected item for single-select combobox.
The default selected item(s) for multi-select combobox.
Disables the combobox, preventing user interaction.
Prevent input from clearing when item is selected. **Note:** Only works with
`Combobox` with `multiple={true}`
Remove the clear button to disable user clearing the input after selecting.
**Note:** Only works with `ComboboxSelect` when `multiple` is not `true`
Prevent the drop-down popover from closing when the user clicks outside of it.
Prevent the drop-down from closing when item is selected. **Note:** Only works
with `Combobox` with `multiple={true}`
See the [match-sorter MatchSorterOptions
type](https://github.com/kentcdodds/match-sorter/blob/v6.3.4/src/index.ts#L50)
**Note:** Not available on `ComboboxSelect`
Name of the property on each item to group by. When provided, items are organized into labeled sections in the dropdown based on the value of this property.
Function to compare two group values for sorting. Must return a negative number if `a` should appear before `b`, and a positive number if `b` should appear before `a`. `ComboboxGroupByValue` is `string | number`.
Function to convert group values to display labels. When not provided, the raw group key value is used as the label. `ComboboxGroupByValue` is `string | number`.
Callback when selection changes. For single-select, receives the selected item or null. For multi-select, receives an array of selected items.
Makes the combobox read-only, allowing focus but preventing changes.
Configuration for "Select All" option in multi-select comboboxes. When provided, adds a "Select All"
option at the top of the dropdown list. Only available when `multiple={true}`.
```tsx theme={null}
```
## `Combobox.SearchField` and `Combobox.SelectTrigger` Props
Control opening the `moreInfo` tooltip.
Prefix content displayed before the input.
This function passes the selected item as a parameter and returns an object to
merge into the selected item `Chip` props.
```tsx theme={null}
{({ items }) => (
{items.map((item, index) => (
{item.name}
))}
)}
```
## `Combobox.Content` Props
```tsx theme={null}
{items.map((item, index) => (
{item.name}
))}
```
## `Combobox.List` Props
```tsx theme={null}
Item content
```
## `Combobox.Item` Props
```tsx theme={null}
console.log(inputValue)}>
Add new item
```
## `Combobox.ItemAddNew` Props
```tsx theme={null}
No results found
```
## `Combobox.Empty` Props
# Combobox – Design
Source: https://anvil.servicetitan.com/docs/web/components/combobox/design
Comboboxes are form elements that allow users to filter and select a value from a list.
Combobox is deprecated. Use [SelectField](/docs/web/components/select-field/code) for single selection or [MultiSelectField](/docs/web/components/multi-select-field/code) for multiple selection. Combobox will be removed in Anvil2 5.0.
## Anatomy
The Combobox consists of four primary elements that work together to allow users to filter and select values from a list.
1. Trigger
2. Content
3. Clear button (Optional)
4. Popover
## Options
The Combobox supports single and multi-select configurations to accommodate various selection scenarios.
```tsx lines theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Casey Maxwell" },
{ id: 2, name: "Giovanni Saunders" },
{ id: 3, name: "Harper Gonzalez" },
{ id: 4, name: "Jane Doe" },
{ id: 5, name: "Meadow Hunter" },
];
function App() {
const [, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
### Multi Select
```tsx lines theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Casey Maxwell" },
{ id: 2, name: "Giovanni Saunders" },
{ id: 3, name: "Harper Gonzalez" },
{ id: 4, name: "Jane Doe" },
{ id: 5, name: "Meadow Hunter" },
];
function App() {
const [, setSelected] = useState- ([]);
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
multiple
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
## Behavior
The Combobox responds to user interaction with flexible selection methods and typeahead functionality.
### Single selecting
```tsx lines theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Casey Maxwell" },
{ id: 2, name: "Giovanni Saunders" },
{ id: 3, name: "Harper Gonzalez" },
{ id: 4, name: "Jane Doe" },
{ id: 5, name: "Meadow Hunter" },
];
const defaultItem = items[0];
function App() {
const [, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
defaultSelectedItem={defaultItem}
style={{ minWidth: "260px" }}
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
Clicking an option updates the text field value and closes the Combobox.
When reopening the Combobox with a selected value, the option matching that value is selected.
### Multi Selecting
```tsx lines theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Casey Maxwell" },
{ id: 2, name: "Giovanni Saunders" },
{ id: 3, name: "Harper Gonzalez" },
{ id: 4, name: "Jane Doe" },
{ id: 5, name: "Meadow Hunter" },
];
const defaultItems = [items[1], items[4]];
function App() {
const [, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
multiple
defaultSelectedItems={defaultItems}
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
#### Continuous selection
Continuous selection refers to a multiselect that stays open while users keep making selections.
Refer to the [usage guide](#continuous-multiselect) for when to use this variant.
```tsx lines theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
];
function App() {
const [, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
multiple
disableCloseOnSelectItem
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
#### Select All
Multi-select Comboboxes support selecting all options at once.
```tsx lines theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Blueberry" },
{ id: 4, name: "Grapefruit" },
{ id: 5, name: "Orange" },
{ id: 6, name: "Peach" },
{ id: 7, name: "Pomegranate" },
{ id: 8, name: "Raspberry" },
{ id: 9, name: "Strawberry" },
{ id: 10, name: "Watermelon" },
];
function App() {
const [selectedItems, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
selectAll={{
label: "Select All",
onSelection: () => {
setSelected(items);
},
isChecked: selectedItems?.length === items.length,
}}
selectedItems={selectedItems ?? undefined}
multiple
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
### As Select-only
Configure the Combobox for scenarios requiring an HTML select-like experience.
```tsx lines theme={null}
import { Combobox, ComboboxSelect } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
{ id: 6, name: "Sixth item" },
{ id: 7, name: "Seventh item" },
{ id: 8, name: "Eighth item" },
{ id: 9, name: "Ninth item" },
{ id: 10, name: "Tenth item" },
];
function App() {
const [selectedItem, setSelectedItem] = useState- ();
return (
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
selectedItem={selectedItem}
onChange={setSelectedItem}
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
### Typeahead
Combobox supports typeahead, narrowing options in the popup as users type.
```tsx lines theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Grapefruit" },
{ id: 4, name: "Orange" },
{ id: 5, name: "Pomegranate" },
];
function App() {
const [, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
### Rows
Comboboxes grow as the number of selections increases. Configure the maximum number of rows.
```tsx lines theme={null}
import { Combobox, Flex } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Casey Maxwell" },
{ id: 2, name: "Giovanni Saunders" },
{ id: 3, name: "Harper Gonzalez" },
{ id: 4, name: "Jane Doe" },
{ id: 5, name: "Meadow Hunter" },
];
const defaultItems = [items[1], items[4]];
function App() {
const [, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
multiple
defaultSelectedItems={defaultItems}
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
multiple
defaultSelectedItems={[items[1], items[2], items[3], items[4]]}
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
#### Truncated Chip
When there are more Chips than rows allowed in the Combobox, a truncated Chip will appear.
```tsx lines theme={null}
import { Combobox, Flex } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Blueberry" },
{ id: 4, name: "Grapefruit" },
{ id: 5, name: "Orange" },
{ id: 6, name: "Peach" },
{ id: 7, name: "Pomegranate" },
{ id: 8, name: "Raspberry" },
{ id: 9, name: "Strawberry" },
{ id: 10, name: "Watermelon" },
];
const defaultItems = [
items[0],
items[1],
items[2],
items[3],
items[4],
items[5],
items[6],
items[7],
items[8],
items[9],
];
function App() {
const [, setSelected] = useState- ();
return (
item?.name ?? ""}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
multiple
defaultSelectedItems={defaultItems}
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
## Usage Guidelines
Use the Combobox when users need to select from many options.
### When not to use
Consider using an alternative component to the Combobox when:
* There are only a few items to choose from. A [radio](/docs/web/components/radio/design) or [checkbox](/docs/web/components/checkbox/design) group may be more appropriate.
* When a particularly complex selection interaction is needed, in which case a [Listbox](/docs/web/components/listbox/design) in a special layout may be preferred.
* In navigational contexts.
### Selection methods
The main selection method for Combobox supports searching for a particular entity to make the selection. This pattern appears in applications like Slack when adding a new member to a channel.
| Scenario | Use a... |
| ----------------------------------------------------------------------------------- | ------------------------------- |
| Has 10+ options | Regular Combobox |
| Users are not familiar with the options | Regular Combobox |
| Has 10 or fewer options | Select-only Combobox |
| Common user behavior is to search/filter for metadata instead of the entity's label | Continuous multiselect Combobox |
| Need for filtering, trees, secondary actions, action confirmation | Select Trigger + Complex Dialog |
#### Select-only
Use a Combobox as select-only in these scenarios:
* **Simple, static list**: Short lists of options (e.g., countries, states, or categories) that users easily recognize and navigate without much effort.
* **No need for search or filtering functionality**: Lists short enough that users can easily scan through all available choices.
* **Simple UX:** Select-only experiences require less complex interactions.
```tsx lines theme={null}
import { Combobox, ComboboxSelect } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
{ id: 6, name: "Sixth item" },
{ id: 7, name: "Seventh item" },
{ id: 8, name: "Eighth item" },
{ id: 9, name: "Ninth item" },
{ id: 10, name: "Tenth item" },
];
function App() {
const [selectedItem, setSelectedItem] = useState- ();
return (
(item ? item.name : "")}
itemToKey={(item) => (item ? item.id : null)}
selectedItem={selectedItem}
onChange={setSelectedItem}
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
#### Continuous multiselect
Combobox supports continuous multi-selection instead of closing after each selection. Use this pattern when users typically search for metadata to find multiple items.
* An example would be using "designer" as a search term to filter down to all designers and selecting multiple designers from that list.
```tsx lines theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "First item" },
{ id: 2, name: "Second item" },
{ id: 3, name: "Third item" },
{ id: 4, name: "Fourth item" },
{ id: 5, name: "Fifth item" },
];
function App() {
const [, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
multiple
disableCloseOnSelectItem
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
#### Select All
The Select All feature enables users to quickly select or deselect all available options within a multi-select Combobox. This pattern improves efficiency when working with large sets of items or when selecting most of the list is common.
```tsx lines theme={null}
import { Combobox } from "@servicetitan/anvil2";
import { useState } from "react";
type Item = {
id: number;
name: string;
};
const items: Item[] = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Blueberry" },
{ id: 4, name: "Grapefruit" },
{ id: 5, name: "Orange" },
{ id: 6, name: "Peach" },
{ id: 7, name: "Pomegranate" },
{ id: 8, name: "Raspberry" },
{ id: 9, name: "Strawberry" },
{ id: 10, name: "Watermelon" },
];
function App() {
const [selectedItems, setSelected] = useState- ();
return (
(item ? item.name : "")}
onChange={setSelected}
filterOptions={{ keys: ["name"] }}
selectAll={{
label: "Select All",
onSelection: () => {
setSelected(items);
},
isChecked: selectedItems?.length === items.length,
}}
selectedItems={selectedItems ?? undefined}
multiple
defaultIsOpen
>
{({ items }) => (
{items.map((item, i) => (
{item.name}
))}
)}
);
}
export default App;
```
#### Complex selection alternatives
When the Combobox is insufficient for a use case, use an alternative UI. These primarily combine the [Select Trigger](/docs/web/components/select-trigger) with the [Dialog](/docs/web/components/dialog/design), [Text Field](/docs/web/components/text-field/design), [Listbox](/docs/web/components/listbox/design), and [List View](/docs/web/components/list-view/design) components, and may include others depending on need.
Rather than adding additional features to the Combobox, use the [Select Trigger](/docs/web/components/select-trigger) for scenarios such as:
* Manual applying actions
* Needing secondary actions
* Tabbed division of options
* Multiple footer actions
### Alternatives
#### Combobox vs Checkbox
Checkbox groups and the Combobox both allow multi-selection of a set of items. Checkbox groups should be used when there are only a few options. For larger options, Checkbox groups can still be used when there is space available and discovery of options is important. Comboboxes are good for 8 or more options and also when conserving space.
#### Combobox vs Menu
The Menu is used to perform actions on the page, and sometimes also a navigational context. The Combobox is used for choosing an option amongst a list, usually in the context of a Form.
#### Combobox vs Popover
When users need to select from many options, use the Combobox instead of the Popover. Avoid recreating a combobox-like or select-like interaction with a Popover.
#### Combobox vs Radio
Radio buttons and the Combobox both allow users to select a single option among a set. Use Radio buttons when there are between 2-7 options. Use a Combobox when there are 8 or more options available.
Additionally, in a group of Radio buttons all options are equally represented whereas a Combobox highlights a single option more prominently.
#### Combobox vs Search Field
Use the Combobox when selecting from options defined in an overlayed list, typically in the context of a form field. Use Search Field in situations requiring a traditional search experience, for filtering lists of items on the page, in tables, with large datasets, and for results requiring additional filtering.
#### Combobox vs Select Field
[Select Field](/docs/web/components/select-field/design) is the eventual replacement for the Combobox component. Use Select Field when looking for features such as Dialog viewing, standardized content display, secondary actions, sectioning, or lazy loading.
Consider using the Combobox if needing a stable component in your application.
### How to Use
#### Labels and help
For information around concepts like labels, placeholder, and help, refer to the [Form pattern](/docs/web/patterns/forms).
#### Sorting options
The Combobox does not prescribe how choices are sorted. Common sorting types include alphabetical, time-based, and categorical. Use a sort that matches user expectations.
Examples of combobox sorts other than alphabetical:
## Content
Content within the Combobox should clearly communicate available options and selection states.
For content guidance, refer to the content guidance provided in the [Form pattern](/docs/web/patterns/forms).
## Keyboard Interaction
Users can navigate the Combobox using standard keyboard controls.
### Text field
| Key | Interaction |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Down Arrow | If the text field is not empty and the listbox is displayed, moves visual focus to the first suggested value. If the text field is empty and the listbox is not displayed, opens the listbox and moves visual focus to the first option. In both cases DOM focus remains on the text field. |
| Alt(Option) + Down Arrow | Opens the listbox without moving focus or changing selection. |
| Up Arrow | If the text field is not empty and the listbox is displayed, moves visual focus to the last suggested value. If the text field is empty, first opens the listbox if it is not already displayed and then moves visual focus to the last option. In both cases DOM focus remains on the text field. |
| Enter | Closes the listbox if it is displayed. |
| Escape | If the listbox is displayed, closes the listbox. If the listbox is not displayed, clears the text field. |
### Popup
NOTE: Even when the visual focus is on the listbox, DOM focus remains on the text field.
| Key | Interaction |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Enter | Sets the text field value to the content of the focused option. Closes the popup. Sets visual focus on the text field. |
| Escape | Closes the popup. Sets visual focus on the text field. |
| Down Arrow | Moves visual focus to the next option. If visual focus is on the last option, moves visual focus to the first option. Note: This wrapping behavior is useful when Home and End move the editing cursor as described below. |
| Up Arrow | Moves visual focus to the previous option. If visual focus is on the first option, moves visual focus to the last option. Note: This wrapping behavior is useful when Home and End move the editing cursor as described below. |
| Right Arrow | Moves visual focus to the text field and moves the editing cursor one character to the right. |
| Left Arrow | Moves visual focus to the text field and moves the editing cursor one character to the left. |
| Home | Moves visual focus to the text field and places the editing cursor at the beginning of the field. |
| End | Moves visual focus to the text field and places the editing cursor at the end of the field. |
### Accessibility
Anvil provides most of the accessibility needs for Combobox out of the box, and only needs some additional considerations for library users.
For more guidance on building accessible form controls, see [custom component best practices](/docs/accessibility/custom-components).
# Tokens
Source: https://anvil.servicetitan.com/docs/web/components/combobox/tokens
Token reference for the Combobox component.
Combobox tokens control the appearance of the Combobox component in its item variant.
## Tokens
## How to override
Use `ThemeProvider` to override component tokens for a subtree:
```tsx theme={null}
import { ThemeProvider } from "@servicetitan/anvil2";
{/* children */}
;
```
The `-hover`, `-active`, and `-disabled` variants of any token can be overridden the same way. To theme every component at once, override tier-2 semantic tokens instead — see [Design Tokens](/docs/web/design-tokens).
## Interactive states
Interactive state tokens follow a `-hover` / `-active` / `-disabled` suffix on the base token name. For example, `combobox.primary.background.color` has siblings `combobox.primary.background.color-hover`, `...color-active`, and `...color-disabled`. The table above surfaces these via the **State** column.
## Related
* [Combobox Design](/docs/web/components/combobox/design)
* [Combobox Code](/docs/web/components/combobox/code)
* [Design Tokens](/docs/web/design-tokens)
# Boolean Edit Mode
Source: https://anvil.servicetitan.com/docs/web/components/data-table/beta-changes/boolean-edit-mode
Guide for adopting the new boolean edit mode for editable DataTable cells.
This guide covers changes to the **beta** DataTable component. These APIs may continue to evolve before the stable release.
## Overview
This guide continues the work started in [Column Types and EditConfig](/docs/web/components/data-table/beta-changes/column-types), which introduced the `editConfig` property. The boolean edit mode is a new addition that provides a purpose-built editing experience for boolean columns.
The DataTable now supports a dedicated **boolean** edit mode for inline editing of boolean cell values:
* **`editConfig.mode: "boolean"`** - Renders a dropdown menu with predefined true/false options
* **Optional null support** - Include a null option for nullable boolean columns
* **Configurable labels** - Customize the display text for `true`, `false`, and `null` values
Previously, editable text cells silently coerced boolean values to strings. The new boolean edit mode preserves the boolean type and provides a constrained selection experience.
## Design Rationale
### Why a dedicated boolean mode?
The text edit mode previously accepted boolean values by converting them to strings:
```tsx theme={null}
// Before: Boolean values silently coerced to strings
createColumn("active", {
headerLabel: "Active",
editConfig: {
mode: "text",
onChange: (value, rowId) => {
// value is a string — requires manual parsing
updateActive(value === "true", rowId);
},
},
});
```
This approach had several problems:
* Values lost their boolean type, requiring manual parsing in `onChange`
* Users could type arbitrary text, not just "true" or "false"
* No constrained selection UX for a binary choice
The new boolean mode addresses all of these:
```tsx theme={null}
// After: Purpose-built boolean editing
createColumn("active", {
headerLabel: "Active",
type: "boolean",
editConfig: {
mode: "boolean",
trueLabel: "Yes",
falseLabel: "No",
onChange: (value, rowId) => {
// value is boolean | null — no parsing needed
updateActive(value, rowId);
},
},
});
```
### Relationship between `type` and `editConfig`
The `type` property controls how a value is **displayed** when the cell is not being edited. The `editConfig` controls the **editing** behavior. These are independent concerns:
```tsx theme={null}
// Display as boolean text, edit with a dropdown
createColumn("active", {
headerLabel: "Active",
type: "boolean", // Formats display as "True" / "False"
editConfig: {
mode: "boolean", // Edits with a dropdown
trueLabel: "Enabled",
falseLabel: "Disabled",
onChange: handleChange,
},
});
```
### Label inheritance from `type.options`
When `type` is configured with `BooleanFormatterOptions` (i.e. `{ type: "boolean", options: { trueLabel, falseLabel } }`), the boolean edit mode will automatically use those labels as defaults for the edit dropdown. This avoids duplicating labels across `type.options` and `editConfig`:
```tsx theme={null}
// "Yes" / "No" labels are used for both display AND the edit dropdown
createColumn("active", {
headerLabel: "Active",
type: { type: "boolean", options: { trueLabel: "Yes", falseLabel: "No" } },
editConfig: {
mode: "boolean",
onChange: handleChange,
},
});
```
The full priority order for label resolution is:
1. `editConfig.trueLabel` / `editConfig.falseLabel` (explicit override)
2. `type.options.trueLabel` / `type.options.falseLabel` (inherited from formatter config)
3. `"True"` / `"False"` (hardcoded defaults)
This means you can still override individual labels in `editConfig` when you need different text for the edit dropdown than for the display formatter.
## Boolean Edit Options Reference
Boolean-specific options are set directly on the `editConfig` object alongside `mode` and `onChange`. All options are optional.
| Option | Type | Default | Description |
| ------------ | --------- | --------- | -------------------------------------------------------------------- |
| `trueLabel` | `string` | `"True"` | The label displayed for the `true` option |
| `falseLabel` | `string` | `"False"` | The label displayed for the `false` option |
| `allowNull` | `boolean` | `false` | Whether to include a null option in the dropdown |
| `nullLabel` | `string` | `"Unset"` | The label displayed for the `null` option (when `allowNull` is true) |
### Basic boolean editing
```tsx theme={null}
createColumn("active", {
headerLabel: "Active",
type: "boolean",
editConfig: {
mode: "boolean",
onChange: (value, rowId) => updateActive(value, rowId),
},
});
```
### Custom labels
```tsx theme={null}
createColumn("active", {
headerLabel: "Active",
type: "boolean",
editConfig: {
mode: "boolean",
trueLabel: "Enabled",
falseLabel: "Disabled",
onChange: (value, rowId) => updateActive(value, rowId),
},
});
```
### Nullable boolean with custom labels
```tsx theme={null}
createColumn("approved", {
headerLabel: "Approved",
type: "boolean",
editConfig: {
mode: "boolean",
trueLabel: "Approved",
falseLabel: "Rejected",
allowNull: true,
nullLabel: "Pending",
onChange: (value, rowId) => updateApproval(value, rowId),
},
});
```
## Migration Guide
### Boolean columns using text edit mode
If you were using `editMode: "text"` or `editConfig.mode: "text"` for boolean data, switch to boolean mode:
```tsx theme={null}
// Before: Text mode with manual boolean handling
createColumn("active", {
headerLabel: "Active",
editMode: "text",
onChange: (value, rowId) => {
updateActive(value === "true", rowId);
},
});
// After: Boolean mode with native boolean handling
createColumn("active", {
headerLabel: "Active",
type: "boolean",
editConfig: {
mode: "boolean",
onChange: (value, rowId) => updateActive(value ?? false, rowId),
},
});
```
### Boolean columns using editConfig text mode
```tsx theme={null}
// Before: editConfig text mode for booleans
createColumn("active", {
headerLabel: "Active",
editConfig: {
mode: "text",
onChange: (value, rowId) => updateActive(value === "true", rowId),
},
});
// After: editConfig boolean mode
createColumn("active", {
headerLabel: "Active",
type: "boolean",
editConfig: {
mode: "boolean",
onChange: (value, rowId) => updateActive(value ?? false, rowId),
},
});
```
### Boolean columns using select edit mode
If you were using `mode: "select"` to simulate boolean editing with manual options:
```tsx theme={null}
// Before: Select mode with manual boolean options
createColumn("active", {
headerLabel: "Active",
editConfig: {
mode: "select",
options: [
{ value: "true", label: "Yes" },
{ value: "false", label: "No" },
],
onChange: (value, rowId) => updateActive(value === "true", rowId),
},
});
// After: Boolean mode with configurable labels
createColumn("active", {
headerLabel: "Active",
type: "boolean",
editConfig: {
mode: "boolean",
trueLabel: "Yes",
falseLabel: "No",
onChange: (value, rowId) => updateActive(value ?? false, rowId),
},
});
```
## Breaking Changes
The text edit mode no longer coerces boolean values to strings. Columns with boolean data using text edit mode will render as read-only and log a warning.
The following change is **breaking**:
* **Text edit mode no longer accepts `boolean` values** - Previously, a cell with `mode: "text"` and a boolean value would silently convert the boolean to a string. Now, the cell falls back to a read-only state and logs a console warning directing you to use `mode: "boolean"` instead.
### Why breaking?
Since DataTable is a beta component, we opted to remove the implicit coercion rather than maintain a deprecation path. The coercion masked type mismatches and produced a suboptimal editing experience for boolean data. The new boolean edit mode is the correct replacement.
## New Exports
The following types are now exported from `@servicetitan/anvil2`:
* `BooleanEditConfig` - Edit configuration type for `mode: "boolean"` columns
# Column Types and EditConfig
Source: https://anvil.servicetitan.com/docs/web/components/data-table/beta-changes/column-types
Guide for adopting the new type and editConfig properties in DataTable column definitions.
This guide covers changes to the **beta** DataTable component. These APIs may continue to evolve before the stable release.
## Overview
The DataTable column helper now supports two new properties that improve developer experience:
* **`type`** - Automatically configures display formatting and alignment based on data type
* **`editConfig`** - Consolidates cell editing configuration into a single, explicit opt-in property
These changes reflect our design philosophy of **providing sensible defaults while maintaining full customization**. Rather than requiring explicit configuration for common patterns, the new API infers reasonable defaults that you can override when needed.
## Design Rationale
### Why `type`?
Previously, configuring a currency column required setting multiple independent properties:
```tsx theme={null}
// Before: Manual configuration for each concern
createColumn("amount", {
headerLabel: "Amount",
align: "end",
renderCell: (value) => currencyFormatter(value),
});
```
These properties are conceptually related - a currency column should right-align and use currency formatting. The new `type` property captures this relationship:
```tsx theme={null}
// After: Single property sets sensible defaults
createColumn("amount", {
headerLabel: "Amount",
type: "currency", // Sets align: "end" and currencyFormatter automatically
});
```
### Why `editConfig`?
The previous API scattered edit-related properties across the column definition:
```tsx theme={null}
// Before: Edit properties spread across the config
createColumn("status", {
headerLabel: "Status",
editMode: "select",
options: [...],
onChange: handleChange,
});
```
This made it unclear which properties were related and whether a column was editable. The new `editConfig` property makes the intent explicit:
```tsx theme={null}
// After: All edit concerns grouped together
createColumn("status", {
headerLabel: "Status",
editConfig: {
mode: "select",
options: [...],
onChange: handleChange,
},
});
```
Importantly, **editing is now opt-in**. Without `editConfig`, columns are read-only regardless of `type`.
### Extensibility
This structure supports new functionality without returning to scattered top-level edit props:
* **New column types** (e.g., `link`, `avatar`, `progress`) can be added with appropriate defaults
* **Shipped edit modes** already share one configuration surface: `text`, `number`, `boolean`, `select`, `multiselect`, and `custom`
* **Mode-specific options** (like `minValue`, `maxValue`, and `step` for number mode, or `renderEditor` for custom mode) live directly inside `editConfig`
## Column Types Reference
Each type sets default values for `align` and `renderCell`:
| Type | Default Align | Default Formatter |
| ---------- | ------------- | ------------------- |
| `text` | `start` | None (raw value) |
| `number` | `end` | `numberFormatter` |
| `currency` | `end` | `currencyFormatter` |
| `percent` | `end` | `percentFormatter` |
| `date` | `end` | `dateFormatter` |
| `dateTime` | `end` | `dateTimeFormatter` |
| `time` | `end` | `timeFormatter` |
| `boolean` | `start` | `booleanFormatter` |
### Using Type with Custom Options
For simple cases, pass the type as a string:
```tsx theme={null}
createColumn("amount", {
headerLabel: "Amount",
type: "currency",
});
```
To customize formatter options, pass an object:
```tsx theme={null}
createColumn("amount", {
headerLabel: "Amount",
type: {
type: "currency",
options: { currency: "EUR", locale: "de-DE" }
},
});
```
### Overriding Type Defaults
Explicit properties always override type defaults:
```tsx theme={null}
createColumn("amount", {
headerLabel: "Amount",
type: "currency",
align: "start", // Overrides the default "end"
renderCell: (value) => `$${value.toFixed(0)}`, // Overrides currencyFormatter
});
```
## EditConfig Reference
The `editConfig` property accepts an object with `mode`, `onChange`, and mode-specific options. Today that includes `text`, `number`, `boolean`, `select`, `multiselect`, and `custom`.
### Text Mode
```tsx theme={null}
createColumn("name", {
headerLabel: "Name",
editConfig: {
mode: "text",
onChange: (value, rowId) => updateName(value, rowId),
},
});
```
### Select Mode
```tsx theme={null}
createColumn("status", {
headerLabel: "Status",
editConfig: {
mode: "select",
options: [
{ id: "pending", label: "Pending" },
{ id: "active", label: "Active" },
{ id: "completed", label: "Completed" },
],
onChange: (option, rowId) => {
if (option) updateStatus(String(option.id), rowId);
},
},
});
```
### Multiselect Mode
```tsx theme={null}
createColumn("tags", {
headerLabel: "Tags",
editConfig: {
mode: "multiselect",
options: [
{ id: "urgent", label: "Urgent" },
{ id: "reviewed", label: "Reviewed" },
],
onChange: (options, rowId) => {
updateTags(options.map((option) => String(option.id)), rowId);
},
},
});
```
### Custom Mode
Custom mode is for object-backed cells whose read view and edit view are intentionally different. It adds `renderEditor` for the custom editor surface, while `renderCell` and `getReadRenderResult` continue to define the read path. `renderEditor` now receives a typed `controller` for draft mutation, validation, focus management, and explicit close requests.
```tsx theme={null}
createColumn("address", {
headerLabel: "Address",
renderCell: (value) => formatAddress(value),
getReadRenderResult: (value) => ({
content: formatAddress(value),
rawString: formatAddressForSort(value),
}),
editConfig: {
mode: "custom",
onChange: (value, rowId) => saveAddress(value, rowId),
renderEditor: ({ controller }) => ,
},
});
```
See [Custom Edit Mode](/docs/web/components/data-table/beta-changes/custom-edit-mode) for the full custom-mode behavior, controller contract, close policy, and surface options.
## Migration Guide
### Display-Only Columns
No changes required. Optionally add `type` for automatic formatting:
```tsx theme={null}
// Before
createColumn("amount", {
headerLabel: "Amount",
renderCell: (value) => currencyFormatter(value),
});
// After (optional improvement)
createColumn("amount", {
headerLabel: "Amount",
type: "currency",
});
```
### Editable Columns
Move `editMode`, `onChange`, and `options` into `editConfig`. Custom editing is also configured only through `editConfig`; there is no legacy top-level custom API:
```tsx theme={null}
// Before
createColumn("name", {
headerLabel: "Name",
editMode: "text",
onChange: handleChange,
});
// After
createColumn("name", {
headerLabel: "Name",
editConfig: {
mode: "text",
onChange: handleChange,
},
});
```
For select/multiselect columns:
```tsx theme={null}
// Before
createColumn("status", {
headerLabel: "Status",
editMode: "select",
options: statusOptions,
onChange: handleChange,
});
// After
createColumn("status", {
headerLabel: "Status",
editConfig: {
mode: "select",
options: statusOptions,
onChange: handleChange,
},
});
```
## Deprecation Timeline
The following props show deprecation warnings in development:
* `editMode` - Use `editConfig.mode` instead
* `onChange` (top-level) - Use `editConfig.onChange` instead
* `options` (top-level) - Use `editConfig.options` instead
**Existing code continues to work** but will log console warnings to guide migration. TypeScript also marks these props with `@deprecated` JSDoc comments.
## New Exports
The following types are now exported from `@servicetitan/anvil2`:
* `ColumnType` - Union of column type literals
* `ColumnTypeConfig` - Type configuration (string or object with options)
* `EditConfig`, `TextEditConfig`, `SelectEditConfig`, `MultiselectEditConfig` - Edit configuration types
* `getColumnTypeDefaults()`, `resolveColumnTypeConfig()` - Utility functions for working with column types
# Custom Edit Mode
Source: https://anvil.servicetitan.com/docs/web/components/data-table/beta-changes/custom-edit-mode
Guide for adopting the new custom edit mode for object-backed editable DataTable cells.
This guide covers changes to the **beta** DataTable component. These APIs may continue to evolve before the stable release.
## Overview
This guide continues the work started in [Column Types and EditConfig](/docs/web/components/data-table/beta-changes/column-types), which introduced the `editConfig` property. Custom edit mode is a peer to the existing text, number, boolean, select, and multiselect edit experiences, but it is designed for cells whose committed value is an object rather than a scalar.
The DataTable now supports a dedicated **custom edit mode** for object-backed cells:
* **`editConfig.mode: "custom"`** — Opens a multi-field editor surface for the cell
* **`renderEditor`** — Renders your custom editor UI with row context and a typed `controller`
* **`onCommit`** — Receives the final committed object value when the editor closes normally
* **`onDraftUpdate`** — Optional advanced callback for consumers that need every local draft mutation
* **`validateDraft` / `blockOnValidationError` / `onRequestClose` / `surface`** — Optional hooks for validation, close policy, and surface presentation
Custom mode keeps the canonical cell value intact. You do not have to flatten an address, form state, or other object into a string just to make the cell editable.
## Read mode vs edit mode
Custom mode intentionally separates the read and edit paths:
* **`renderCell`** remains the read-mode renderer for the cell.
* **`getCellText`** is the preferred optional plain-text extractor for sorting when your read view is JSX or multiline.
* **`getReadRenderResult`** is the advanced optional read hook when you want one hook to return both rendered read content and a sortable read string together.
* **`renderEditor`** is the edit-mode renderer for the anchored editor surface.
This matters for object-backed values because the display version of a cell is often formatted text or JSX, while the editable version is a small form.
```tsx theme={null}
createColumn("address", {
headerLabel: "Address",
renderCell: (value) => renderAddress(value),
getCellText: (value) => formatAddress(value),
editConfig: {
mode: "custom",
onCommit: (value, rowId) => saveAddress(value, rowId),
renderEditor: ({ controller }) => {
return ;
},
},
});
```
Use `getCellText` by default when you already have a separate `renderCell` and just need a plain sortable string. Reach for `getReadRenderResult` only when you need one hook to produce both the rendered read content and the raw sortable string together.
## Editor context and controller API
`renderEditor` receives a context object with:
* `row` — the full row data
* `rowId` — the row identifier as a string
* `value` — the committed canonical value for the cell
* `controller` — typed draft, validation, focus, and lifecycle control for the editor
The controller surface includes:
* `draftValue` / `initialValue`
* `isDirty` / `changedFields`
* `validation`
* `setDraftValue`
* `setDraftField`
* `handleSubmit`
* `submit`
* `discard`
* `requestClose(reason)`
* `setInitialFocus(focus)`
When you want Enter-to-submit behavior inside a custom editor, render the editor contents inside a `