UIPackage
Menu

Framework

Change language

Boilerplate repo

Board — compositional kanban / sortable-list primitive

boardui

Six small composable components for any board / kanban / sortable-list surface — opinionated about drop targeting and animation, agnostic about layout, data, and chrome. Drop `<Board>` around a grid of `<BoardLane>` columns; slot `<BoardLaneHeader>`, `<BoardLaneBody>`, `<BoardLaneEmpty>`, and `<BoardCard>` items inside each lane. State lives in the sibling `useBoard()` hook (insertion-index drop math, keyboard a11y, accept predicate). Animation comes from the `motion-list` motion preset by default — enter / leave / move all share one settle curve so a card travelling between two lanes reads as one continuous motion. Mirrors the Timeline / Card sub-component pattern (three-level context injection: board → lane → card). The current monolithic `@uipkge/kanban-board` block can be rebuilt on top of this primitive — Board is the layer underneath, kanban-board is one opinionated assembly.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/board.json
Named registry:npx shadcn-vue@latest add @uipkge/boardInstalls to:app/components/ui/board/

Examples

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
tone
'default''plain'
defaultoptional
state
'idle''over''rejecting'
idleoptional
classHTMLAttributes['class']optional
orientationBoardOrientation'horizontal'optional
densityBoardDensity'default'optional
motion

TransitionGroup name applied by BoardLaneBody. Defaults to `motion-list`.

string'motion-list'optional
accepts

Predicate run per drop. Defaults to always-accept.

BoardAcceptsFn() => () => true, moveItem: () => () => {}, draggingIds: …optional
moveItem

Imperative move from a parent's useBoard composable.

(itemId: string | string[], toLaneId: string, toIndex?: number) => voidoptional
draggingId

External state — pass `state.draggingId` etc. from useBoard.

string | nulloptional
draggingIdsreadonly string[]optional
dragOverLaneIdstring | nulloptional
justMovedIdstring | nulloptional
selectedIdsReadonlySet<string>optional
toggleSelection

consumers that don't wire selection.

(itemId: string, additive?: boolean) => voidoptional
clearSelection() => voidoptional
registerAllowedLanes(cardId: string, lanes: readonly string[] | undefined) => voidoptional
unregisterAllowedLanes(cardId: string) => voidoptional
registerLaneDisabled(laneId: string, disabled: boolean) => voidoptional
unregisterLaneDisabled(laneId: string) => voidoptional
isLaneAcceptingFor(laneId: string) => booleanoptional

Schema

Type aliases from this item's source — use them to shape the data you pass in.

BoardDropEvent
interface BoardDropEvent {
  /** First (anchor) item moved. For multi-item drops, see `itemIds`. */
  itemId: string
  /** All items moved in this drop, in their final visual order. */
  itemIds: string[]
  from: string
  to: string
  /** Insertion index of the first item in the target lane. */
  index: number
}
BoardContext
interface BoardContext {
  orientation: Ref<BoardOrientation>
  density: Ref<BoardDensity>
  /** TransitionGroup name. Defaults to `motion-list` (the @uipkge motion preset). */
  motion: Ref<string>
  /** Currently-grabbed primary card id (pointer drag OR keyboard grab). */
  draggingId: Ref<string | null>
  /** All cards being dragged this turn — usually `[draggingId]`, but
   *  expands to the full selection when the user grabs one of a multi-
   *  selected set. Lanes read this to compute drop math (the dragged
   *  cards are excluded from the insertion-index walk). */
  draggingIds: Ref<readonly string[]>
  /** Lane currently under the pointer/keyboard cursor. */
  dragOverLaneId: Ref<string | null>
  /** Card that just landed — used by consumers for a momentary highlight. */
  justMovedId: Ref<string | null>
  /** Multi-select set. Click-and-drag any selected card moves the whole
   *  set; click a non-selected card to drag that one alone. */
  selectedIds: Ref<ReadonlySet<string>>
  /** Predicate run by lanes; defaults to always-accept. */
  accepts: Ref<BoardAcceptsFn>
  /** Imperative move — used by keyboard handlers + external callers. */
  moveItem: (itemId: string | string[], toLaneId: string, toIndex?: number) => void
  /** Toggle one item in the selection set (or replace it if `additive` is false). */
  toggleSelection: (itemId: string, additive?: boolean) => void
  /** Drop the entire selection. */
  clearSelection: () => void
  /** Per-card allow-list registry. BoardCard registers its own `allowedLanes`
   *  on mount; lanes consult this to short-circuit rejected drops without
   *  the consumer having to encode the rule inside `accepts`. */
  registerAllowedLanes: (cardId: string, lanes: readonly string[] | undefined) => void
  unregisterAllowedLanes: (cardId: string) => void
  /** Whether the lane's drops are accepted for the dragging item, given
   *  the current `accepts` + per-card allowedLanes + lane disabled state. */
  isLaneAcceptingFor: (laneId: string) => boolean
  /** Lane registration (mirrors Timeline pattern). */
  laneIds: Ref<symbol[]>
  registerLane: (id: symbol) => void
  unregisterLane: (id: symbol) => void
  /** Lane disabled registry — lanes call register/unregister; useBoard +
   *  isLaneAcceptingFor read from it. */
  registerLaneDisabled: (laneId: string, disabled: boolean) => void
  unregisterLaneDisabled: (laneId: string) => void
}
BoardLaneContext
interface BoardLaneContext {
  laneId: Ref<string>
  isDragOver: Ref<boolean>
  isAccepting: Ref<boolean>
  disabled: Ref<boolean>
}
BoardCardContext
interface BoardCardContext {
  cardId: Ref<string>
  laneId: Ref<string>
  isDragging: Ref<boolean>
  isJustMoved: Ref<boolean>
  isSelected: Ref<boolean>
  disabled: Ref<boolean>
}

npm dependencies

Used by

Files installed (9)

  • app/components/ui/board/Board.vue3.8 kB
    <script setup lang="ts">
    import { provide, ref, toRef, type Ref } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    import { BOARD_CONTEXT, type BoardAcceptsFn, type BoardDensity, type BoardOrientation } from './context'
    
    interface Props {
      class?: HTMLAttributes['class']
      orientation?: BoardOrientation
      density?: BoardDensity
      /** TransitionGroup name applied by BoardLaneBody. Defaults to `motion-list`. */
      motion?: string
      /** Predicate run per drop. Defaults to always-accept. */
      accepts?: BoardAcceptsFn
      /** Imperative move from a parent's useBoard composable. */
      moveItem?: (itemId: string | string[], toLaneId: string, toIndex?: number) => void
      /** External state — pass `state.draggingId` etc. from useBoard. */
      draggingId?: string | null
      draggingIds?: readonly string[]
      dragOverLaneId?: string | null
      justMovedId?: string | null
      selectedIds?: ReadonlySet<string>
      /** Optional toggle/clearSelection from useBoard so the primitive can
       *  expose selection mutations through context (consumed by BoardCard
       *  click handler). Both default to no-ops, so Board still works with
       *  consumers that don't wire selection. */
      toggleSelection?: (itemId: string, additive?: boolean) => void
      clearSelection?: () => void
      registerAllowedLanes?: (cardId: string, lanes: readonly string[] | undefined) => void
      unregisterAllowedLanes?: (cardId: string) => void
      registerLaneDisabled?: (laneId: string, disabled: boolean) => void
      unregisterLaneDisabled?: (laneId: string) => void
      isLaneAcceptingFor?: (laneId: string) => boolean
    }
    
    const props = withDefaults(defineProps<Props>(), {
      orientation: 'horizontal',
      density: 'default',
      motion: 'motion-list',
      accepts: () => () => true,
      moveItem: () => () => {},
      draggingIds: () => [] as readonly string[],
      selectedIds: () => new Set<string>() as ReadonlySet<string>,
      toggleSelection: () => () => {},
      clearSelection: () => () => {},
      registerAllowedLanes: () => () => {},
      unregisterAllowedLanes: () => () => {},
      registerLaneDisabled: () => () => {},
      unregisterLaneDisabled: () => () => {},
      isLaneAcceptingFor: () => () => true,
    })
    
    const draggingIdRef = toRef(props, 'draggingId')
    const draggingIdsRef = toRef(props, 'draggingIds')
    const dragOverLaneIdRef = toRef(props, 'dragOverLaneId')
    const justMovedIdRef = toRef(props, 'justMovedId')
    const selectedIdsRef = toRef(props, 'selectedIds')
    const laneIds = ref<symbol[]>([])
    
    provide(BOARD_CONTEXT, {
      orientation: toRef(props, 'orientation'),
      density: toRef(props, 'density'),
      motion: toRef(props, 'motion'),
      draggingId: draggingIdRef as unknown as Ref<string | null>,
      draggingIds: draggingIdsRef as unknown as Ref<readonly string[]>,
      dragOverLaneId: dragOverLaneIdRef as unknown as Ref<string | null>,
      justMovedId: justMovedIdRef as unknown as Ref<string | null>,
      selectedIds: selectedIdsRef as unknown as Ref<ReadonlySet<string>>,
      accepts: toRef(props, 'accepts'),
      moveItem: (...args) => props.moveItem!(...args),
      toggleSelection: (...args) => props.toggleSelection!(...args),
      clearSelection: () => props.clearSelection!(),
      registerAllowedLanes: (...args) => props.registerAllowedLanes!(...args),
      unregisterAllowedLanes: (...args) => props.unregisterAllowedLanes!(...args),
      registerLaneDisabled: (...args) => props.registerLaneDisabled!(...args),
      unregisterLaneDisabled: (...args) => props.unregisterLaneDisabled!(...args),
      isLaneAcceptingFor: (laneId) => props.isLaneAcceptingFor!(laneId),
      laneIds,
      registerLane: (id) => {
        if (!laneIds.value.includes(id)) laneIds.value.push(id)
      },
      unregisterLane: (id) => {
        laneIds.value = laneIds.value.filter((i) => i !== id)
      },
    })
    </script>
    
    <template>
      <div data-uipkge data-slot="board" :data-orientation="orientation" :class="cn('w-full', props.class)">
        <slot />
      </div>
    </template>
    
  • app/components/ui/board/BoardLane.vue3.2 kB
  • app/components/ui/board/BoardLaneHeader.vue0.3 kB
  • app/components/ui/board/BoardLaneBody.vue1.6 kB
  • app/components/ui/board/BoardLaneEmpty.vue0.6 kB
  • app/components/ui/board/BoardCard.vue7.3 kB
  • app/components/ui/board/board.variants.ts2 kB
  • app/components/ui/board/context.ts3.6 kB
  • app/components/ui/board/index.ts0.7 kB

Raw manifest:https://uipkge.dev/r/vue/board.json