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.
<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.
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
Controlled columns
Extract the move logic into a hook and reuse it across screens.
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.
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.
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.
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.
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>;
}