> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dnd Sort - Web

> Dnd Sort is a collection of components built for the purpose of solving bucketing and sorting, two very common applications of the drag-and-drop interaction.

> [!NOTE]
>
> For development, import `DndSort` from
> `@servicetitan/anvil2`. For design, find it in the
> [Anvil2 file](https://www.figma.com/design/xaNQsvRKczoxu7pj0M6QT8/Anvil2-v3?node-id=2625-10093).

## Anatomy

![Anatomy of Drag and Drop sort](https://mintcdn.com/servicetitan/gaitSLKK1iHwa_zv/images/docs/web/components/dnd-sort/anatomy.png?fit=max\&auto=format\&n=gaitSLKK1iHwa_zv\&q=85\&s=8bb9623fe3151d0c2c8bd695e32c777d)

1. Drag Handle
2. Drag Handle Button
3. Draggable Card
4. Sort Line
5. Drag Ghost
6. Drag Preview
7. Drop Zone

## Options

### Draggable Card

Besides the handle, the Draggle Card's content is highly customizable. Draggable Card can use many of the features of the base Card component.

dnd-sort-card

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-card?file=src/App.tsx)

```tsx lines theme={null}
import { useState } from "react";
import { DndSort } from "@servicetitan/anvil2";

type Items = {
  [key: string | number]: { name: string; type: string };
};

type Order = (string | number)[];

const teams: Items = {
  A: { name: "Card Content", type: "item" },
};

function App() {
  const [order, _setOrder] = useState<Order>(["A"]);

  return (
    <DndSort>
      {order.map((itemId) => (
        <DndSort.Card
          key={itemId}
          label={teams[itemId].name}
          id={itemId}
          alignItems="center"
        >
          {teams[itemId].name}
        </DndSort.Card>
      ))}
    </DndSort>
  );
}

export default App;
```

### Draggable Target

Where a user can grab a Card is configurable to the whole card or only on the drag handle.

#### Whole Card

dnd-sort-whole-card

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-whole-card?file=src/App.tsx)

```tsx lines theme={null}
import { useState } from "react";
import { DndSort, type DndSortChangeEvent } from "@servicetitan/anvil2";

type Items = {
  [key: string | number]: { name: string; type: string };
};

const teams: Items = {
  A: { name: "Whole Card Target #1", type: "item" },
  B: { name: "Whole Card Target #2", type: "item" },
};

function App() {
  const [_mostRecentEvent, setMostRecentEvent] = useState<DndSortChangeEvent>();
  const [order, setOrder] = useState<(string | number)[]>(["A", "B"]);

  const handleDrop = (event: DndSortChangeEvent) => {
    setMostRecentEvent(event);
    setOrder((previousOrder) => event.zoneSort || previousOrder);
  };

  return (
    <DndSort onDrop={handleDrop}>
      <DndSort.Zone
        sortable
        id="zone-1"
        orientation="vertical"
        sortedIds={order}
        label="Zone 1"
        defaultDropPosition="end"
      >
        {order.map((itemId) => (
          <DndSort.Card
            key={itemId}
            label={teams[itemId].name}
            id={itemId}
            alignItems="center"
          >
            {teams[itemId].name}
          </DndSort.Card>
        ))}
      </DndSort.Zone>
    </DndSort>
  );
}

export default App;
```

Use the whole Card as a draggable region when:

* Users do NOT need to select the text of the Card
* D\&D is a major feature of the content
* Outside of an overflow menu, other inner Card actions are not present

#### Drag handle only

dnd-sort-handle-only

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-handle-only?file=src/App.tsx)

```tsx lines theme={null}
import { useState } from "react";
import { DndSort, type DndSortChangeEvent } from "@servicetitan/anvil2";

type Items = {
  [key: string | number]: { name: string; type: string };
};

type Order = (string | number)[];

const teams: Items = {
  A: { name: "Drag Handle Only Target #1", type: "item" },
  B: { name: "Drag Handle Only Target #2", type: "item" },
};

function App() {
  const [_mostRecentEvent, setMostRecentEvent] = useState<DndSortChangeEvent>();
  const [order, setOrder] = useState<Order>(["A", "B"]);

  const handleDrop = (event: DndSortChangeEvent) => {
    setMostRecentEvent(event);
    setOrder((previousOrder) => event.zoneSort || previousOrder);
  };

  return (
    <DndSort onDrop={handleDrop}>
      <DndSort.Zone
        sortable
        id="zone-1"
        orientation="vertical"
        sortedIds={order}
        label="Zone 1"
        defaultDropPosition="end"
      >
        {order.map((itemId) => (
          <DndSort.Card
            key={itemId}
            label={teams[itemId].name}
            id={itemId}
            alignItems="center"
            dragOnlyWithHandle
          >
            {teams[itemId].name}
          </DndSort.Card>
        ))}
      </DndSort.Zone>
    </DndSort>
  );
}

export default App;
```

Use the Drag Handle only as the draggable region when:

* A user should be able to select the text of the Card
* The Card is very large
* There are other, non-overflow menu actions a user can take in the Card. This could be something as simple as a Checkbox.

### Drag Preview

Drag Previews, what is displayed while dragging, can be customized.

dnd-sort-preview

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-preview?file=src/App.tsx)

```tsx lines theme={null}
import { useState } from "react";
import {
  DndSort,
  Card,
  Flex,
  Avatar,
  Text,
  Chip,
  type DndSortChangeEvent,
} from "@servicetitan/anvil2";

type Order = (string | number)[];

function App() {
  const [_mostRecentEvent, setMostRecentEvent] = useState<DndSortChangeEvent>();
  const [order, setOrder] = useState<Order>(["A", "B", "C"]);

  const handleDrop = (event: DndSortChangeEvent) => {
    setMostRecentEvent(event);
    setOrder((previousOrder) => event.zoneSort || previousOrder);
  };

  return (
    <DndSort onDrop={handleDrop}>
      <DndSort.Zone
        sortable
        id="zone-1"
        orientation="vertical"
        sortedIds={order}
        label="Zone 1"
        defaultDropPosition="end"
      >
        <DndSort.Card label="label" id="A" alignItems="center">
          No custom drag preview
        </DndSort.Card>
        <DndSort.Card
          label="label"
          id="B"
          alignItems="center"
          previewRenderer={() => <Card>Small drag preview</Card>}
        >
          Small drag preview
        </DndSort.Card>
        <DndSort.Card
          label="label"
          id="C"
          alignItems="center"
          previewRenderer={() => (
            <Card>
              <Flex alignItems="center" gap={3}>
                <Avatar image="/dog-01.png" name="Dog01" />
                <Flex direction="column">
                  <Flex alignItems="center" gap={3}>
                    <Text variant="headline" el="h4" size="small">
                      Maximilian the Magnificent
                    </Text>
                  </Flex>
                  <Text size="small" variant="body">
                    Director of Plumber Operations
                  </Text>
                </Flex>
                <Chip label="Admin" />
              </Flex>
            </Card>
          )}
        >
          Highly custom preview
        </DndSort.Card>
      </DndSort.Zone>
    </DndSort>
  );
}

export default App;
```

### Drop Zone

dnd-sort-drop-zone

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-drop-zone?file=src/App.tsx)

```tsx lines theme={null}
import { Flex, Dnd } from "@servicetitan/anvil2";

function App() {
  return (
    <Flex gap={4}>
      <Dnd.Zone style={{ width: "25%" }} className="p-3">
        Resting Drop Zone
      </Dnd.Zone>
      <Dnd.Zone isDragging style={{ width: "25%" }} className="p-3">
        Visible Drop Zone while dragging
      </Dnd.Zone>
      <Dnd.Zone
        isDragging
        isOver
        isValid
        style={{ width: "25%" }}
        className="p-3"
      >
        Valid Drop Zone, user is over the Drop Zone
      </Dnd.Zone>
      <Dnd.Zone isDragging isOver style={{ width: "25%" }} className="p-3">
        Invalid drop zone, user is over the Drop Zone
      </Dnd.Zone>
    </Flex>
  );
}

export default App;
```

### Sort Line

dnd-sort-line-vertical

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-line-vertical?file=src/App.tsx)

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

function App() {
  return (
    <div style={{ width: "100%", position: "relative" }}>
      <Dnd.SortLine orientation="vertical" position="before" offset="0" />
    </div>
  );
}

export default App;
```

dnd-sort-line-horizontal

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-line-horizontal?file=src/App.tsx)

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

function App() {
  return (
    <div style={{ height: "40px", position: "relative" }}>
      <Dnd.SortLine orientation="horizontal" position="before" offset="0" />
    </div>
  );
}

export default App;
```

## Behavior

### Drag handle alignment

The drag handle is always top-aligned in the Draggable Card. The rest of the card content can be vertically aligned as desired.

dnd-sort-handle-alignment

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-handle-alignment?file=src/App.tsx)

```tsx lines theme={null}
import { useState } from "react";
import { DndSort } from "@servicetitan/anvil2";

type Items = {
  [key: string | number]: { name: string; type: string };
};

type Order = (string | number)[];

const teams: Items = {
  A: {
    name: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus semper vitae metus ut venenatis. Morbi commodo ante nec faucibus porta. Etiam dignissim neque id libero mollis pretium. Cras vitae mattis mauris. Ut ut enim porta, blandit nibh et, finibus massa. Ut tincidunt nibh risus, et dictum felis tristique non.",
    type: "item",
  },
};

function App() {
  const [order, _setOrder] = useState<Order>(["A"]);

  return (
    <DndSort>
      {order.map((itemId) => (
        <DndSort.Card
          key={itemId}
          label={teams[itemId].name}
          id={itemId}
          alignItems="center"
          style={{ width: "400px" }}
        >
          {teams[itemId].name}
        </DndSort.Card>
      ))}
    </DndSort>
  );
}

export default App;
```

## Accessibility

### Provide alternatives to Drag and Drop

While Dnd Sort is keyboard navigable, it is still preferable that users have alternatives to using drag and drop functionality to complete a task.

## Usage Guidelines

| Standard | Requirement |
| - | - |
| DoSorting a list | Sorting a vertical or horizontal list of items. |
| DoBucketing items | Bucketing and sorting items, such as a kanban UI. |
| DoUse the provided drag-handle icon | Use the base `Dnd.Handle` icon for drag and drop; see [How to Use](#how-to-use). |
| DoUse the default Drop Zone and sort line styles | Use the provided default styles of Drop Zone and sort line; see [How to Use](#how-to-use). |
| Don'tNon-standard drag and drop scenarios | Dnd Sort only covers a handful of drag and drop scenarios. Don't force alternative scenarios to use this approach — use the individual components found in Dnd to assemble a custom scenario instead. |
| Don'tAlternative drag-handle icons | Avoid using alternative icons for drag and drop; see [How to Use](#how-to-use). |
| Don'tSimplified drag-and-drop styling | Avoid simplifying the styling of drag and drop — the default styling provides clarity on where a user can drop in a complicated app; see [How to Use](#how-to-use). |

### Examples of DND Sort

#### Sorting a List

Sorting a list is the primary usage of Dnd Sort. This can be done vertically and horizontally.

[dnd-sort-list-vertical](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-list-vertical?embed=1\&hideNavigation=1\&hideExplorer=1\&terminalHeight=0\&file=src/App.tsx)

[dnd-sort-list-horizontal](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-list-horizontal?embed=1\&hideNavigation=1\&hideExplorer=1\&terminalHeight=0\&file=src/App.tsx)

#### Kanban-style sort

Also known as bucketing. Note that the Card containers & text are not explicitly part of the Dnd Sort component.

dnd-sort-kanban

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-kanban?file=src/App.tsx)

```tsx lines theme={null}
import { useState } from "react";
import {
  DndSort,
  Flex,
  Grid,
  Card,
  Text,
  type DndSortChangeEvent,
} from "@servicetitan/anvil2";

type Buckets = {
  [key: string]: (string | number)[];
};

function App() {
  const [_mostRecentEvent, setMostRecentEvent] = useState<DndSortChangeEvent>();
  const [buckets, setBuckets] = useState<Buckets>({
    "zone-1": ["A", "B", "C"],
    "zone-2": ["D", "E"],
    "zone-3": ["F"],
  });

  const someStylesOfYourChoosing = {
    flexDirection: "column",
    width: "100%",
    borderRadius: "12px",
  } as const;

  const handleDrop = (event: DndSortChangeEvent) => {
    setMostRecentEvent(event);
    setBuckets((prevBuckets) => {
      // If the item wasn't dropped in a zone, do nothing
      if (!event.zoneId) return prevBuckets;

      // If the item was dropped in an invalid zone, do nothing
      if (!event.valid) return prevBuckets;

      // If the item was dropped in the zone it was already in, do nothing
      if (event.zoneId === event.previousZoneId) return prevBuckets;

      // Create a copy of the current buckets
      const newBuckets = { ...prevBuckets };

      // Remove the item from its previous bucket
      const prevKey = event.previousZoneId;
      if (prevKey) {
        newBuckets[prevKey] = newBuckets[prevKey].filter(
          (id) => id !== event.draggableId,
        );
      }

      // Add the item to the target bucket
      newBuckets[event.zoneId] = newBuckets[event.zoneId].concat(
        event.draggableId,
      );

      return newBuckets;
    });
  };

  return (
    <DndSort onDrop={handleDrop}>
      <Flex direction="column" gap="8" style={{ width: "100%" }}>
        <Grid templateColumns="repeat(3, 1fr)" gap="4">
          <Card padding="0" background="strong">
            <DndSort.Zone
              id="zone-1"
              label="Zone 1"
              style={someStylesOfYourChoosing}
              gap={3}
            >
              <Text>
                <b>Todo</b>
              </Text>
              {buckets["zone-1"].map((itemId) => (
                <DndSort.Card
                  key={itemId}
                  label={`Item ${itemId}`}
                  id={itemId}
                  alignItems="center"
                >
                  Item {itemId}
                </DndSort.Card>
              ))}
            </DndSort.Zone>
          </Card>
          <Card padding="0" background="strong">
            <DndSort.Zone
              id="zone-2"
              label="Zone 2"
              style={someStylesOfYourChoosing}
              gap={3}
            >
              <Text>
                <b>In Progress</b>
              </Text>
              {buckets["zone-2"].map((itemId) => (
                <DndSort.Card
                  key={itemId}
                  label={`Item ${itemId}`}
                  id={itemId}
                  alignItems="center"
                >
                  Item {itemId}
                </DndSort.Card>
              ))}
            </DndSort.Zone>
          </Card>
          <Card padding="0" background="strong">
            <DndSort.Zone
              id="zone-3"
              label="Zone 3"
              style={someStylesOfYourChoosing}
              gap={3}
            >
              <Text>
                <b>Done</b>
              </Text>
              {buckets["zone-3"].map((itemId) => (
                <DndSort.Card
                  key={itemId}
                  label={`Item ${itemId}`}
                  id={itemId}
                  alignItems="center"
                >
                  Item {itemId}
                </DndSort.Card>
              ))}
            </DndSort.Zone>
          </Card>
        </Grid>
      </Flex>
    </DndSort>
  );
}

export default App;
```

#### Dashboard-type arrangements

dnd-sort-dashboard

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-dashboard?file=src/App.tsx)

```tsx lines theme={null}
import { useState } from "react";
import { DndSort, Grid, type DndSortChangeEvent } from "@servicetitan/anvil2";

type Buckets = {
  [key: string]: (string | number)[];
};

function App() {
  const [_mostRecentEvent, setMostRecentEvent] = useState<DndSortChangeEvent>();
  const [buckets, setBuckets] = useState<Buckets>({
    "zone-1": ["A"],
    "zone-2": ["B"],
    "zone-3": ["C"],
    "zone-4": ["D"],
  });

  const someStylesOfYourChoosing = {
    flexDirection: "column",
    width: "100%",
  } as const;

  const handleDrop = (event: DndSortChangeEvent) => {
    console.info(event);
    setMostRecentEvent(event);
    setBuckets((prevBuckets) => {
      // If the item wasn't dropped in a zone, do nothing
      if (!event.zoneId) return prevBuckets;

      // If the item was dropped in an invalid zone, do nothing
      if (!event.valid) return prevBuckets;

      // If the item was dropped in the zone it was already in, do nothing
      if (event.zoneId === event.previousZoneId) return prevBuckets;

      // Create a copy of the current buckets
      const newBuckets = { ...prevBuckets };

      const targetBucketItems = newBuckets[event.zoneId];
      const draggableId = event.draggableId;
      const previousZoneId = event.previousZoneId;

      // Check if there's an item in the target zone and it's not the item being dragged
      if (
        targetBucketItems &&
        targetBucketItems.length > 0 &&
        targetBucketItems[0] !== draggableId
      ) {
        const itemToSwapOut = targetBucketItems[0]; // Assuming only one item per zone for simplicity or taking the first one
        // Remove the item from its previous bucket (the draggableId)
        if (previousZoneId) {
          newBuckets[previousZoneId] = newBuckets[previousZoneId].filter(
            (id) => id !== draggableId,
          );
        }
        // Add the item that was in the target bucket to the previous bucket
        if (previousZoneId) {
          // Only if there was a previous zone
          newBuckets[previousZoneId] =
            newBuckets[previousZoneId].concat(itemToSwapOut);
        }

        // Replace the item in the target bucket with the draggableId
        newBuckets[event.zoneId] = [draggableId]; // Assuming only one item per zone
      } else {
        // --- Original logic if no swap or if target bucket is empty ---

        // Remove the item from its previous bucket
        if (previousZoneId) {
          newBuckets[previousZoneId] = newBuckets[previousZoneId].filter(
            (id) => id !== draggableId,
          );
        }

        // Add the item to the target bucket
        newBuckets[event.zoneId] = newBuckets[event.zoneId].concat(draggableId);
      }

      return newBuckets;
    });
  };

  return (
    <DndSort onDrop={handleDrop}>
      <Grid templateColumns="repeat(2, 1fr)" gap="0">
        <DndSort.Zone
          id="zone-1"
          label="Zone 1"
          style={someStylesOfYourChoosing}
          gap={2}
        >
          {buckets["zone-1"].map((itemId) => (
            <DndSort.Card
              key={itemId}
              label={`Item ${itemId}`}
              id={itemId}
              style={{ height: "150px" }}
            >
              Dashboard item {itemId}
            </DndSort.Card>
          ))}
        </DndSort.Zone>

        <DndSort.Zone
          id="zone-2"
          label="Zone 2"
          style={someStylesOfYourChoosing}
          gap={2}
        >
          {buckets["zone-2"].map((itemId) => (
            <DndSort.Card
              key={itemId}
              label={`Item ${itemId}`}
              id={itemId}
              style={{ height: "150px" }}
            >
              Dashboard item {itemId}
            </DndSort.Card>
          ))}
        </DndSort.Zone>
        <DndSort.Zone
          id="zone-3"
          label="Zone 3"
          style={someStylesOfYourChoosing}
          gap={2}
        >
          {buckets["zone-3"].map((itemId) => (
            <DndSort.Card
              key={itemId}
              label={`Item ${itemId}`}
              id={itemId}
              style={{ height: "150px" }}
            >
              Dashboard item {itemId}
            </DndSort.Card>
          ))}
        </DndSort.Zone>

        <DndSort.Zone
          id="zone-4"
          label="Zone 4"
          style={someStylesOfYourChoosing}
          gap={2}
        >
          {buckets["zone-4"].map((itemId) => (
            <DndSort.Card
              key={itemId}
              label={`Item ${itemId}`}
              id={itemId}
              style={{ height: "150px" }}
            >
              Dashboard item {itemId}
            </DndSort.Card>
          ))}
        </DndSort.Zone>
      </Grid>
    </DndSort>
  );
}

export default App;
```

### How to Use

Use the base configurations of icons, drop zones

dnd-sort-handle-icon

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-handle-icon?file=src/App.tsx)

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

function App() {
  return <Dnd.Handle />;
}

export default App;
```

**Do**
use the provided icon for drag and drop

dnd-sort-handle-dont

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-handle-dont?file=src/App.tsx)

```tsx lines theme={null}
function App() {
  return (
    <svg
      width="120"
      height="24"
      viewBox="0 0 240 48"
      xmlns="http://www.w3.org/2000/svg"
      fill="var(--foreground-color-subdued)"
    >
      <g clipPath="url(#clip0_2833_38276)">
        <path d="M24 16C26.21 16 28 14.21 28 12C28 9.79 26.21 8 24 8C21.79 8 20 9.79 20 12C20 14.21 21.79 16 24 16ZM24 20C21.79 20 20 21.79 20 24C20 26.21 21.79 28 24 28C26.21 28 28 26.21 28 24C28 21.79 26.21 20 24 20ZM24 32C21.79 32 20 33.79 20 36C20 38.21 21.79 40 24 40C26.21 40 28 38.21 28 36C28 33.79 26.21 32 24 32Z" />
      </g>
      <path d="M104 18H72V22H104V18ZM72 30H104V26H72V30Z" />
      <path d="M152 44L143.5 35.5L146.35 32.65L150 36.3V26H139.75L143.4 29.6L140.5 32.5L132 24L140.45 15.55L143.3 18.4L139.7 22H150V11.7L146.35 15.35L143.5 12.5L152 4L160.5 12.5L157.65 15.35L154 11.7V22H164.25L160.6 18.4L163.5 15.5L172 24L163.5 32.5L160.65 29.65L164.3 26H154V36.25L157.6 32.6L160.5 35.5L152 44Z" />
      <path d="M206 34L196 24L206 14L208.8 16.8L203.65 22H228.35L223.2 16.8L226 14L236 24L226 34L223.2 31.2L228.35 26H203.65L208.8 31.2L206 34Z" />
      <defs>
        <clipPath id="clip0_2833_38276">
          <rect width="48" height="48" />
        </clipPath>
      </defs>
    </svg>
  );
}

export default App;
```

**Don’t**
use alternative icons for drag and drop

***

![Do use the provided default styles of Drop Zone, sort line](https://mintcdn.com/servicetitan/gaitSLKK1iHwa_zv/images/docs/web/components/dnd-sort/do-use-drop-zone.png?fit=max\&auto=format\&n=gaitSLKK1iHwa_zv\&q=85\&s=bc441fa242dda32f414fe178a68ea91f)

**Do**
use the provided default styles of Drop Zone, sort line

![Don't simplify the styling of drag and drop](https://mintcdn.com/servicetitan/gaitSLKK1iHwa_zv/images/docs/web/components/dnd-sort/dont-simplify-drop-zone.png?fit=max\&auto=format\&n=gaitSLKK1iHwa_zv\&q=85\&s=70b195b42e7487c66a54060d92d1c816)

**Don’t**
simplify the styling of drag and drop. The default styling provides clarity on where a user can drop in a complicated app.

## How to Use

Use the base configurations of icons, drop zones

dnd-sort-handle-icon

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-handle-icon?file=src/App.tsx)

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

function App() {
  return <Dnd.Handle />;
}

export default App;
```

**Do**
use the provided icon for drag and drop

dnd-sort-handle-dont

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dnd-sort-handle-dont?file=src/App.tsx)

```tsx lines theme={null}
function App() {
  return (
    <svg
      width="120"
      height="24"
      viewBox="0 0 240 48"
      xmlns="http://www.w3.org/2000/svg"
      fill="var(--foreground-color-subdued)"
    >
      <g clipPath="url(#clip0_2833_38276)">
        <path d="M24 16C26.21 16 28 14.21 28 12C28 9.79 26.21 8 24 8C21.79 8 20 9.79 20 12C20 14.21 21.79 16 24 16ZM24 20C21.79 20 20 21.79 20 24C20 26.21 21.79 28 24 28C26.21 28 28 26.21 28 24C28 21.79 26.21 20 24 20ZM24 32C21.79 32 20 33.79 20 36C20 38.21 21.79 40 24 40C26.21 40 28 38.21 28 36C28 33.79 26.21 32 24 32Z" />
      </g>
      <path d="M104 18H72V22H104V18ZM72 30H104V26H72V30Z" />
      <path d="M152 44L143.5 35.5L146.35 32.65L150 36.3V26H139.75L143.4 29.6L140.5 32.5L132 24L140.45 15.55L143.3 18.4L139.7 22H150V11.7L146.35 15.35L143.5 12.5L152 4L160.5 12.5L157.65 15.35L154 11.7V22H164.25L160.6 18.4L163.5 15.5L172 24L163.5 32.5L160.65 29.65L164.3 26H154V36.25L157.6 32.6L160.5 35.5L152 44Z" />
      <path d="M206 34L196 24L206 14L208.8 16.8L203.65 22H228.35L223.2 16.8L226 14L236 24L226 34L223.2 31.2L228.35 26H203.65L208.8 31.2L206 34Z" />
      <defs>
        <clipPath id="clip0_2833_38276">
          <rect width="48" height="48" />
        </clipPath>
      </defs>
    </svg>
  );
}

export default App;
```

**Don’t**
use alternative icons for drag and drop

***

![Do use the provided default styles of Drop Zone, sort line](https://mintcdn.com/servicetitan/gaitSLKK1iHwa_zv/images/docs/web/components/dnd-sort/do-use-drop-zone.png?fit=max\&auto=format\&n=gaitSLKK1iHwa_zv\&q=85\&s=bc441fa242dda32f414fe178a68ea91f)

**Do**
use the provided default styles of Drop Zone, sort line

![Don't simplify the styling of drag and drop](https://mintcdn.com/servicetitan/gaitSLKK1iHwa_zv/images/docs/web/components/dnd-sort/dont-simplify-drop-zone.png?fit=max\&auto=format\&n=gaitSLKK1iHwa_zv\&q=85\&s=70b195b42e7487c66a54060d92d1c816)

**Don’t**
simplify the styling of drag and drop. The default styling provides clarity on where a user can drop in a complicated app.

## Code

**Implementation**

[dndsort-playground](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dndsort-playground?embed=1\&hideNavigation=1\&hideExplorer=1\&terminalHeight=0\&file=src/App.tsx)

## Common Examples

### Bucketing

`DndSort` is a great way to create a drag-and-drop interface for moving items between different buckets.

[dndsort-bucket](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dndsort-bucket?embed=1\&hideNavigation=1\&hideExplorer=1\&terminalHeight=0\&file=src/App.tsx)

### Sorting a list

`DndSort` is also a great way to create a drag-and-drop interface for sorting a list of items.

[dndsort-list](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dndsort-list?embed=1\&hideNavigation=1\&hideExplorer=1\&terminalHeight=0\&file=src/App.tsx)

### Bucket and sort

`DndSort` can also be used to create a drag-and-drop interface for moving items between different buckets and sorting them within those buckets.

Note this example also demonstrates cards initially beginning outside of the buckets.

[dndsort-bucket-list](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dndsort-bucket-list?embed=1\&hideNavigation=1\&hideExplorer=1\&terminalHeight=0\&file=src/App.tsx)

### Validation

`DndSort` provides ways to validate the drag-and-drop interaction. This is useful for preventing invalid interactions, such as moving an item to a bucket that does not accept it.

#### Basic Validation

The simplest way to validate is to use the `type` prop on the `DndSort.Card` component in conjunction with `acceptedTypes` prop on the `DndSort.Zone` component.

```tsx theme={null}
<DndSort.Zone acceptedTypes={["fruit"]}>
  <DndSort.Card type="fruit">🍎 Apple</DndSort.Card>
</DndSort.Zone>
```

[dndsort-acceptedtypes](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dndsort-acceptedtypes?embed=1\&hideNavigation=1\&hideExplorer=1\&terminalHeight=0\&file=src/App.tsx)

#### Advanced Validation

A more advanced way to validate is to use the `validator` prop on the `DndSort.Zone` component. This prop takes a function which receives the item `id`. The function should return a boolean indicating whether the drop is valid. Here are some examples of how to use the `validator` prop:

```tsx theme={null}
// Limit the zone to only accept items with ids in an allowed list
const validator = (id) => validIds.includes(id);
<DndSort.Zone validator={validator}></DndSort.Zone>;

// Limit the based on a property of the item
const validator = (id) => people[id].isFriendly;
<DndSort.Zone validator={validator}></DndSort.Zone>;

// Limit the number of items in a zone to 5
const validator = () => zoneItems.length <= 5;
<DndSort.Zone validator={validator}></DndSort.Zone>;
```

### Customization

`DndSort` provides a number of ways to customize the drag-and-drop interaction. This includes customizing the appearance of the items and zones.

#### Advanced zone rendering

The `advancedRenderer` prop of `DndSort.Zone` is an alternative to `children`, allowing you to customize the appearance of the drop zone based upon its internal state.This allows you to show different content when a drag is happening, when an item is over the drop zone, or if the dragged item is valid/invalid for the drop zone.

#### Custom card preview rendering

The `previewRenderer` prop of `DndSort.Card` offers a means to customize the appearance of the card when it is being dragged. This is useful for showing a different appearance for the card, such as a simpler version of a complex card.

### Interactive Children

By default, the `DndSort.Card` places a button around the entire card. This is optimal for accessibility. But if you want to place interactive elements inside your card, it harms accessibility. To resolve this issue, set `dragOnlyWithHandle` on the `DndSort.Card` if your card has interactive elements.

dndsort-dragonlywithhandle

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/dndsort-dragonlywithhandle?file=src/App.tsx)

```tsx lines expandable theme={null}
import { DndSort, Flex, Text, Button } from "@servicetitan/anvil2";

function App() {
  return (
    <DndSort>
      <DndSort.Card id="example" label="example" dragOnlyWithHandle>
        <Flex direction="column" gap="4">
          <Text>
            This card contains interactive elements. <br /> That means it should
            only be draggable with the handle.
          </Text>
          <Button appearance="primary" onClick={() => alert("button clicked")}>
            Click me
          </Button>
        </Flex>
      </DndSort.Card>
    </DndSort>
  );
}

export default App;
```

## Anti-Patterns

### Nesting

`DndSort` does not support nesting. i.e. you may not use a zone as an item.

This is an intentional design decision to guarantee these components meet our accessibility and usability standards.

If you have a use case that requires nesting, please reach out to us. We would love to hear about your use case and help you find a solution.

## Best Practices

### dragOnlyWithHandle

```tsx theme={null}
<DndSort.Card dragOnlyWithHandle>
  <Button />
</DndSort.Card>
```

> [!TIP]
>
> **Do**

Use the dragOnlyWithHandle prop if your card contains interactive elements.

```tsx theme={null}
<DndSort.Card>
  <Button />
</DndSort.Card>
```

> [!CAUTION]
>
> **Don't**

Don't use interactive elements in cards without dragOnlyWithHandle.

**DndSort Props**

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

function ExampleComponent() {
return (

<DndSort onDrop={(event) => console.log(event)}>
  {/* DndSort.Zone and DndSort.Card components */}
</DndSort>
); }

```

## `DndSort` Props

`DndSort` is the root component that must wrap `DndSort` sub-components. It provides the context and internal logic for the drag-and-drop interaction.

**children** `React.ReactNode`

Various children elements including `DndSort.Zone` and `DndSort.Card`.

**onDrop** `(DndSortChangeEvent) =\> void`

Callback function fired when an item is dropped.

**DndSort.Card Props**

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

function ExampleComponent() {
  return (
    <DndSort.Card
      id="card-1"
      label="Card 1"
      dragOnlyWithHandle={false}
    >
      Card content
    </DndSort.Card>
  );
}
```

## `DndSort.Card` Props

`DndSort.Card` is a component that represents a card that can be dragged and dropped. It uses `Card` internally and most of the props are forwarded to the `Card`.

**id** `string | number` *required*

The unique identifier for the card.

**label** `string` *required*

The label for the card, used for accessibility.

**children** `React.ReactNode`

The content of the card.

**dragOnlyWithHandle** `boolean` (default: false)

If true, the card can only be dragged when the drag handle is used. If false,
the card can be dragged by any part of it.

**onDrop** `(DndSortChangeEvent) =\> void`

Callback function to be called when the card is dropped. Same as `onDrop` on
the `DndSort` component.

**previewRenderer** `(\{ valid \}) =\> React.ReactNode`

Advanced. A function that returns a React node to be used as the preview
renderer.

**type** `string`

The type of the card, used for validation (optional).

**DndSort.Zone Props**

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

function ExampleComponent() {
return (

<DndSort.Zone
  id="zone-1"
  label="Zone 1"
  sortable={false}
  orientation="vertical"
>
  {/* Zone content */}
</DndSort.Zone>
); }

```

## `DndSort.Zone` Props

`DndSort.Zone` is a component that represents a drop zone where items can be dropped. It may either be sortable or non-sortable. Only the basic styling is applied to the zone (i.e. border and background during a drag-and-drop interaction). Props are forwarded to the underlying `div` element, so you can provide `styles`, `className`, and other props to customize the appearance of the zone.

**id** `string | number` *required*

The unique identifier for the zone.

**label** `string` *required*

The label for the zone, used for accessibility.

**acceptedTypes** `string[]`

An array of accepted types for this zone. If not provided, all types are
accepted. Compares against the type prop of the card.

**advancedRenderer** `(\{ isDragging, isOver, isValid \}) =\> React.ReactNode`

Advanced. A function to render the drop zone using internal state parameters.

**children** `React.ReactNode`

The contents of the zone.

**defaultDropPosition** `"start" | "end"` (default: end)

The default position where items will be dropped if dropped directly onto the zone.

**gap** `GapSize` (default: 4)

The gap between items in the zone. See `Flex` for more info. Important for sortable zones to maintain
enough space for the sort line.

**orientation** `"horizontal" | "vertical"` (default: vertical)

The orientation of the items in the zone.

**sortable** `boolean` (default: false)

If `true`, the zone is sortable. If `false`, the zone is not sortable.

**sortedIds** `(string | number)[]` (default: \[])

Required if sortable. An array of sorted IDs for the items in the zone, must
match render order.

**validator** `(id) =\> boolean`

A function to validate the drop action. Given an item's `id`, this function
must return `true` for valid drops, `false` for invalid drops.

**DndSortChangeEvent**

## DndSortChangeEvent

**draggableId** `string | number`

The ID of the draggable item that was dropped.

**previousZoneId** `string | number | null`

The ID of the previous drop zone where the item was located before being
dropped. This can be `null` if the item is coming from outside of any zone.

**previousZoneSort** `"(string" | "number)[]" | "null"`

An array representing the sort order of items in the previous drop zone before
the drop. This can be used to update the order of items in the previous zone.
This value is `null` if the previous drop zone (if any) was not sortable.

**valid** `boolean`

Indicates whether the drop was valid based on the accepted types and custom
validation.

**zoneId** `string | number | null`

The ID of the drop zone where the item was dropped. This can be `null` if the
item was not dropped in a zone.

**zoneSort** `"(string" | "number)[]" | "null"`

An array representing the sort order of items in the drop zone after the drop.
This can be used to update the order of items in the zone. This value is
`null` if the target drop zone (if any) is not sortable.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.