Skip to content

NbBlueprintCard is a node card designed to live inside an NbBlueprint canvas. It renders a card with an identity rail down its left edge, typed input/output ports, a category label, an optional enable toggle, and an optional remove button. It is presentational: the parent owns the card's position and the connections between cards.

Basic card ​

vue
<template>
  <NbBlueprintCard
    id="filter"
    title="Low-pass Filter"
    category="effect"
    color="#a855f7"
    :ports="[
      { id: 'in', label: 'Audio', type: 'input' },
      { id: 'out', label: 'Audio', type: 'output' },
    ]"
  />
</template>

Ports and connected state ​

Ports are split automatically by type: input on the left, output on the right. Pass connectedPorts with an array of port IDs to show which ports are wired; a wired pin is filled in its own colour.

Ports are the part of a node graph people look at most, so their anatomy, geometry and states are documented in full under Ports below.

Ports ​

A port is three elements, because it answers three different questions and each has a different right answer.

ElementIsSized
.nb-blueprint-card__port-hitThe <button>. The only thing that takes a click or a keypress.24×24, transparent
.nb-blueprint-card__portThe pin. Carries data-port, so this box, and nothing else, is what the canvas measures to place a wire endpoint.8×16 by default
.nb-blueprint-card__port-labelThe optional inline label, a sibling of the pin, drawn inside the card.Type only

Keeping them apart is what lets the target be comfortable without the pin being drawn that large, and what keeps a label out of the box the wire layer measures.

Geometry ​

Pins sit outside the card. The hit target's inner edge lands exactly on the card's outer edge, so nothing in the port column overlaps the card: its border and its identity rail stay continuous, and a click just inside the card is never intercepted by a port. It is also what Unreal, Blender, n8n and Node-RED do, so it is what people arriving at a node graph already expect.

The column is anchored to the top of the card, not centred in its height:

Custom propertyDefaultIs
--nb-blueprint-port-top17px, or 26px with a category, 13px when compactCentre of the first pin, from the card's top edge.
--nb-blueprint-port-pitch24pxCentre-to-centre spacing, and the height of each hit target, so adjacent targets tile rather than overlap.
--nb-blueprint-port-width8pxPin width.
--nb-blueprint-port-height16pxPin height.
--nb-blueprint-port-hit24pxHit target width.

Two things follow from anchoring to the top, and both matter more than they sound:

  • A card's wires never move when its body changes. Expanding a card, adding a parameter row, or rendering different slot content leaves every pin exactly where it was.
  • A straight chain draws straight wires. Two cards whose ports start at the same offset are joined by a horizontal wire regardless of their heights.

A card that has more ports than chrome grows to contain them: its min-height is derived from the pin count, so the last pin always sits inside the card with a corner's worth of clearance. The pitch is never compressed to make ports fit, because the pitch is also the hit target's height, and a 12-output card should look like a 12-output card.

To move the whole column, a card with a taller custom header, say, set --nb-blueprint-port-top on the card. Prefer that to changing the pitch, which is tied to the hit-target height.

States ​

StateLooks likeSet by
FreeOutline on the card surfaceThe default
HoverFilled in the pin colourPointer over the hit target
Focus2px focus ring around the pinKeyboard focus on the hit target
ConnectedSolid fill, no haloconnectedPorts
LiveSolid fill, plus a static halo in the signal colouractivePorts
MeteredFills from the bottom in proportion to the levelportLevels
Metered + liveThe meter, plus the haloBoth together
Valid targetAccent ringAutomatic, while a wire is being dragged
Invalid targetDimmed to 30%Automatic, while a wire is being dragged
RequiredHeavier outlinerequired: true on an input port

A connected pin is filled and carries no halo. The halo means signal, and a wired-but-silent port has none, so "this is wired" and "this is carrying something" stay tellable apart. The halo is static: the objection to the old treatment was that it looped forever on every live port, not that live ports were marked at all.

portLevels wins over activePorts for the pin's fill, so a port in both renders as a meter rather than a solid block, and takes the halo on top.

Drop targets ​

While a wire is being dragged, every pin in the canvas answers, in advance, whether it could accept it. Compatible pins take an accent ring; the rest dim. Nothing needs wiring up for this: NbBlueprint publishes the origin port through its card context and each card works out its own pins.

A pin can accept a wire when all of these hold:

  1. It is on a different card.
  2. It faces the other way, an output can only reach an input.
  3. Its dataType is compatible with the origin's.

Compatibility is deliberately loose, because a graph editor that refuses plausible connections is worse than one that allows a few odd ones:

  • An undeclared dataType, or 'any' at either end, connects to anything.
  • Identical types connect.
  • Members of a family connect: audio:mono reaches audio:stereo and audio:bus, because they share the segment before the colon. midi reaches neither.

Collapsed cards ​

A collapsed card keeps its header: the chevron, the title, the status glyph and the toggle. Its width floor is derived from that chrome plus a readable amount of title, so the title is never the thing squeezed out to make room for the controls.

Its ports stay in the DOM, so wires can still resolve their endpoints, but the slots flatten onto a single combined pin per side. Every hidden pin sits exactly where that combined pin is drawn, so wires converge on the connection point the user can actually see.

Signal level and activity ​

Pass portLevels, a map of port id to a number from 0 to 1, and each of those pins becomes a meter, filling from the bottom in the pin colour. A quiet port looks quiet, a hot one looks hot, and nothing loops. Values outside the range are clamped.

A port in activePorts with no entry in portLevels renders as a solid fill instead.

The expanding ring is reserved for genuinely discrete moments: the card fires a single ping when a port enters activePorts, so one ping means one thing happened. Under prefers-reduced-motion: reduce the card schedules no ping at all.

vue
<NbBlueprintCard
  id="filter"
  title="Low-pass"
  :ports="ports"
  :connected-ports="['in', 'out']"
  :active-ports="['in', 'out']"
  :port-levels="{ in: inputLevel, out: outputLevel }"
/>

Three cards at different levels. The fill is the port's own colour, so a busy graph still reads by node:

portLevels is an ordinary reactive prop, so write it at frame rate, not at audio rate. For audio-rate values, use the blueprint's non-reactive live channel instead, which the PixiJS renderer reads on its own throttled tick.

Multi-channel ports ​

A port that carries multiple channels (a stereo pair, a multi-output bus, a multi-channel MIDI port) can declare them inline via the channels array instead of writing one entry per channel by hand. The card always renders one pin per channel; there is no "bundle" or expand/collapse, so wires always land on a specific channel and the routing is visually unambiguous.

Each channel pin is addressable in connectedPorts and in IBlueprintConnection records as ${port.id}/${channel.id}.

vue
<template>
  <NbBlueprintCard
    id="reverb"
    title="Hall Reverb"
    category="effect"
    color="#10b981"
    :ports="[
      {
        id: 'in',
        label: 'Stereo In',
        type: 'input',
        dataType: 'audio:stereo',
        channels: [
          { id: 'l', label: 'L' },
          { id: 'r', label: 'R' },
        ],
      },
      {
        id: 'out',
        label: 'Stereo Out',
        type: 'output',
        dataType: 'audio:stereo',
        channels: [
          { id: 'l', label: 'L' },
          { id: 'r', label: 'R' },
        ],
      },
    ]"
    :connected-ports="['in/l', 'in/r', 'out/l']"
  />
</template>

Inline port labels ​

Ports default to tooltip-only labels (hover the pin to see them). For nodes where the port name is the primary information, set the per-port showLabel: true, or apply a card-level default with showPortLabels.

showPortLabels accepts 'left', 'right', 'both', or false. Per-port showLabel always overrides the card-level default. When the inline label is shown, the card's body padding bumps automatically so the title and parameters do not overlap the labels.

For multi-channel ports, the label rendered next to each pin is the channel's label (e.g. L, R); the parent port's label appears in the tooltip (e.g. Stereo In . L).

vue
<!-- Audio interface with eight named outputs -->
<NbBlueprintCard
  id="iface"
  title="Focusrite Scarlett"
  category="i/o . hardware"
  color="#f97316"
  show-port-labels="right"
  :ports="[
    {
      id: 'inputs',
      label: 'Inputs',
      type: 'output',
      dataType: 'audio:bus',
      channels: Array.from({ length: 8 }, (_, i) => ({
        id: `i${i + 1}`,
        label: `I${i + 1}`,
      })),
    },
  ]"
/>

<!-- Single labeled MIDI port via per-port showLabel -->
<NbBlueprintCard
  id="midiport"
  title="Roland A-49"
  category="i/o . midi"
  color="#a855f7"
  :ports="[
    {
      id: 'midi',
      label: 'MIDI Out',
      type: 'output',
      dataType: 'midi',
      showLabel: true,
    },
  ]"
/>

Selected state ​

Pass selected to draw the card's border in its own identity colour, with a matching inset line. Selection is a colour change and nothing else: it does not lift the card or cast a shadow, because moving a card moves its pins, and its pins are where its wires are anchored.

Enable toggle and disabled state ​

When enabled is passed, a compact accent-tinted toggle renders in the header. Disabled cards drop to 55% opacity, collapse the body, and append " . off" to the category tag.

Parameter rows ​

Use the parameters prop to display structured data inside the card body. Each row has a monospaced label, a value, an optional unit, and an optional progress bar.

vue
<template>
  <NbBlueprintCard
    id="terrain"
    title="Terrain"
    category="geometry"
    color="#6366f1"
    :parameters="[
      { label: 'elevation', value: 6, bar: 60 },
      { label: 'seed', value: '0x2A' },
    ]"
  />
</template>

Status ​

Three states, each a distinct glyph so they stay apart in greyscale, for a reader with a colour vision deficiency, and at the zoom levels where a coloured dot would be sub-pixel.

The glyph is a header cell rather than part of the title, so a long title ellipsises without pushing the status out of view.

Removable ​

Custom body content ​

Anything placed in the default slot renders inside the card body, below the parameters.

Density ​

density controls how tightly the card packs its chrome. Set it once on NbBlueprint and every card inherits it; a card can still override its own.

DensityHeaderCategory lineParameter rowsFirst pin
'default'34px, or 51px when the card has a categoryShown24px17px / 26px
'compact'26pxHidden20px13px

'compact' is worth reaching for once a graph is past roughly twenty nodes, where the headers are more of the canvas than the graph is.

Density changes chrome only. Port width, height, pitch and hit area are identical at both densities, so switching density does not move a single wire. The one thing that does move is --nb-blueprint-port-top, which tracks the header it is meant to line up with.

The pins sit at the same offset in both, which is the point: switching density across a whole canvas does not move a single wire.

vue
<!-- Every card in this canvas is compact... -->
<NbBlueprint density="compact">
  <NbBlueprintCard id="a" title="Gain" />
  <!-- ...except this one. -->
  <NbBlueprintCard id="b" title="Master bus" density="default" />
</NbBlueprint>

Keyboard and assistive technology ​

The card is a focusable role="group", labelled with its title, category, status and enabled state. Every control inside it is a real button with its own accessible name.

KeyWhereDoes
TabAnywhereMoves through the card and its controls.
← → ↑ ↓CardMoves the card by one canvas unit.
Shift + arrowCardMoves it by ten.
Enter / SpacePortStarts a connection, or completes one already started.
EscPortAbandons a connection in progress.
Enter / SpaceCollapse chevron, toggle, removeActivates that control.

Connecting by keyboard walks the same two-step path the mouse does and goes through the same handlers, so a keyboard connection is indistinguishable from a dragged one to the host. While a connection is in progress, valid targets ring and invalid ones dim exactly as they do for the mouse, which is what makes the keyboard flow navigable at all.

Nudging is routed through the parent NbBlueprint, so it reports itself with the same move event a drag does, and nudging a card that is part of the selection moves the whole selection, again matching the mouse. Positions have one path out of the component, not two.

Focusing a card also selects it, so the inspector and the canvas agree about what the user is looking at.

Inside a blueprint ​

For a complete example wiring cards and ports into an NbBlueprint canvas, see the Blueprint docs.

Props ​

PropTypeDefaultDescription
idstring(required)Unique card identifier. Used in connection records and the data-port attribute.
titlestring(required)Card title (rendered as the dominant element in the header).
colorstringvar(--nb-c-primary)Node identity colour: the left rail, the selected border, connected pin fill, meter fill.
enabledbooleantrueShows the header toggle. Omit to hide the toggle entirely.
selectedbooleanfalseDraws the card's border in its own colour, with a matching inset line.
categorystring''Uppercase label below the title, in neutral text. Also moves the first pin down to line up with the taller header.
portsIBlueprintPort[][]Port definitions. input ports render on the left edge, output on the right.
connectedPortsstring[][]IDs of pins that are currently connected. Use ${port.id}/${channel.id} for channel pins.
parametersIBlueprintCardParameter[][]Structured parameter rows displayed in the card body.
xnumber0Canvas X position (documentary: the parent positions the card).
ynumber0Canvas Y position (documentary: the parent positions the card).
removablebooleanfalseShows a remove button in the header. Emits remove when clicked.
collapsedbooleanfalseCollapses the body, showing only the header row.
statusTBlueprintCardStatus'none'Status glyph in the header: a ringed check, a warning triangle, or a filled error circle. Shape carries the meaning, so the three stay apart in greyscale.
previewstring''Compact monospaced preview text shown in the body.
showPortLabelsTBlueprintPortLabelModefalseCard-level default for inline port labels: 'left', 'right', 'both', or false.
portLevelsRecord<string, number>{}Signal level per port id, 0 to 1. Listed pins render as meters; see Signal level and activity.
densityTBlueprintDensityinherited'default' or 'compact'. Inherited from the parent NbBlueprint when unset. Chrome only: it never moves a pin.

Events ​

EventPayloadDescription
selectstring (the card id)Emitted on mousedown anywhere on the card body.
toggle[id: string, enabled: boolean]Emitted when the header toggle is clicked.
toggle-collapsestring (the card id)Emitted when the collapse chevron is clicked.
removestring (the card id)Emitted when the remove button is clicked.
port-mousedownIBlueprintCardPortEventportId is the rendered pin id (${port.id} or ${port.id}/${channel.id}); dataType is the pin's declared type, if it has one.
port-mouseupIBlueprintCardPortEventSame portId semantics. Forward both to NbBlueprint to drive wire dragging.

Slots ​

SlotDescription
defaultCustom body content rendered below the parameters (e.g. small value readouts).

Types ​

ts
type TBlueprintPinDataType =
  | 'geometry'
  | 'celestial'
  | 'lighting'
  | 'effect'
  | 'surface'
  | 'audio'
  | 'audio:mono'
  | 'audio:stereo'
  | 'audio:bus'
  | 'midi'
  | 'midi:rechannelized'
  | 'control'
  | 'entity'
  | 'number'
  | 'vector3'
  | 'color'
  | 'asset'
  | 'any'

type TBlueprintPortLabelMode = 'left' | 'right' | 'both' | false

interface IBlueprintPortChannel {
  id: string
  label: string
}

interface IBlueprintPort {
  id: string
  label: string
  type: 'input' | 'output'
  dataType?: TBlueprintPinDataType
  required?: boolean
  /**
   * Optional list of sub-channels. When set, the port renders one pin per
   * channel, addressable as `${port.id}/${channel.id}`. There is no bundle
   * or expand/collapse: every channel always shows so wires land on a
   * specific pin and routing is visually unambiguous.
   */
  channels?: IBlueprintPortChannel[]
  /** Render the port label inline next to the pin (in addition to the tooltip). */
  showLabel?: boolean
}

interface IBlueprintCardParameter {
  label: string
  value: string | number
  unit?: string
  bar?: number // 0 to 100, renders a thin progress bar
}

Pin colours and signal kind by dataType ​

Every data type renders as the same pill. A pin is the target a user aims a wire at, and a target whose shape changes with its type is one they have to re-learn per port, which costs more than the shape ever told them. What varies is colour and signal kind.

dataTypeColourSignalTypical use
audio#22c55eanalogGeneric mono audio.
audio:mono#22c55eanalogSingle-channel audio (semantic alias).
audio:stereo#10b981analogStereo pair (declare with channels: [L, R]).
audio:bus#059669analogN-channel audio bus (any channel count).
midi#a855f7digitalMIDI stream.
midi:rechannelized#9333eadigitalMIDI duplicated across channels (GP-style).
control#94a3b8digitalTriggers, gates, bypass wiring.
geometry#6366f1analogGeometric data.
celestial#f97316analogCelestial / scene-graph data.
lighting#f59e0banalogLighting data.
effect#a855f7analogEffect-graph data.
surface#3b82f6analogSurface / mesh data.
entity#ec4899digitalEntity / actor reference.
number#94a3b8analogScalar number.
vector3#38bdf8analog3-component vector.
color#fb923cdigitalColour value.
asset#a78bfadigitalAsset reference (texture, sample, plugin).
any#64748banalogUntyped / wildcard.

Analog and digital ​

signal is the library's own axis, independent of dataType. It answers a question dataType cannot: does this port carry a continuously varying quantity, or discrete events? That decides two things.

Decoration. Digital pins are striped, analog pins are solid. The stripe is a fill, not a form, so it survives being scaled: at 40% canvas zoom a striped pin still reads as "not solid", where a smaller glyph or a rotated silhouette would have collapsed into the same few pixels as a plain pill. It also leaves the colour channel alone, which dataType already spends.

Activity. The two kinds show they are busy in different ways, because they have different things to say.

SignalActivityDriven by
analogFills from the bottom in proportion to its levelportLevels
digitalOne pulse per event, in the pin's own colourEntering activePorts

An analog port has a magnitude worth showing, so it shows it. A digital port does not: a level meter on a note stream is noise, and a ring that loops while the port stays active only ever says "still wired". One pulse per event means one pulse per thing that happened.

The pulse is drawn in the pin's colour rather than a separate signal colour, so it reads as that port firing rather than as a third thing happening nearby.

Side by side. The audio pins are solid, the MIDI pin is striped, and neither changed shape to say so:

Defaults come from dataType (see the table above), so existing ports behave sensibly without being touched. Set signal on a port to override, which is what you want for, say, a control port carrying a continuous automation lane rather than a trigger:

ts
const ports: IBlueprintPort[] = [
  // Audio in: solid, and metered from portLevels.
  { id: 'in', label: 'In', type: 'input', dataType: 'audio' },
  // MIDI in: striped, and pulses once per note.
  { id: 'midi', label: 'MIDI', type: 'input', dataType: 'midi' },
  // A control port that is really a continuous value, not a trigger.
  {
    id: 'mod',
    label: 'Mod',
    type: 'input',
    dataType: 'control',
    signal: 'analog',
  },
]

Per-port style overrides ​

Three optional fields on IBlueprintPort override the dataType-derived defaults when two ports of the same dataType need to look different (typical example: a "bypass" control input that should read distinctly from other control ports).

FieldTypeEffect
shape'pill' | 'square' | 'circle'An explicit outline. Never derived from dataType: shape is the target a user aims at, so it stays constant unless a card deliberately sets one.
colorCSS color stringBeats the dataType-derived color. Wires drawn from / to this pin inherit it.
size'sm' | 'md' | 'lg''md' is the default. 'sm' de-emphasises secondary ports; 'lg' flags primary.
signal'analog' | 'digital'Beats the dataType-derived default. Drives the stripe decoration and which activity treatment the pin uses.
ts
const ports: IBlueprintPort[] = [
  // A primary stereo audio input — emphasise it.
  {
    id: 'in',
    label: 'Audio In',
    type: 'input',
    dataType: 'audio:stereo',
    size: 'lg',
  },
  // A control input styled distinctly from MIDI / parameter ports
  // even though it shares dataType: 'control'.
  {
    id: 'bypass-in',
    label: 'Bypass',
    type: 'input',
    dataType: 'control',
    shape: 'circle',
    color: '#94a3b8',
    size: 'sm',
  },
]

Every shape and size is a real dimension on the pin element, so a pin's appearance, its wire endpoint and its hit area always agree. Shapes are centred on the same axis, so overriding a shape does not move the wire endpoint. The symmetrical shapes (square, circle) sit tangent to the card edge rather than flush against it.

Shape is never derived. Reach for a variant only when a port has to be told apart from its immediate neighbours on the same card, and never to encode what a port carries: that is what signal and color are for, and neither disturbs the outline a user is aiming at.

Sub-pins (channels of a multi-channel port) render shorter than a root pin so a row of them reads as one stacked port.

CSS custom properties ​

VariableDefaultDescription
--nb-card-colorvar(--nb-c-primary)Node identity colour: the left rail, the selected border, connected pin fill, meter fill.
--nb-card-glow(computed)18% alpha version of the identity colour. Still published for host CSS; the card itself no longer paints with it.
--nb-blueprint-port-top17px / 26px / 13pxCentre of the first pin from the card's top edge. See Geometry.
--nb-blueprint-port-pitch24pxPin centre-to-centre spacing, and the hit-target height.
--nb-blueprint-port-width8pxPin width.
--nb-blueprint-port-height16pxPin height.
--nb-blueprint-port-hit24pxHit-target width.
--nb-blueprint-rail-width3pxWidth of the identity rail on the card's left edge.

The card reads these theme tokens rather than any literal colour, so retheming it is a token change and never a component change: --nb-c-port-bg, --nb-c-port-border, --nb-c-port-signal, --nb-c-node-row-bg, and --nb-c-status-valid / -warning / -error.