@fieldui/react
v0.4.0
Published
React audio components for Field — waveforms, transports, meters, spectrograms, scrubbers, regions, knobs, faders. Built on @fieldui/core hooks and @fieldui/tokens.
Maintainers
Readme
@fieldui/react
Visual React components for Field, a headless-first React component library for audio interfaces (waveforms, transports, meters, spectrograms, scrubbers, regions, knobs, faders).
This package is the paint layer. Every component reads tokens from
@fieldui/tokens for
styling and consumes hooks from
@fieldui/core for behaviour.
If you want a custom UI on the same audio engine, drop this package and
build directly on @fieldui/core.
┌──────────────────────────────────────────────────────────────┐
│ @fieldui/react ← components (canvas + radix + tailwind) │
│ @fieldui/core headless hooks (audio graph, RAF, etc.)│
│ @fieldui/tokens CSS variables + TS token map │
└──────────────────────────────────────────────────────────────┘Highlights
- Canvas where speed matters. Waveforms, meters, spectrograms, and
timelines paint to canvas at device pixel ratio. Sizing happens in
effects (not RAF) so the bitmap is always correct. Colours are resolved
once per resize via
getComputedStyleand cached for the hot loop. - Controlled or hook-driven. Anything that animates accepts both
styles. Pass
currentTimefor SSR or static rendering, ortimeRef + duration + playingto drive the visual imperatively from a RAF loop with zero React re-renders during playback. The unplayed-dim on the waveform tracks the playhead frame-perfectly this way. - Compound transport.
<Transport.Root>carries state and callbacks via context..Play,.Pause,.Stop,.SkipBack,.SkipForward, and.Recordare slot-style children. Pick which buttons you mount, in what order. - Customisation paths, in order of invasiveness. ① override CSS
variables on
:rootfor theming. ②classNamepassthrough for per-component tweaks (every component runs throughcn()so user classes win). ③ compound slots (Transport,MediaButton). ④ drop to@fieldui/coreand write your own markup. - Accessibility baked in. Knobs and faders expose proper ARIA
(
role="slider"witharia-valuemin/max/now), level meters userole="meter", waveformsrole="img"with alabelprop, and the transport toolbar carriesrole="toolbar". Keyboard handling (arrows / shift fine / page / home / end) on every continuous control.
Install
npm install @fieldui/react @fieldui/core @fieldui/tokens react react-dom
# or pnpm / yarn / bun@fieldui/core and @fieldui/tokens are runtime deps of this package
and will be installed automatically. Listing them above only matters
because their public APIs surface in your code (you import providers from
@fieldui/core, override variables defined by @fieldui/tokens).
Then import the styles once at your app entry:
import "@fieldui/react/styles.css";This includes @fieldui/tokens/tokens.css plus a thin field-preset.css
layer for body font, paper background, .field-tabular, and
.field-bitmap helpers.
Compatible with Tailwind v4, but does not require it. The
styles.css shipped in dist/ is pre-compiled — every utility class
the components emit (h-10, min-h-20, the MediaButton size
variants, etc.) is already baked in, alongside the design tokens and
base styles. You don't need Tailwind installed, no @source config in
your app, and no node_modules scanning quirks: @import
"@fieldui/react/styles.css" is genuinely all you need.
If you DO use Tailwind v4 in your app and want to compose with your own utility layer (and tree-shake against your app's own usage), import the source preset instead:
@import "tailwindcss";
@import "@fieldui/react/field-preset.css";
@source "../node_modules/@fieldui/react/dist";The @source line is required because Tailwind v4 excludes
node_modules from auto-scanning even when the imported CSS declares
its own @source. (This is why the pre-compiled styles.css exists —
so most consumers don't have to know this.)
Fonts
field-preset.css loads DM Sans, JetBrains Mono, and VT323 from Google
Fonts via an @import url(...). This works out of the box but bypasses
Next.js's font optimization. For production Next.js apps, prefer
next/font/google and override the font CSS variables yourself:
// app/layout.tsx
import { DM_Sans, JetBrains_Mono, VT323 } from "next/font/google";
const sans = DM_Sans({ subsets: ["latin"], variable: "--font-sans-next" });
const mono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono-next" });
const lcd = VT323({ weight: "400", subsets: ["latin"], variable: "--font-lcd-next" });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html className={`${sans.variable} ${mono.variable} ${lcd.variable}`}>
<body>{children}</body>
</html>
);
}/* app/globals.css */
:root {
--font-family: var(--font-sans-next);
--font-mono-family: var(--font-mono-next);
--font-bitmap: var(--font-lcd-next);
}The Google Fonts CDN load in field-preset.css becomes a no-op (the
browser caches it but your overrides win) or you can opt out entirely by
copying field-preset.css into your app and removing the @import url.
Quickstart
A complete one-file player with providers, decode, transport, scrubber, playhead, waveform, and time readout:
import "@fieldui/react/styles.css";
import {
AudioGraphProvider,
RenderLoopProvider,
useAudioBuffer,
useAudioGraph,
useAudioSource,
usePlaybackTime,
useTransport,
} from "@fieldui/core";
import {
PlayheadMarker,
Scrubber,
TimeDisplay,
Timeline,
Transport,
WaveformDisplay,
} from "@fieldui/react";
const App = () => (
<RenderLoopProvider>
<AudioGraphProvider>
<Player src="/clip.wav" />
</AudioGraphProvider>
</RenderLoopProvider>
);
const Player = ({ src }: { src: string }) => {
const { masterAnalyser } = useAudioGraph();
const { buffer } = useAudioBuffer(src);
const audioSource = useAudioSource(buffer ? { buffer } : null);
const transport = useTransport({
buffer,
createBufferSource: audioSource.createBufferSource,
connectSource: (node) => masterAnalyser && node.connect(masterAnalyser),
});
const time = usePlaybackTime(transport);
const duration = transport.duration;
return (
<section className="space-y-4 p-6">
<header className="flex items-baseline justify-between">
<h1 className="text-2xl font-semibold">Audio player</h1>
<TimeDisplay seconds={transport.currentTime} format="mm:ss.ms" />
</header>
<div className="relative">
<WaveformDisplay
buffer={buffer}
timeRef={time.timeRef}
duration={duration}
playing={transport.state === "playing"}
progress={duration > 0 ? transport.currentTime / duration : 0}
className="h-32"
/>
<PlayheadMarker
timeRef={time.timeRef}
duration={duration}
playing={transport.state === "playing"}
position={duration > 0 ? transport.currentTime / duration : 0}
/>
</div>
<Timeline
duration={duration}
currentTime={transport.currentTime}
timeRef={time.timeRef}
playing={transport.state === "playing"}
/>
<div className="flex items-center gap-4">
<Transport
state={transport.state}
onPlay={transport.play}
onPause={transport.pause}
onStop={transport.stop}
onSkip={(d) => transport.seek(transport.getCurrentTime() + d)}
>
<Transport.SkipBack />
<Transport.Play />
<Transport.Pause />
<Transport.Stop />
<Transport.SkipForward />
</Transport>
<div className="flex-1">
<Scrubber
value={transport.currentTime}
max={Math.max(duration, 0.0001)}
onChange={transport.seek}
/>
</div>
</div>
</section>
);
};Component surface
| Family | Components |
|--------------|------------------------------------------------------------|
| button | MediaButton. Flat-prop API (icon="play", label, badge="on") for the common case, plus the compound .Root + .Icon + .Label + .Badge slots for full control. Square or rectangle, three sizes, three tones. Built-in icons: play, pause, stop, forward, back, record (the record dot pulses on pressed, matching Transport.Record). |
| transport | Transport (compound: .Root, .Play, .Pause, .Stop, .SkipBack, .SkipForward, .Record) |
| waveform | WaveformDisplay. Peaks, peaksUrl, or buffer; signed envelope; RAF-driven progress overlay. density prop (comfortable / compact / thin) controls the min-height floor — set thin to let a smaller className="h-2" actually take effect. |
| scrubber | Scrubber. Radix Slider underneath, brand-styled. |
| timeline | Timeline. Nice-interval ticks, controlled or RAF-driven playhead. |
| meter | LevelMeter. Segmented LED bars, controlled or analyser-driven. |
| controls | Knob (rotary), Fader (linear or log taper) |
| display | TimeDisplay. LCD-style; formats mm:ss, mm:ss.ms, hh:mm:ss, SMPTE, bars:beats, samples. |
| region | RegionSelector (drag/resize via useRegionDrag), PlayheadMarker |
| spectrogram| Spectrogram. Scrolling FFT, mono or fire colour map. |
| layout | Grid, GridItem. 4/8/16-col responsive scaffold. |
Every visual component:
- Forwards
classNameand arbitrary HTML props through...rest. - Reads colour from CSS variables, so the brand override pattern in
@fieldui/tokensjust works. - Is DPR-aware on canvas. Sharp on retina, no blurry bitmap on resize.
Theming
There's no ThemeProvider. Re-declare the variables on :root (or any
ancestor) and the change cascades through every component:
:root[data-theme="lab"] {
--color-audio-scrubber: var(--color-cobalt);
--color-audio-region: color-mix(in oklab, var(--color-cobalt) 18%, transparent);
--color-audio-meter-warn: var(--color-ochre);
--color-button-accent: var(--color-cobalt);
}Toggle <html data-theme="lab"> on a media-query, a user setting, or a
Storybook toggle, and rust shifts to cobalt across every meter, scrubber,
and button. No React state involved.
See @fieldui/tokens for the
full vocabulary.
Sibling packages
| Package | Role |
|---------|------|
| @fieldui/core | Headless React hooks for audio: transport, audio graph, peaks, regions, recording, analysis, export. Every component here is a paint layer over these hooks. |
| @fieldui/tokens | Design tokens. CSS variables and TypeScript token map. Brand palette, typography stack, spacing/radius scales, audio-semantic colours, Tailwind anchor overrides. |
Compatibility
- React 18 or 19 (peer dep on both).
- Tailwind v4 for the prebuilt class strings.
- Modern evergreen browsers. Uses
AudioContext,MediaRecorder,ResizeObserver, Pointer Events, andmatchMedia. ES2022 / Node ≥ 20 for development.
License
MIT. © 2026 fieldui.
