story-map
Horizontal user story map with activity columns and drag-and-drop between columns.
Overview
A web component that renders a horizontal user story map organized by activity columns. Each column contains a <drag-surface> enabling drag-and-drop reorder within a column and transfer between columns. Ideal for sprint planning, backlog grooming, and mapping user activities to deliverable stories.
<story-map> <section data-activity="discovery" data-activity-label="Discovery"> <article draggable="true" data-id="PROJ-201">Interview target users</article> <article draggable="true" data-id="PROJ-202">Competitive analysis</article> <article draggable="true" data-id="PROJ-203">Survey existing customers</article> </section> <section data-activity="design" data-activity-label="Design"> <article draggable="true" data-id="PROJ-204">Create wireframes</article> <article draggable="true" data-id="PROJ-205">Design component library</article> </section> <section data-activity="development" data-activity-label="Development"> <article draggable="true" data-id="PROJ-206">Build API endpoints</article> <article draggable="true" data-id="PROJ-207">Implement dashboard UI</article> <article draggable="true" data-id="PROJ-208">Add keyboard shortcuts</article> </section> <section data-activity="testing" data-activity-label="Testing"> <article draggable="true" data-id="PROJ-209">Write integration tests</article> <article draggable="true" data-id="PROJ-210">User acceptance testing</article> </section></story-map>
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
src |
string (URL) | — | Path to a JSON file containing activity and story data |
compact |
boolean | — | Reduces column widths, padding, and header font sizes for denser layouts |
title |
string | — | Optional heading displayed above the story map |
Child Attributes
The <story-map> component expects direct <section> children, each defining an activity column. Story items go inside each section.
Section Attributes (Activity Columns)
| Attribute | Type | Default | Description |
|---|---|---|---|
data-activity |
string | — | Activity column identifier. Used in event details and as the internal key. |
data-activity-label |
string | Title-cased data-activity |
Display label for the column header. Falls back to a title-cased version of the activity ID. |
data-journey-phase |
string | — | Optional link to a <user-journey> phase name for cross-referencing |
Item Attributes (Story Cards)
Items inside each section become draggable story cards. Use <article> elements for simple text or <user-story> elements for rich story cards.
| Attribute | Type | Description |
|---|---|---|
draggable |
boolean | Must be set to "true" for drag capability |
data-id |
string | Stable identifier for the item, included in event details |
Column Structure
Each activity column consists of a sticky header and a drag surface area:
- Header — displays the activity label in uppercase with a count badge showing the number of stories in that column. The badge updates automatically when items are transferred.
- Drag surface — a
<drag-surface>element that holds the story items. All surfaces share the samegroupidentifier, enabling cross-column transfers. - Empty state — when a column has no stories, an italic "No stories yet" placeholder appears. The placeholder is removed when an item is dropped in, and re-added when the last item is dragged out.
The columns sit inside a horizontally scrollable container. Each column has a minimum width of 14rem (11rem in compact mode) and a maximum of 22rem, using flex: 1 0 14rem to fill available space while allowing horizontal scroll when columns exceed the viewport.
Drag Interaction
Items can be reordered within a column and transferred between columns:
- Reorder — drag an item up or down within the same column. Fires a
story-map:reorderevent with the old and new indices. - Transfer — drag an item from one column and drop it onto another. The count badges on both columns update automatically. Fires a
story-map:transferevent. - Keyboard —
<drag-surface>provides keyboard reorder and cross-surface transfer using its built-in keyboard shortcuts.
const map = document.querySelector('story-map'); map.addEventListener('story-map:transfer', (e) => { console.log(`${e.detail.itemId} moved from ${e.detail.fromActivity} to ${e.detail.toActivity}`);}); map.addEventListener('story-map:reorder', (e) => { console.log(`${e.detail.itemId} reordered in ${e.detail.activity}: ${e.detail.oldIndex} -> ${e.detail.newIndex}`);});
JSON Loading
Load the entire story map from a JSON file using the src attribute. The component creates activity columns with <user-story> cards automatically.
<story-map src="/data/story-map.json"></story-map>
{ "activities": [ { "id": "discovery", "label": "Discovery", "journeyPhase": "awareness", "stories": [ { "id": "PROJ-101", "title": "User interviews", "persona": "Researcher", "action": "interview 10 target users", "priority": "high", "status": "done", "points": "5" } ] }, { "id": "design", "label": "Design", "stories": [ { "id": "PROJ-201", "title": "Dashboard wireframes", "persona": "Designer", "action": "create wireframes for new dashboard", "priority": "high", "status": "in-progress", "points": "5" } ] } ]}
Using with User Stories
For richer story cards, use <user-story> elements as children instead of plain <article> elements. Add the compact attribute on each story for a tighter layout within the columns.
<story-map> <section data-activity="discovery" data-activity-label="Discovery"> <user-story draggable="true" data-id="PROJ-201" story-id="PROJ-201" persona="Sarah Chen" action="interview target users about dashboard needs" priority="high" status="in-progress" points="3" compact> </user-story> </section> <section data-activity="development" data-activity-label="Development"> <user-story draggable="true" data-id="PROJ-206" story-id="PROJ-206" persona="Alex Rivera" action="build API endpoints for timeline data" priority="high" status="to-do" points="8" compact> </user-story> </section></story-map>
Events
| Event | Detail | When |
|---|---|---|
story-map:reorder |
{ itemId, activity, oldIndex, newIndex } |
Fires when an item is reordered within the same activity column |
story-map:transfer |
{ itemId, fromActivity, toActivity, newIndex } |
Fires when an item is dragged from one activity column to another |
story-map:ready |
{ activityCount, storyCount } |
Fires after the component initializes with the total number of activity columns and stories |
Accessibility
- The scroll container uses
role="region"witharia-label="Story map"and is keyboard-focusable viatabindex="0" - Each
<drag-surface>has anaria-labeldescribing the activity column (e.g., "Discovery stories") - A hidden live region with
aria-live="polite"announces item transfers between columns - Keyboard navigation is provided by the underlying
<drag-surface>components - Column headers are sticky so the activity label remains visible while scrolling down through long columns
- Respects
prefers-reduced-motion: reduceby disabling scroll behavior transitions - Uses
container-type: inline-sizeand stacks columns vertically at narrow viewports (<30rem) - Print styles remove sticky positioning, enable wrapping columns, and set
break-inside: avoidon columns
Related
<user-story>— Rich story cards that work as draggable items in columns<impact-effort>— Prioritization matrix for triaging stories<empathy-map>— Empathy maps for user research informing stories<user-journey>— Journey maps linked viadata-journey-phase<user-persona>— Persona cards providing context for story owners<drag-surface>— The underlying drag-and-drop primitive used by each column- UX Planning Pack — loads all six UX components together