React Bode Plot

The standard view for filters and control loops — and the one that answers the question that matters, which is not "what does it do" but "is it stable".

Two-pole loop response — 40 dB DC gain

10Hz – 10MHz · DC gain 40.0 dB

101001.0k10k100k1.0M10M0dB−3dB−180°45-939°-190°dBphase
−3 dB corner
997 Hz
Unity gain
57k Hz
Phase margin
35.9°

Installation

npx shadcn@latest add "https://kelvinui.com/registry/bode-plot.json"

Props

PropTypeDescription
transfer(freqHz: number) => ComplexYour transfer function, evaluated at a frequency
fMin fMaxnumberSweep range in Hz
resolutionnumberSamples per decade (default 60)
heightnumberSVG height (default 340)

Phase margin is the output

Magnitude is 20·log10|H|, phase is atan2(im, re) unwrapped across the sweep — without unwrapping, a two-pole response jumps from −179° to +179° and the phase margin comes out nonsense.

The three reported figures are found by interpolating in log-frequency across the sampled sweep: the −3 dB corner (relative to DC gain), the unity-gain crossover, and the phase margin, which is 180° + phase at crossover. The margin is colour-coded — green at ≥45°, amber at ≥30°, red below — because that is the number that decides whether a loop is damped, rings, or oscillates.

The demo is a two-pole loop with 40 dB DC gain, poles at 1 kHz and 40 kHz. It checks out against the analytic values: exactly 40.000 dB at DC, 36.99 dB at the first pole (the expected −3 dB), −46.4° phase there (−45° from the dominant pole plus −1.4° from the second), −133.6° at the second pole, and a −180° asymptote. Phase margin lands at 35.9° — amber, which is a loop that will ring on a step.

Pass any transfer you like: measured VNA data, a filter prototype, or a closed-loop expression. It only needs to return a complex number.

New components every week

Get the week's new Kelvin UI components and templates in one short email. No spam, unsubscribe anytime.