Skip to content

Collapsible & expandable reorderable list feature #211

Description

@LunatiqueCoder

Discussed in #210

Originally posted by iCodePup March 22, 2026
Hello everyone,

I’m currently experimenting with the new V1 version, which looks very promising. thanks a lot for the great work :)

I’m trying to implement a reorderable list with collapsible and expandable items.
The goal is:

  • When the user starts dragging an item, all other items should collapse and gather together.
  • When the drag ends (after sorting), the items should expand again.

Additionally, each row can have a dynamic height (not fixed), which makes the behavior a bit more complex.
Unfortunately, I haven’t been able to get this working so far
Below is the example code I used, along with a video showing the result.
Thanks in advance for your help!

video that demonstrates the expected behavior I’m trying to achieve :

460819458-81418873-5cf1-41a8-8847-7b769401881f.mp4

current result

Edited_20260322_114216.mp4
import { useCallback, useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Animated, { LinearTransition } from 'react-native-reanimated';
import { DraxProvider, DraxList } from 'react-native-drax';

interface Recipe {
  id: string;
  title: string;
  ingredients: { name: string; quantity: string }[];
}

const MOCK_RECIPES: Recipe[] = [
  { id: '1', title: 'Pasta Carbonara', ingredients: [{ name: 'Spaghetti', quantity: '200g' }, { name: 'Bacon', quantity: '150g' }, { name: 'Eggs', quantity: '3' }] },
  { id: '2', title: 'Caesar Salad', ingredients: [{ name: 'Romaine', quantity: '1 head' }, { name: 'Croutons', quantity: '100g' }, { name: 'Parmesan', quantity: '50g' }] },
  { id: '3', title: 'Chicken Curry', ingredients: [{ name: 'Chicken', quantity: '500g' }, { name: 'Curry paste', quantity: '3 tbsp' }] },
  { id: '4', title: 'Margherita Pizza', ingredients: [{ name: 'Dough', quantity: '300g' }, { name: 'Mozzarella', quantity: '200g' }, { name: 'Tomato sauce', quantity: '100ml' }] },
  { id: '5', title: 'Beef Tacos', ingredients: [{ name: 'Ground beef', quantity: '400g' }, { name: 'Tortillas', quantity: '6' }] },
];

const COLLAPSED_HEIGHT = 48;

export default function App() {
  const [recipes, setRecipes] = useState<Recipe[]>(MOCK_RECIPES);
  const [collapsed, setCollapsed] = useState(false);

  const renderItem = useCallback(
    ({ item }: { item: Recipe }) => {
      const layoutTransition = LinearTransition.delay(collapsed ? 0 : 50);

      return (
        <Animated.View
          layout={layoutTransition}
          style={[
            styles.card,
            collapsed && { height: COLLAPSED_HEIGHT },
          ]}
        >
          <View style={styles.titleRow}>
            <Animated.Text layout={layoutTransition} style={styles.title}>
              {item.title}
            </Animated.Text>
          </View>
          {!collapsed && (
            <View style={styles.setsContainer}>
              <View style={styles.headerRow}>
                <Text style={styles.headerText}>Ingredient</Text>
                <Text style={styles.headerText}>Quantity</Text>
              </View>
              {item.ingredients.map((ing, i) => (
                <View key={i} style={styles.setRow}>
                  <Text style={styles.setText}>{ing.name}</Text>
                  <Text style={styles.setText}>{ing.quantity}</Text>
                </View>
              ))}
              <TouchableOpacity
                style={styles.addButton}
                onPress={() => {
                  setRecipes((prev) =>
                    prev.map((r) =>
                      r.id === item.id
                        ? { ...r, ingredients: [...r.ingredients, { name: 'New ingredient', quantity: '0g' }] }
                        : r,
                    ),
                  );
                }}
              >
                <Text style={styles.addButtonText}>+ Add Ingredient</Text>
              </TouchableOpacity>
            </View>
          )}
        </Animated.View>
      );
    },
    [collapsed, recipes],
  );

  return (
    <DraxProvider>
      <DraxList
        data={recipes}
        keyExtractor={(item) => item.id}
        onReorder={({ data }) => setRecipes(data)}
        animationConfig="spring"
        renderItem={renderItem}
        itemDraxViewProps={{
          hoverDraggingStyle: { height: COLLAPSED_HEIGHT, overflow: 'hidden' },
        }}
        onDragStart={() => setCollapsed(true)}
        onDragEnd={() => setCollapsed(false)}
      />
    </DraxProvider>
  );
}

const styles = StyleSheet.create({
  card: {
    backgroundColor: '#f5f5f5',
    marginHorizontal: 8,
    marginVertical: 4,
    borderRadius: 8,
    overflow: 'hidden',
  },
  titleRow: {
    padding: 12,
    backgroundColor: '#e0e0e0',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
  },
  title: {
    fontSize: 16,
    fontWeight: '600',
  },
  setsContainer: {
    padding: 12,
  },
  headerRow: {
    flexDirection: 'row',
    marginBottom: 4,
  },
  headerText: {
    flex: 1,
    fontWeight: '500',
    color: '#666',
  },
  setRow: {
    flexDirection: 'row',
    paddingVertical: 4,
  },
  setText: {
    flex: 1,
  },
  addButton: {
    marginTop: 8,
    paddingVertical: 6,
    alignItems: 'center',
    backgroundColor: '#e0e0e0',
    borderRadius: 6,
  },
  addButtonText: {
    fontSize: 14,
    fontWeight: '500',
    color: '#36877F',
  },
});

```</div>

Metadata

Metadata

Assignees

No one assigned

    Labels

    DraxListSpecific to DraxList component functionalityenhancementNew feature or requestgesturesRelated to gesture handlinghelp wantedExtra attention is neededresearchFor issues which require additional research work

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions