Draggable Card
The Draggable Card component provides a powerful way to display and interact with cards, featuring drag and drop functionality. Built using dnd-kit, it offers a comprehensive solution for complex drag and drop interactions.
Variations
With actions
This example shows how to add interactive elements like close buttons to your draggable cards:
Grip outside
The draggable card supports placing the grip handle outside the card container using gripPosition="outside". This creates a cleaner card appearance with the drag handle positioned to the left of the card.
Sticky headers
Use the sticky and header props on DraggableCardList for the common pattern of a pinned header above a draggable list inside a scrollable drawer. These props wrap the list in a StickyListSection internally.
Contained mode
Use contained when the DraggableCardList is nested inside form input wrappers or other intermediate elements, but the drawer body is still the scroll container. Set --cn-sticky-container-padding on an ancestor to control how far the header bleeds horizontally.
Installation
import { DraggableCard, DraggableCardList, CardData } from '@harnessio/ui/components'
// Define your cardsconst [cards, setCards] = useState<CardData[]>([ { id: '1', title: 'First Card', description: 'This is the first card description', disableDragAndDrop: false // Optional: disable dragging for this card }, { id: '2', title: 'Second Card', description: 'This is the second card description' }])
// Use the DraggableCardList to render and manage the cardsreturn ( <DraggableCardList cards={cards} setCards={setCards} />)Both title and description accept React components, not just strings, so you can render richer content:
const [cards, setCards] = useState<CardData[]>([ { id: '1', title: ( <div className="flex items-center justify-between w-full"> <span>Card with Custom Title</span> <IconV2 name="settings" size="sm" /> </div> ), description: ( <div className="space-y-cn-xs"> <p>This card has a custom description component</p> <Button>Action Button</Button> </div> ) }])Anatomy
The draggable card system is two components working together:
// Individual card component<DraggableCard id="unique-id" title="Card Title" description="Card Description" disableDragAndDrop={false}/>
// List component that manages multiple cards<DraggableCardList cards={cardsArray} setCards={setCardsFunction}/>- DraggableCardList — the container you’ll usually reach for. It owns the drag context (dnd-kit’s
DndContext+SortableContext), renders each entry incardsas aDraggableCard, and callssetCardswith the new order when a drag ends.listGapcontrols the spacing between cards. - DraggableCard — a single Card with a grip handle. By default (
gripPosition="inside") the grip sits inline at the start of the title row;gripPosition="outside"moves it to the left of the card for a cleaner surface. SettingdisableDragAndDropon a card makes it static. - Grip handle — a
grip-dotsicon that shows a grab cursor; it’s the drag affordance dnd-kit’s pointer sensor listens to. - Sticky header — passing
headerrenders content above the list; addingstickywraps the whole thing in aStickyListSectionso the header pins while the list scrolls.
Best practices
Do
- Give every card a stable, unique
id— reordering relies on it. - Use
gripPosition="outside"when the card content is dense or already has its own actions, so the drag affordance stays distinct. - Reach for
sticky+headerwhen a reorderable list lives inside a scrollable drawer or panel and the group needs a label. - Set
disableDragAndDropon cards that must stay in place rather than hiding the grip yourself.
Don’t
- Don’t put the only copy of a critical action behind drag — provide buttons (like the remove button in the examples) for anything a keyboard or touch user must reach.
- Don’t nest draggable lists so deeply that it’s unclear which list an item belongs to.
Content
- Keep card titles short and scannable; a reorderable list is easier to manage when each row reads at a glance.
- Put secondary details in
description, and keep row actions (remove, settings) compact and clearly labeled with tooltips.
Accessibility
Reordering is powered by dnd-kit. The list is configured with a pointer sensor, so dragging works with mouse and touch.
Labeling
- The grip is an icon-only affordance. Any interactive content you place in a card (buttons, links) must carry its own accessible label — the examples use
tooltipPropson the remove buttons for this.
Focus & states
- While a card is being dragged it dims (reduced opacity) and lifts above its siblings, giving a clear visual cue for the item in motion.
Known limitations
- Reordering is pointer/touch-only. The list registers a pointer sensor but no keyboard sensor, so cards can’t be reordered from the keyboard. Where order matters for keyboard or assistive-technology users, provide an alternative way to change it (for example, up/down controls).
Properties
DraggableCard
The individual card component that can be dragged and reordered. It also accepts Card root props (such as size and className).
Prop | Required | Default | Type |
|---|---|---|---|
| id | true | string | |
| title | true | string | ReactNode | |
| description | false | string | ReactNode | |
| disableDragAndDrop | false | false | boolean |
| gripPosition | false | 'inside' | 'inside' | 'outside' |
DraggableCardList
A container component that manages a list of draggable cards.
Prop | Required | Default | Type |
|---|---|---|---|
| cards | true | CardData[] | |
| setCards | true | (newCards: CardData[]) => void | |
| header | false | ReactNode | |
| sticky | false | boolean | |
| contained | false | boolean | |
| listGap | false | 'md' | GapSize |
CardData
The interface for the card data objects used by the DraggableCardList component. It extends the Card root props (minus title / children).
interface CardData { id: string; title: string | React.ReactNode; description?: string | React.ReactNode; disableDragAndDrop?: boolean; gripPosition?: "inside" | "outside"; // ...plus Card root props such as size, className}