Quick Start

Build a flow with sources, a process and selectable outcomes. FlowKit draws the connectors, animates signals and switches the center view.

Vanilla JavaScript · two files · zero dependencies · MIT license

Install

FlowKit ships as two plain files, not a package. Copy lib/flowkit.js and lib/flowkit.css next to your page (or host them wherever you like) and reference them with regular tags:

index.html
<link rel="stylesheet" href="flowkit.css">
<script src="flowkit.js"></script>

Download flowkit.js and flowkit.css. The engine also works from a local file. Config icons, view HTML and render functions are trusted application code; sanitize any external input before passing it in.

Your first stage

A stage is three things: a list of sources, a list of outcomes, and a center card with at least one view. Save the two files below next to flowkit.js / flowkit.css and open quickstart.html — no server needed.

quickstart.html
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="flowkit.css">
</head>
<body style="margin:0">
  <div id="app" style="width:100%"></div>
  <script src="flowkit.js"></script>
  <script src="app.js"></script>
</body>
</html>
app.js
const icons = {
  visits: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"/><circle cx="12" cy="12" r="3"/></svg>',
  signups: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="10" cy="8.5" r="3.5"/><path d="M3.5 19c.6-3.4 3-5.3 6.5-5.3s5.9 1.9 6.5 5.3"/><path d="M18.5 8.5v5M16 11h5"/></svg>',
  events: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 10l9 3.5-4 1.5-1.5 4L10 10z"/><path d="M5 3.5v2.5M3.5 8H6M8.5 3.5L7 6"/></svg>',
  trial: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3c3 1 5 4 5 8 0 3-1.5 6-2.5 7.5L12 21l-2.5-2.5C8.5 17 7 14 7 11c0-4 2-7 5-8z"/><circle cx="12" cy="10.5" r="1.8"/></svg>',
  upgrade: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 16.5l6-6 4 4 7-8"/><path d="M15 6.5h5.5V12"/></svg>',
  churn: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M9 9l6 6M15 9l-6 6"/></svg>',
  logo: '<svg viewBox="0 0 24 24" fill="none"><path d="M9.5 3l1.9 5.1L16.5 10l-5.1 1.9L9.5 17l-1.9-5.1L2.5 10l5.1-1.9L9.5 3z" fill="#a78bfa"/></svg>',
  refresh: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4.7 10.2a7.6 7.6 0 1 1-.5 4.3"/><path d="M4.2 5.2v5h5"/></svg>',
};

function renderMotor() {
  return '<div class="fk-orbit"><svg width="360" height="180" viewBox="0 0 360 180">' +
    '<circle cx="180" cy="90" r="26" class="core-glow"/>' +
    '<g class="rot" style="--rdur:60s"><ellipse cx="180" cy="90" rx="150" ry="50" class="ring"/></g>' +
    '<circle cx="180" cy="90" r="9" class="core"/></svg></div>';
}

const config = {
  headers: { left: 'Data sources', right: 'Outcomes' },
  sources: [
    { id: 'visits', label: 'Visits', icon: icons.visits },
    { id: 'signups', label: 'Signups', icon: icons.signups },
    { id: 'events', label: 'Events', icon: icons.events },
  ],
  outcomes: [
    { id: 'trial', label: 'Trial started', icon: icons.trial, accent: '#4ade80', source: 'signups',
      callout: { line1: 'Acme Inc.', line2: '14-day trial', value: '+1', unit: 'active trial' } },
    { id: 'upgrade', label: 'Upgraded', icon: icons.upgrade, accent: '#f0a832', source: 'events',
      callout: { line1: 'Pro plan', line2: 'Monthly billing', value: '+$49', unit: 'MRR' } },
    { id: 'churn', label: 'Churned', icon: icons.churn, accent: '#f4587a', source: 'visits',
      callout: { line1: 'Inactive 30 days', line2: 'No recent visits', value: '-$49', unit: 'MRR' } },
  ],
  center: {
    brand: 'Acme', logo: icons.logo,
    outputLabel: 'Output', outputValue: 'Trial started',
    views: { motor: { tag: 'Live', height: 260, render: renderMotor } },
  },
  pulses: { signups: { dash: '8 92', dur: 6 } },
  feedback: { icon: icons.refresh, label: 'Learns from the result' },
};

const app = FlowKit.embed(document.getElementById('app'), config);
app.setView('motor');
app.selectOutcome('trial');

Open quickstart.html in a browser. The example uses FlowKit.embed() to fit its container. The live result below uses the same configuration.

Result

Sources

Each entry in sources is one row in the left column: an id other config refers back to, a label, and an inline icon (raw <svg> markup, sized around 22×22). A source becomes highlighted — bold label, brighter dot, a bright "spark" riding its wire — whenever setActiveSource(id) runs, which happens automatically for the source named in a selected outcome's source field.

app.js
sources: [
  { id: 'visits', label: 'Visits', icon: icons.visits },
  { id: 'signups', label: 'Signups', icon: icons.signups },
],

app.setActiveSource('signups');

Engine views

The center card can carry several named views. Each one supplies a tag pill, a fixed height, an optional dashed "ghost" border, and either a render() function returning body markup or an html string. Use html for configurations stored as JSON. app.setView(name) swaps between them; if a view sets toggleLabel, the card footer grows a click-through toggle that cycles the view names in order (a view with no toggleLabel hides the button once it's showing).

app.js
center: {
  brand: 'Acme', logo: icons.logo, outputLabel: 'Output', outputValue: 'Trial started',
  views: {
    motor: { tag: 'Live', height: 260, toggleLabel: 'see detail', render: renderMotor },
    detail: { tag: 'Detail', height: 420, dashed: true, render: renderDetail },
  },
},

app.setView('detail');

Outcomes & callouts

Each entry in outcomes draws one button in the right column, colored by its own accent. Hovering a button with a callout previews it — a floating card with two lines and a big value; moving away reverts to whatever's currently selected. Clicking calls selectOutcome(id): it rings the button, updates the card's output value and color, lights up the outcome's source, and pins the callout. Skip callout on an outcome to opt it out of the hover preview entirely.

app.js
{ id: 'upgrade', label: 'Upgraded', icon: icons.upgrade, accent: '#f0a832', source: 'events',
  callout: { line1: 'Pro plan', line2: 'Monthly billing', value: '+$49', unit: 'MRR' } }

app.selectOutcome('upgrade');

Two lower-level primitives sit alongside this: showCallout(html, {left, top}) drops a free-form floating card anywhere on the stage, and addChip(sourceId, t, html) rides a pill along a source's wire at fraction t (0 at the dot, 1 at the card). Both are covered in API Reference.

Pulses

Wires without a pulses entry for their source just sit static. Give a source's id an entry to animate it: the default is a comet head with a fading tail travelling the wire on a loop; a four-value dash (e.g. '6 2 10 82') switches to a static repeating pattern instead. dur sets the loop length in seconds, a negative delay desyncs parallel wires, soft dims a pulse for a background feel, and stops sets the gradient colors it travels through.

app.js
pulses: {
  signups: { dash: '8 92', dur: 6.2, delay: -1.8, stops: ['#f5a0ee', '#c084fc', '#8f83f3'] },
  events: { dash: '9 91', dur: 7.0, delay: -3.4, soft: true },
},

Embedding & scaling

Use FlowKit.embed(container, config, opts) for a diagram inside a page. At widths of 700px and above, it scales a 1600×855 design canvas to fit the container. Below 700px, it stacks the sources, center card, and outcomes at readable text sizes. A ResizeObserver updates the layout when the container changes. Set responsive: false to keep the scaled diagram at every width.

Reusing the config object from Your first stage, and its width/height defaults made explicit:

app.js
const app = FlowKit.embed(document.getElementById('embed-result'), config, {
  width: 1600,
  height: 855,
  view: 'motor',
  select: 'upgrade',
});
Result

Full-screen app

Use FlowKit.create() when your application controls the stage dimensions and has enough space for all three columns. This method does not scale or stack the layout. Use embed() when the available width varies.

index.html
<style>
  html, body { margin: 0; height: 100%; }
  #app { position: fixed; inset: 0; }
</style>
<div id="app" class="fk-root"></div>
<script src="flowkit.js"></script>
<script src="app.js"></script>

In app.js, call FlowKit.create(document.getElementById('app'), config), then app.setView('motor'). Window resize redraws the wires automatically. Call app.draw() after other changes to the stage layout.

Theming

Every stage color and its body font live on --fk-* custom properties read inside .fk-root. Override them on :root to theme every stage on the page at once, or scope them to one stage's own element to give it its own palette:

styles.css
#app {
  --fk-page-bg: #0a0612;
  --fk-wire: #3a2f52;
  --fk-amber: #22d3ee;
  --fk-font: 'Inter', system-ui, sans-serif;
}

--fk-page-bg, --fk-wire, and --fk-font take effect immediately; --fk-amber recolors the output value and every callout's accent (independent of each outcome's own accent). The complete token list — including a couple declared for later that nothing reads yet — is in API Reference → CSS tokens.

Next steps

Playground

Tweak sources, outcomes and pulses live and watch the stage update, no file editing.

Open playground →

Examples

Five complete configurations with live previews and copyable source.

Browse examples →

API Reference

Every method, every config field, and the full CSS token table.

Read the API →