# Module UI

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

Canonical: https://maru.audio/docs/ui
Documentation index: https://maru.audio/llms.txt

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.

To embed a built module and its face in your own application, use the
[headless React module player](https://maru.audio/docs/module-player-react.md). Its provider and
hooks expose audio startup, parameters, presets, notes and host handles while
you choose the surrounding controls and styling.

The [UI kit catalog](https://maru.audio/ui-kit) 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/contracts"
import {
  FaceModuleNavbar,
  FaceParamKnob,
} from "maruaudio/ui/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](https://maru.audio/docs/audio-sdk.md) introduces the declaration and
parameter contract; [Project structure](https://maru.audio/docs/project-structure.md) 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](https://maru.audio/ui-kit) 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](https://maru.audio/ui-kit) 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](https://maru.audio/ui-kit/fader):

```tsx
import type { AudioModuleFaceProps } from "maruaudio/contracts"
import { Fader } from "maruaudio/ui"
import {
  faceParamBinding,
  useFaceParamEditGesture,
} from "maruaudio/ui/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, `maruaudio/contracts` and the `maruaudio/ui/...`
controls 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. [Check, build and export](https://maru.audio/docs/export.md) explains target selection and
verification.
