Module UI

Build a flexible module face with host-bound controls, portable dependencies, and the UI kit.

A module's UI is its face: the panel people see in Studio, on a module page, or in a supported plugin host. You can start with schema-driven controls or author a React/TSX face in src/ui/index.tsx. The face can have its own layout, art, animation, and module-owned CSS. It is ordinary browser UI code, but its musical inputs and outputs come through the module host.

The UI kit catalog is the live list of components, source, props, installation instructions, and theme examples. This guide explains where those pieces fit in a module. You do not need to reproduce the catalog in your face.

One contract, separate sound and visuals

The module declaration owns ports, parameter paths, ranges, defaults, presets, and its Graph or Processor. The audio runtime plays that definition. The face reads host-supplied state and sends edits back through AudioModuleFaceProps. Changing the look of a knob does not change its audio algorithm or create a second parameter definition.

Keep visual state, such as which panel is expanded, in the face. Keep parameter values, preset selection, bypass, CC assignments, and audio or MIDI input with the host. A face should not open an AudioContext, acquire devices, import its Processor or DSP files, or calculate its own audio signal. The host also supplies diagnostic and meter data when those capabilities are available.

For a custom face, use the SDK type and the UI kit's module bindings:

tsx
import type { AudioModuleFaceProps } from "@maruaudio/audio-sdk"
import {
  FaceModuleNavbar,
  FaceParamKnob,
} from "@maruaudio/ui-kit/module-face-controls"
import "./styles.css"

export default function MyFace(face: AudioModuleFaceProps) {
  return (
    <main className="my-module-face">
      <FaceModuleNavbar face={face} brand="My Module" />
      <FaceParamKnob face={face} path="filter.cutoff" label="Cutoff" />
    </main>
  )
}

filter.cutoff must be a path in the module's parameter schema. The bound knob gets its value, limits, default, and taper from that schema and writes through the host. The navbar uses host capabilities for presets and module power when they are available. For a custom control, use faceParamBinding(face, path) or readModuleFaceValue(face, path) instead of copying ranges and fallback values into the UI. Audio SDK introduces the declaration and parameter contract; Project structure shows where the face belongs.

Choose how much to customize

  1. Use stock controls. Start with the schema-driven face or drop in FaceParamKnob, FaceParamEnvelopeEditor, and other bound controls. They already read the host's parameter descriptors and write through its bridge.
  2. Change colors and variants. Choose a component's visual variant, then set --uikit-* tokens and module-scoped styles for your palette, type, and layout. The live UI kit panel lets you compare theme and finish examples.
  3. Draw your own controls. Use headless controls and behavior hooks for gestures, keyboard access, focus, and CC affordances while supplying your own rendering. Bind them to the same schema path and host callbacks.
  4. Build a fully custom face. Compose the panel from browser-safe UI code, art, animation, and declared dependencies. The first-party rich-face source is React/TSX; you can use libraries such as Three.js for a 3D view or other browser UI libraries inside it. The design can be entirely yours while parameter edits and host inputs still use the SDK face contract.

There is no fixed visual style or short allowlist of UI libraries. A dependency must still run in the face's browser environment, be declared by the module, and bundle for the targets you intend to export. Check any use of browser APIs in each host you support.

What the kit gives you

The UI kit is a growing library of controls already built for plugin interaction. It includes:

  • Controls: knobs, faders, switches, XY pads, envelopes, keyboards, grids, transport controls, and preset selection.
  • Feedback: meters, scopes, spectrum and waveform views, EQ and dynamics displays, and musical visualizers.
  • Foundations: gesture and value helpers, CC targets and badges, formatting, theme tokens, and reusable interaction behavior.

Use a finished component to get pointer, keyboard, focus, value, and CC behavior quickly. Many controls also expose headless primitives or hooks so you can draw your own skin while retaining that behavior. The ready-made and headless paths are different presentations of the same interaction contract, not separate ways to store parameters or MIDI mappings. The UI kit catalog is the current inventory, with source and props for each component.

Port an existing panel one control at a time

Keep the existing layout first. Map every sound-changing input to an existing parameter path, or add that parameter to the module declaration before changing the view. Move its range, default, step, options, and taper into the schema. Then replace the local state or direct engine call with a face binding. For example, a plain horizontal range input for a linear output.level parameter can become a styled UI-kit fader:

tsx
<input
  type="range"
  min={0}
  max={1}
  step={0.01}
  value={level}
  onChange={(event) => setLevel(Number(event.currentTarget.value))}
/>

Replace the local level state and duplicated bounds with the module's output.level descriptor and a Fader:

tsx
import type { AudioModuleFaceProps } from "@maruaudio/audio-sdk"
import { Fader } from "@maruaudio/ui-kit"
import {
  faceParamBinding,
  useFaceParamEditGesture,
} from "@maruaudio/ui-kit/module-face-controls"

export function OutputLevel({ face }: { face: AudioModuleFaceProps }) {
  const path = "output.level"
  const binding = faceParamBinding(face, path)
  const edit = useFaceParamEditGesture(face)

  return (
    <Fader
      {...binding}
      label="Output"
      orientation="horizontal"
      variant="flat"
      onChange={(value) => edit.change(path, value)}
      onChangeEnd={(value) => edit.end(path, value)}
    />
  )
}

The path from faceParamBinding is the CC target; the host supplies the current value, including changes from an assigned hardware potentiometer or encoder. The edit callbacks bracket a drag for host automation and undo. Use a schema-aware bound control, such as FaceParamKnob, when the parameter has a nonlinear taper; keep that response in the descriptor rather than drawing a different curve only in the face.

After each control, verify pointer and keyboard edits, preset recall, automation, and CC mapping in the actual host. Move shared pure labels or geometry into a module shared file if both DSP and UI need them. Port visuals and layout after the parameter path works, then build the packaged face to catch missing CSS, assets, or dependencies.

Let the host handle every input

A mouse drag, host automation lane, recalled preset, and MIDI CC should all reach the same schema path. When a player turns a mapped potentiometer or encoder, the host updates the parameter and the face receives the new value; the control moves with it. Bind visible controls through the face contract; include the stable path on mappable controls so the shared CC UI can offer assignment, Learn, and activity. The host receives physical MIDI, manages the mapping, and writes the parameter. A face should not listen to MIDI devices or keep a second CC map. A parameter's response curve belongs in its schema taper, so its position agrees across the face, automation, and CC.

Use the host's gesture callbacks for a custom drag control, and consume host capabilities for transport, notes, telemetry, or bypass rather than inventing a local replacement. Some capabilities differ by host; only show a control when the host can honor it. For example, module power is exposed as face.power, and FaceModuleNavbar already renders its bound switch. A visual-only bypass switch would not affect the audio engine.

Keep the face exportable

The face is bundled separately from the audio runtime for supported targets. Import its styles explicitly from the face entry, keep visual assets in the module, and use browser-safe dependencies declared in the module's package.json. React, the public SDK face surface, and the public UI kit are the shared platform surface; other browser-safe UI libraries can be bundled when the project declares them. Avoid imports from the website or Studio app, repository-relative package internals, Node or native APIs, remote runtime assets, and DSP implementation files. Put pure data needed by both the face and Processor in a small shared module instead.

An import that works in a local preview is not proof that the face will package or mount in every target. Build the intended target and check the packaged face, its styles, assets, keyboard behavior, parameter edits, and available host capabilities. Export explains target selection and verification.