# Dependencies and links between events

> Draw finish-to-start and other links between events from your data, style them, react to clicks, and enforce dependency rules in your application's own code.

Source: https://superscheduler.org/en/docs/links-dependencies/
Reviewed: 2026-10-07

Pass links as an array of { id, from, to, type } objects, where from and to are event ids; FinishToStart is the default type and curved lines are the default drawing. Style links with color, width, style and cssClass or the --super-scheduler-link tokens, and handle clicks with onLinkClick. The library draws links but does not enforce them: check dependencies in onEventMoving and onEventMove. Creating links by dragging between events is reserved and not implemented in 0.1.0.

Production steps, shoots and campaigns depend on each other: cutting starts when design ends, two crews start together. SuperScheduler draws those dependencies as links between events and keeps them attached while users scroll, zoom and drag. What a dependency means for scheduling (whether a move is allowed, whether successors shift) stays in your code, where your rules live.

Links need SuperScheduler Pro.

## Draw links from data
A link is a plain object in the `links` prop. `from` and `to` are event ids (the `id` of `EventData`); `id` identifies the link itself.

```ts
// src/links.ts
import type { SuperScheduler } from 'super-scheduler'

// `from` and `to` are event ids. Links between ids that are not loaded are not drawn.
export const LINKS: SuperScheduler.LinkData[] = [
  // Cutting can start once design has finished (the default type).
  { id: 'design-cut', from: 'design', to: 'cutting', type: 'FinishToStart' },
  // Painting and drying start together.
  { id: 'paint-dry', from: 'painting', to: 'drying', type: 'StartToStart', style: 'dashed' },
  // Inspection and packing finish together; drawn below the events, in a fixed color.
  {
    id: 'qa-pack',
    from: 'inspection',
    to: 'packing',
    type: 'FinishToFinish',
    color: '#7c3aed',
    layer: 'Below',
  },
  // A heavier line, styled further by CSS through its class.
  { id: 'ship', from: 'packing', to: 'shipping', width: 3, cssClass: 'link--critical' },
]

export const LINK_OPTIONS = {
  linkShape: 'Curved', // default; 'RightAngled' draws segments
  linkBottomMargin: 12, // clearance for links that run below events (8 px minimum for curves)
  linkLayer: 'Above', // default layer of links without their own `layer`
  links: LINKS,
} satisfies SuperScheduler.SchedulerConfig
```
You should see four lines: a solid curve from design to cutting, a dashed one between painting and drying, a violet one under the inspection and packing events, and a thicker one into shipping.

Links come from your data model: a `dependsOn` column, a table of dependencies, or a rule such as "same order, next step". Derive the array with `useMemo` so its identity changes only when the data does. From code, `control.links` offers `list`, `add(link)`, `update(link)`, `remove(link or id)`, `find(id)`, `findByFromTo(from, to)` and `findAllByFromTo(from, to)`; with React state, prefer updating your array.

## Link types
| `type` | Drawn from | To | Typical meaning |
|---|---|---|---|
| `'FinishToStart'` (default) | the end of `from` | the start of `to` | `to` starts after `from` finishes |
| `'StartToStart'` | the start of `from` | the start of `to` | both start together, or `to` after `from` starts |
| `'FinishToFinish'` | the end of `from` | the end of `to` | both finish together |
| `'StartToFinish'` | the start of `from` | the end of `to` | `to` finishes when `from` starts |

The type changes where the line attaches and how it is drawn. It does not change what users may do; see [enforce dependencies](#validation).

## Shape and routing
| Option | Default | Effect |
|---|---|---|
| `linkShape` | `'Curved'` | Smooth curves with a small ring at the origin and an arrow head. `'RightAngled'` draws straight segments. |
| `linkBottomMargin` | `17` | Clearance for links that pass below events (at least 8 px for curves) |
| `linkLayer` | `'Above'` | Default layer for links without their own `layer`: above or below the events |
| `linkWidth` | `'Auto'` | Default stroke width in pixels (curves are 2 px) |

Links between ends on the same side bow outward with a bounded reach, so links to distant rows do not create oversized loops. Backward links use the gap between rows, and run below both events when those rows overlap. The routes avoid the two linked events, not every event in between: this is not an obstacle-avoidance router.

## Style links
Per link, `color`, `width`, `style` (`'solid'`, `'dashed'`, `'dotted'`), `layer` and `cssClass` change one link; an explicit `width` wins over the CSS width token. For the rest, use tokens on any ancestor or on a link class:

```css
.planning {
  --super-scheduler-link: #8b5cf6;
  --super-scheduler-link-hover: #6d28d9;
  --super-scheduler-link-width: 2;
}
.dark .planning {
  --super-scheduler-link: #b89afa;
  --super-scheduler-link-hover: #d8c6ff;
}
/* A class from your data or from onBeforeLinkRender */
.link--late {
  --super-scheduler-link: var(--super-scheduler-danger);
}
```

Links are SVG elements with the class `super-scheduler__link` and `data-super-scheduler-part="link"`; the arrow and origin ring take the link's color.

To style from the state of the linked events, use `onBeforeLinkRender`. It receives a copy of the link as `args.data`, plus the two events as `args.from` and `args.to`. Change `color`, `width`, `style`, `cssClass`, `layer` or `type` on the copy; the ends cannot change. The hook runs once per drawn link and again only when one of its events changes appearance.

## When links are drawn
A link is drawn when both events are loaded and laid out in the same section of the grid (the scrolling rows, or the same frozen top or bottom rows). It is not drawn when:

- either id is not loaded, for example outside the range you fetched;
- an end is hidden by a filter or sits in a collapsed tree row;
- the zoom level draws an end as a thin overview bar;
- it lies entirely outside the rendered area (it reappears when scrolled into view).

While an event is dragged, its links follow the preview with the same routing.

## Click, hover and menus
- `onLinkClick` runs on a click with `args.link` (a `SuperScheduler.Link`: `data`, `id()`, `from()`, `to()`, `type()`) and `args.originalEvent`; `args.preventDefault()` skips `onLinkClicked`, which runs after it.
- `linkBubble` shows a link's `bubbleHtml` on hover.
- `contextMenuLink`, or a link's own `contextMenu`, opens on right click.

Links take pointer input only when one of these is configured, so by default they never get in the way of event drags. Their invisible hit area is wider than the visible line.

## Enforce dependencies in your application
Links are drawings. Nothing stops a user from moving cutting before design ends unless your code checks it. Write the rule once as a plain function:

```ts
// src/finishToStart.ts
import { SuperScheduler } from 'super-scheduler'

export interface Bounds {
  /** Ticks of the civil wall clock, as SuperScheduler.Date.getTime() returns them. */
  readonly start: number
  /** Exclusive end. */
  readonly end: number
}

export const boundsOf = (event: SuperScheduler.EventData): Bounds => ({
  start: new SuperScheduler.Date(event.start).getTime(),
  end: new SuperScheduler.Date(event.end).getTime(),
})

/**
 * Finish-to-start rule: a task may not start before each of its predecessors ends.
 * Returns the first link that `proposed` times for `taskId` would break, or null.
 * Other link types need rules of their own; links only draw what the data says.
 */
export function brokenFinishToStart(
  taskId: SuperScheduler.EventId,
  proposed: Bounds,
  links: readonly SuperScheduler.LinkData[],
  find: (id: SuperScheduler.EventId) => Bounds | null,
): SuperScheduler.LinkData | null {
  for (const link of links) {
    if ((link.type ?? 'FinishToStart') !== 'FinishToStart') continue
    if (link.to === taskId) {
      const predecessor = find(link.from)
      if (predecessor !== null && proposed.start < predecessor.end) return link
    } else if (link.from === taskId) {
      const successor = find(link.to)
      if (successor !== null && successor.start < proposed.end) return link
    }
  }
  return null
}

/** Ids of the links the current data already breaks, to draw them differently. */
export function lateLinks(
  links: readonly SuperScheduler.LinkData[],
  events: readonly SuperScheduler.EventData[],
): ReadonlySet<SuperScheduler.LinkData> {
  const byId = new Map(events.map((event) => [event.id, boundsOf(event)]))
  const late = new Set<SuperScheduler.LinkData>()
  for (const link of links) {
    const from = byId.get(link.from)
    const to = byId.get(link.to)
    if ((link.type ?? 'FinishToStart') === 'FinishToStart' && from && to && to.start < from.end)
      late.add(link)
  }
  return late
}
```
Then call it from the hooks that run during and before a change:

```tsx
// src/ProductionPlan.tsx
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SchedulerProps, SuperScheduler } from 'super-scheduler'
import { boundsOf, brokenFinishToStart, lateLinks } from './finish-to-start'
import type { Bounds } from './finish-to-start'

export function ProductionPlan(props: {
  resources: SuperScheduler.ResourceData[]
  initialTasks: SuperScheduler.EventData[]
  /** Dependencies come from your data: the library draws them, it does not invent them. */
  initialLinks: SuperScheduler.LinkData[]
}) {
  const [tasks, setTasks] = useState(props.initialTasks)
  const [links, setLinks] = useState(props.initialLinks)
  const owned = useMemo(() => tasks.slice(), [tasks])

  // Links the data already breaks get a class; the color comes from CSS.
  const drawnLinks = useMemo(() => {
    const late = lateLinks(links, tasks)
    return links.map((link) => (late.has(link) ? { ...link, cssClass: 'link--late' } : link))
  }, [links, tasks])

  const onEventsChange = useCallback(
    (args: SchedulerEventsChangeArgs) => setTasks([...args.events]),
    [],
  )

  const config = useMemo<SchedulerProps>(() => {
    // The other tasks as the control holds them right now.
    const finder =
      (control: SuperScheduler.SchedulerApi) =>
      (id: SuperScheduler.EventId): Bounds | null => {
        const event = control.events.find(id)
        return event === null ? null : boundsOf(event.data)
      }
    return {
      linkShape: 'Curved',
      // While dragging: refuse and explain, frame by frame.
      onEventMoving(args) {
        const proposed = { start: args.start.getTime(), end: args.end.getTime() }
        const broken = brokenFinishToStart(args.e.id(), proposed, links, finder(this))
        if (broken !== null) {
          args.allowed = false
          args.message = 'This breaks a finish-to-start dependency'
        }
      },
      onEventResizing(args) {
        const proposed = { start: args.start.getTime(), end: args.end.getTime() }
        if (brokenFinishToStart(args.e.id(), proposed, links, finder(this)) !== null)
          args.allowed = false
      },
      // Before the commit: also covers keyboard moves.
      onEventMove(args) {
        const proposed = { start: args.newStart.getTime(), end: args.newEnd.getTime() }
        if (brokenFinishToStart(args.e.id(), proposed, links, finder(this)) !== null)
          args.preventDefault()
      },
      // Clicking a link offers to remove the dependency.
      onLinkClick(args) {
        const link = args.link.data
        void confirmWithUser('Remove this dependency?').then((ok) => {
          if (ok) setLinks((current) => current.filter((item) => item.id !== link.id))
        })
      },
    }
  }, [links])

  return (
    <SuperSchedulerComponent
      {...config}
      events={owned}
      links={drawnLinks}
      onEventsChange={onEventsChange}
      resources={props.resources}
      startDate="2026-10-05"
      days={14}
      scale="Day"
    />
  )
}
```
You should see:

- while dragging a step before its predecessor ends, the drag card says "Not allowed · This breaks a finish-to-start dependency" and the drop is refused;
- links your data already breaks drawn in the danger color;
- a click on a link asking whether to remove that dependency.

`onEventMoving` and `onEventResizing` run on every frame of a drag, so keep the check cheap: look up only the links of the dragged event, as `brokenFinishToStart` does. `onEventMove` runs once before the commit and also covers keyboard moves and events dropped from outside.

> **Tip:**
> Automatic rescheduling (pushing successors later when a predecessor grows) is application logic too. In `onEventMoved` or `onEventResized`, compute the new times of the successors, update your state, and group the whole change in one [history batch](https://superscheduler.org/en/docs/undo-redo/#commands) so a single undo reverts it. Critical paths, lags and calendars for dependencies are not part of the library.

## Reserved: interactive link creation
> **Limitation:**
> Creating links by dragging from one event to another is not implemented in 0.1.0. `linkCreateHandling`, `onLinkCreate`, `onLinkCreated`, `linkDotSize` and `linkPointSize` are typed for compatibility, warn once in development and do nothing; `linksLoadMethod` and `control.links.load()` do not load from a URL. The `text` and `textAlignment` fields of a link are accepted but no label is drawn.

To let users create dependencies today, add them from your own interface (for example "Add dependency" in an event's detail panel or context menu, picking the other event from a list or from the current selection) and append the link to your array.

## Related
→ https://superscheduler.org/en/examples/manufacturing-orders/
→ https://superscheduler.org/en/examples/video-production/
→ https://superscheduler.org/en/examples/port-berths/
- [Drag, resize and business rules](https://superscheduler.org/en/docs/drag-resize-rules/) for `onEventMoving`, `onEventMove` and refusal messages.
- [Undo, redo and history](https://superscheduler.org/en/docs/undo-redo/) to make rescheduling undoable.
