# vue-kanban: a drag and drop board component for Vue 2 with an xstate option

> A small Vue plugin that renders a kanban board from two arrays, moves blocks with dragula, optionally restricts transitions with a dragula accept callback or an xstate machine, and emits an update-block event rather than mutating your data.

**BrockReece/vue-kanban** — A vue based drag and drop kanban board 

- Repository: https://github.com/BrockReece/vue-kanban
- Stars: 825 · Forks: 153
- Language: JavaScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/brockreece-vue-kanban

## Two arrays in, one component out

vue-kanban is a Vue plugin that installs a single global component, `kanban-board`, and the whole contract is two required props: `stages`, an array of stage names, and `blocks`, an array of objects describing the cards. The component is registered globally when the plugin is installed, so it can be used in a template without an explicit import.

The data shape is deliberately plain. A block carries an `id`, a `status` matching one of the stage strings, and a `title`:

```js
{
  stages: ['on-hold', 'in-progress', 'needs-review', 'approved'],
  blocks: [
    {
      id: 1,
      status: 'on-hold',
      title: 'Test',
    },
  ],
}
```

Because `status` is a string drawn from the `stages` array rather than a stage index, the board stays correct when stages are reordered, and the same string is what the move event reports back.

The other notable property of the design is that the component does not own the data. It renders what you pass and emits an event when a block moves, leaving your application to update its own array. That is the right default for Vue, and it also means the board can be driven from a store, a server payload, or a computed list without any adapter.

## Dragging is dragula, and the guard is a dragula option

The drag and drop behaviour comes from dragula, which is a runtime dependency of the plugin along with Vue and xstate. The component passes its `config` prop straight through as dragula options, and the documentation points at the dragula options documentation for the details.

The example given is the case that motivates the whole feature. A block in the `approved` stage should not be draggable out of it, and dragula's `accepts` callback expresses exactly that:

```js
{
  config: {
    // Don't allow blocks to be moved out of the approved stage
    accepts(block, target, source) {
      return source.dataset.status !== 'approved',
    }
  }
}
```

The mechanism is dragula's own: the element carries its stage in a `data-status` attribute and the callback reads it from the source element. This is a thin, honest integration. Rather than reimplementing drag behaviour, the component stands up a dragula container per stage and passes your options through.

The practical implication is that every dragula option is available to you, including the ones that have nothing to do with restrictions: drop target behaviour, mirroring, sorting and so on. If you know dragula, you already know how to configure this component. If you do not, the accept callback is the one piece of vocabulary worth learning.

## Moves come back as an event, and you update your own array

The component emits an event when a block is moved, and the documented wiring looks like this:

```html
<kanban-board :stages="stages" :blocks="blocks" @update-block="updateBlock"></kanban-board>
<script>
...
  methods: {
    updateBlock(id, status) {
      this.blocks.find(b => b.id === Number(id)).status = status;
    },
  },
...
</script>
```

Two details in that snippet matter. The event carries an `id` and a `status`, and the handler casts the id with `Number(id)` before comparing it, which suggests the id arrives as a string even though the data holds numbers. If your ids are strings, that cast is the first thing to remove.

The handler mutates the array in place by finding the block and setting its status. In Vue 2 that is enough to trigger a re-render, because setting a property on a reactive object is detected by the reactivity system (array index assignment would not be). It is worth knowing that the board does not sort or reorder the array for you; if you want a specific ordering of blocks within a stage, that is your sort to write.

This event-driven shape also means the component is straightforward to test and to sync with a backend. You get a hook with exactly the two facts that changed, and everything else about the board is derived from props.

## An xstate config can own the allowed moves

The second way to restrict movement is a state machine. The documentation says vue-kanban is compatible with xstate state machines and that you can pass an xstate config as a prop, in which case the board uses the state machine to restrict which moves are allowed.

The example config is a workflow with four states and named events:

```js
stateMachineConfig: {
  id: 'kanban',
  initial: 'on-hold',
  states: {
    'on-hold': {
      on: {
        PICK_UP: 'in-progress',
      },
    },
    'in-progress': {
      on: {
        RELINQUISH_TASK: 'on-hold',
        PUSH_TO_QA: 'needs-review',
      },
    },
    'needs-review': {
      on: {
        REQUEST_CHANGE: 'in-progress',
        PASS_QA: 'approved',
      },
    },
    approved: {
      type: 'final',
    },
  },
},
```

The value of this route is that the workflow becomes data. The same config that describes which transitions are legal can be rendered, tested and shared with whatever else governs the process, and xstate gives you a machine you can send events to rather than a set of conditionals scattered across a component.

There is a wrinkle in the documentation. The advanced props section names the prop `state-machine-config`, while the example object literal uses the key `stateMachineConfig`. One of the two is what the component actually reads, and the documentation shown does not settle it. Before you rely on the xstate path, check the component's prop definition in `src/` and use the name it expects.

Also note that having both mechanisms available raises a design question rather than answering it. The dragula `accepts` callback and the xstate config can both express a rule like nothing leaves `approved`, and if they disagree the board will behave in whichever way the code resolves it. Decide which one owns the rules.

## Blocks and stages are both slots, so you own the markup

Each block has a named slot, and the documentation shows how to extend it from the parent component. The stage slots and the block slots are both addressed by name, stages by their stage string and blocks by their id:

```html
<kanban-board :stages="stages" :blocks="blocks" @update-block="updateBlock">
  <div v-for="stage in stages" :slot="stage" :key="stage">
    <h2>{{ stage }}</h2>
  </div>
  <div v-for="block in blocks" :slot="block.id" :key="block.id">
    <div>
      <strong>id:</strong> {{ block.id }}
    </div>
    <div>
      {{ block.title }}
    </div>
  </div>
</kanban-board>
```

So the header of each column and the entire body of each card are yours to render. That is what makes the component usable with a design system rather than only with its own default look.

Styling is handled the same way. The repository ships an scss stylesheet described as a starting point that can be used in your project, imported from a style block:

```html
<style lang="scss">
  @import './assets/kanban.scss';
</style>
```

Note that the path assumes you copy the file into your own assets directory. The point of the starting stylesheet is that it is a starting point, not a theme, so plan on replacing it.

## Installing it, and the Vue 2 constraint that comes with it

There are two installation routes. Through the Vue CLI the plugin is added with a single command:

```bash
vue add vue-cli-plugin-kanban
```

Or manually, the package is added with npm:

```bash
npm install vue-kanban
```

or with yarn:

```bash
yarn add vue-kanban
```

Either way, the plugin itself has to be registered in your entry file:

```js
import vueKanban from 'vue-kanban'

Vue.use(vueKanban)
```

After that the component is available globally, and the first usage is the one-liner template:

```html
<kanban-board :stages="stages" :blocks="blocks"></kanban-board>
```

The constraint to notice before you plan a project around this is the Vue version. The runtime dependencies are dragula at ^3.7.2, vue at ^2.2.6 and xstate at ^4.6.7, so this is a Vue 2 plugin, and the package version is 1.8.0. The repository itself is a Vue CLI project with a webpack based build (`build/dev-server.js` for `dev` and `start`, `build/build.js` for `build`), a `.babelrc`, a `.postcssrc.js` and an `.eslintrc.js`.

For a Vue 3 application this is a dealbreaker. For a Vue 2 application that already exists, it is a reasonable option, and the effort of dropping it in is small.

## Testing, tooling and the state of the repository

The package.json carries a full testing setup, which tells you about the project's era. The `test` script runs `jest -i`, the `unit` script runs Karma with `cross-env BABEL_ENV=test` against `test/unit/karma.conf.js` in single run mode, and `lint` runs ESLint over `src` and `test/unit/specs` with the `.js` and `.vue` extensions. Dev dependencies include `jest-vue`, `jest-serializer-vue`, `chai` and `inject-loader`, which is the standard stack for testing Vue components in this generation of tooling.

The licence is MIT, with the LICENSE file at the repository root.

On maintenance, the repository is not archived, and the last push was on 2026-04-14. There are no GitHub releases, so the version to track is the npm version, currently 1.8.0 in package.json. The build badge in the documentation points at Travis CI, which is a build service that has changed ownership and status since then, so a green or red badge in the README is not a reliable signal of current health.

The gaps to keep in mind when you evaluate this are concrete rather than abstract. The dependency on Vue 2 rules out a Vue 3 codebase. The prop naming discrepancy between `state-machine-config` and `stateMachineConfig` needs checking. The xstate integration is described as compatibility rather than as a documented guarantee of how it interacts with the dragula accept callback. And the stylesheet is a starting point you will replace. For a small internal tool on Vue 2, none of these are blockers. For a product, the prop naming question is the one to resolve first.

## Conclusion

Adopt vue-kanban if you are on Vue 2 and need a working kanban board in an afternoon, particularly if you want xstate to own the allowed transitions rather than hand written guards. Do not adopt it for Vue 3, since its runtime dependency is Vue 2, and do not adopt it for a board that must persist its own state, because the component emits changes rather than owning them. Verify first that the dragula accept callback and the xstate config agree on the same transition rules, since the documentation presents them as separate mechanisms and only one of them can be authoritative for a given move.

## FAQ

### How do I install vue-kanban in a Vue project?

Through the Vue CLI with `vue add vue-cli-plugin-kanban`, or manually with `npm install vue-kanban` or `yarn add vue-kanban`, followed by registering the plugin in your entry file with `import vueKanban from 'vue-kanban'` and `Vue.use(vueKanban)`.

### What props does the kanban-board component require?

Two required props, stages as an array of stage names and blocks as an array of block objects. Each block carries an id, a status matching one of the stage strings, and a title.

### How do I prevent blocks from being moved out of a stage in vue-kanban?

Pass dragula options through the config prop and use the accepts callback, for example checking that the source element's data-status is not approved. The component passes the config straight to dragula.

### How does vue-kanban report that a block was moved?

The component emits an update-block event carrying the block id and the new status. The documented handler finds the block and sets its status, casting the id with Number(id) before the comparison.

### Can xstate control which moves are allowed on a vue-kanban board?

Yes. You can pass an xstate config as a prop and the board uses the state machine to restrict which moves are allowed, with states and named events such as PICK_UP and PUSH_TO_QA. The documentation names the prop state-machine-config while the example uses stateMachineConfig, so check the component definition in src/ before relying on it.

### Does vue-kanban work with Vue 3?

No. The package lists vue at ^2.2.6 among its runtime dependencies, so it is a Vue 2 plugin, and the current npm version is 1.8.0.

## Sources

- [BrockReece/vue-kanban on GitHub](https://github.com/BrockReece/vue-kanban)
- [Issues](https://github.com/BrockReece/vue-kanban/issues)
- [License: MIT](https://github.com/BrockReece/vue-kanban/blob/master/LICENSE)
- [README](https://github.com/BrockReece/vue-kanban/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/brockreece-vue-kanban
