Expo Kanban
On this page5 sections

Getting started

Examples

Recipes that mirror real screens. Each example is intentionally small enough to copy into your app.

Basic board

The smallest useful board: three columns, custom cards, and local state.

MinimalBoard.tsx
<ReactNativeKanbanBoard
  columnData={columns}
  renderItem={(item, isDragged) => <Card item={item} active={isDragged} />}
  renderHeader={(header) => <Text>{header.title}</Text>}
  onDragEnd={({ fromColumnIndex, toColumnIndex, itemId }) => {
    moveItem(fromColumnIndex, toColumnIndex, itemId);
  }}
/>;

For the complete screen including types, data, and styles, see the Usage page.

Custom cards

Render priority, owner, and dates without the component knowing about them.

LeadCard.tsx
const renderItem = (item: Lead, isDragged?: boolean) => (
  <View style={[styles.card, isDragged && styles.lifted]}>
    <View style={styles.row}>
      <Text style={styles.name}>{item.name}</Text>
      <View
        style={[
          styles.priority,
          item.priority === 'high' && styles.priorityHigh,
          item.priority === 'medium' && styles.priorityMedium,
        ]}
      >
        <Text style={styles.priorityLabel}>{item.priority}</Text>
      </View>
    </View>

    <Text style={styles.company}>{item.company}</Text>

    <View style={styles.footer}>
      <Text style={styles.owner}>{item.owner}</Text>
      <Text style={styles.date}>{item.createdOn}</Text>
    </View>
  </View>
);

Style the lifted state

The second argument is only true for the floating preview. Use it to add elevation, scale, or a rotation-free shadow.

Controlled columns

Extract the move logic into a hook and reuse it across screens.

useKanban.ts
import { useCallback, useState } from 'react';

export function useKanban(initialColumns: Column[]) {
  const [columns, setColumns] = useState(initialColumns);

  const onDragEnd = useCallback(
    ({ fromColumnIndex, toColumnIndex, itemId }: DragEnd) => {
      if (fromColumnIndex === toColumnIndex) return;

      setColumns((current) => {
        const next = current.map((column) => ({
          ...column,
          items: [...column.items],
        }));

        const source = next[fromColumnIndex].items;
        const index = source.findIndex((item) => item.id === itemId);
        if (index === -1) return current;

        const [moved] = source.splice(index, 1);
        next[toColumnIndex].items.push(moved);
        return next;
      });
    },
    []
  );

  return { columns, setColumns, onDragEnd };
}

Keeping onDragEnd stable with useCallback prevents unnecessary card re-renders while dragging.

BoardScreen.tsx
import React, { useState } from 'react';
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native';
import ReactNativeKanbanBoard from 'react-native-expo-kanban';

export default function BoardScreen() {
  const { width } = useWindowDimensions();
  const [columns, setColumns] = useState(initialColumns);

  const renderHeader = (header: ColumnHeader) => (
    <View style={styles.header}>
      <Text style={styles.headerTitle}>{header.title}</Text>
      {header.subtitle ? (
        <Text style={styles.headerSubtitle}>{header.subtitle}</Text>
      ) : null}
    </View>
  );

  const renderItem = (item: Lead, isDragged?: boolean) => (
    <View style={[styles.card, isDragged && styles.cardDragged]}>
      <Text style={styles.cardTitle}>{item.name}</Text>
      <Text style={styles.cardMeta}>{item.company}</Text>
      <Text style={styles.cardMeta}>Priority: {item.priority}</Text>
    </View>
  );

  const onDragEnd = ({ fromColumnIndex, toColumnIndex, itemId }: {
    fromColumnIndex: number;
    toColumnIndex: number;
    itemId: string;
  }) => {
    if (fromColumnIndex === toColumnIndex) return;

    setColumns((current) => {
      const next = current.map((column) => ({
        ...column,
        items: [...column.items],
      }));

      const source = next[fromColumnIndex].items;
      const index = source.findIndex((item) => item.id === itemId);
      if (index === -1) return current;

      const [moved] = source.splice(index, 1);
      next[toColumnIndex].items.push(moved);
      return next;
    });
  };

  return (
    <ReactNativeKanbanBoard
      columnData={columns}
      renderHeader={renderHeader}
      renderItem={renderItem}
      onDragEnd={onDragEnd}
      columnWidth={width * 0.8}
      gapBetweenColumns={12}
      columnContainerStyle={styles.column}
      columnContainerStyleOnDrag={styles.columnHover}
      columnHeaderStyle={styles.columnHeader}
    />
  );
}

const styles = StyleSheet.create({
  column: {
    borderRadius: 16,
    backgroundColor: '#f4f4f5',
  },
  columnHover: {
    backgroundColor: '#e4e4e7',
    borderColor: '#a1a1aa',
    borderWidth: 1,
  },
  columnHeader: {
    paddingBottom: 8,
  },
  header: {
    gap: 2,
  },
  headerTitle: {
    fontSize: 16,
    fontWeight: '600',
  },
  headerSubtitle: {
    fontSize: 12,
    color: '#71717a',
  },
  card: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 14,
    gap: 4,
    borderWidth: 1,
    borderColor: '#e4e4e7',
  },
  cardDragged: {
    transform: [{ scale: 1.02 }],
    shadowColor: '#000',
    shadowOpacity: 0.15,
    shadowRadius: 12,
    elevation: 8,
  },
  cardTitle: {
    fontSize: 14,
    fontWeight: '600',
  },
  cardMeta: {
    fontSize: 12,
    color: '#71717a',
  },
});

Full-width columns

One column per screen with zero gap.

FullWidthBoard.tsx
import { useWindowDimensions } from 'react-native';

const { width } = useWindowDimensions();

<ReactNativeKanbanBoard
  columnData={columns}
  renderItem={renderItem}
  renderHeader={renderHeader}
  onDragEnd={onDragEnd}
  columnWidth={width}
  gapBetweenColumns={0}
/>;

Use gapBetweenColumns=0 when columns should meet edge to edge. The board still snaps one full screen at a time.

Using the hooks

The internals are importable if you need to build a custom board.

MyList.tsx
import { useLayoutAnimation } from 'react-native-expo-kanban/dist/hooks/useLayoutAnimation';

function MyList({ items }) {
  useLayoutAnimation(items, { effectType: 'easeInEaseOut', duration: 200 });
  return <List data={items} />;
}

The hooks are compiled into the package at react-native-expo-kanban/dist/hooks and dist/utils. They are useful references, but deep imports are not covered by semver and may change between minor releases.

CardOverlay.tsx
import { useDragContext } from 'react-native-expo-kanban/dist/utils/DraggedCardContext';

function CardOverlay() {
  const { dragCard, dragCardX, dragCardY } = useDragContext();

  if (!dragCard) return null;

  return <Text>Dragging {dragCard.id}</Text>;
}