Getting StartedComponentsDesign TokensThemingTheme SynthesizerFrameworksAccessibilityUtilitiesServer RenderingBrowser SupportContributingChangelog App ShellAspect GridAuth ShellCenterClusterContainerDashboard GridDockFloat BarInsetMasonryPage HeaderPage LayoutResizableResponsive SwitcherSectionSettings LayoutSplit PaneStatus BarStickyToolbar Anchor NavBottom NavBreadcrumbBreadcrumb MenuCommand BarDrawerFooterLinkMenubarNavigation MenuPage IndicatorPaginationRailScroll IndicatorScroll SpyScroll To TopSidebarSkip LinkSpeed DialStepper NavTabsTop BarTree View AccordionAspect RatioAvatarAvatar GroupCalloutCardCarouselCollapsibleColor SwatchCTA BannerDividerEmpty StateFeature CardIconImageImage CompareImage HotspotsInfinite ScrollLightboxMarqueeQR CodeScroll AreaSeparatorSkeletonSpinnerStackVideoVirtual List Activity HeatmapAnimated NumberBadgeChartClockComparisonCountdown TimerData GridData TableDescription ListDiffEvent CalendarGaugeJSON TreeKanbanKey ValueLevel MeterListMeterSparklineStatStepperTableTagTimelineUptimeValue CardWaveform BlockquoteCode BlockGradient TextHighlightKbdKeyboard MapMarkdownNumber FormatProseTerminalTextTime AgoTruncateTypewriter ButtonButton GroupCalendarCheckboxChipColor PickerComboboxCopy ButtonDate PickerDate Range PickerFieldsetFile UploadFormHotkeyIcon ButtonImage CropperInline EditInputInput GroupKnobLabelMasked InputMulti SelectNumber InputOTP InputPassword InputPin InputRadio GroupRange SliderRatingSearchSegmented ControlSelectSignature PadSliderSortable ListSwitch GroupTag InputTextareaTheme ToggleTime PickerToggleTransfer ListTree Select AlertAnnouncementBannerCommand PaletteConfirmConnection StatusContext MenuConversationDialogDropdown MenuGuided TourHover CardInline MessageLoading OverlayModalNotification PanelPopoverProgressProgress ToastSheetSnackbarSpotlightToastTooltip
ARC UI ARC Radiant Components
v3.2 Docs Components Tokens Synthesizer
Getting StartedFrameworksServer Rendering Design TokensThemingTheme SynthesizerTypographyUtilities All ComponentsAccessibilityBrowser SupportChangelogContributingStats App ShellAspect GridAuth ShellCenterClusterContainerDashboard GridDockFloat BarInsetMasonryPage HeaderPage LayoutResizableResponsive SwitcherSectionSettings LayoutSplit PaneStatus BarStickyToolbar Anchor NavBottom NavBreadcrumbBreadcrumb MenuCommand BarDrawerFooterLinkMenubarNavigation MenuPage IndicatorPaginationRailScroll IndicatorScroll SpyScroll To TopSidebarSkip LinkSpeed DialStepper NavTabsTop BarTree View AccordionAspect RatioAvatarAvatar GroupCalloutCardCarouselCollapsibleColor SwatchCTA BannerDividerEmpty StateFeature CardIconImageImage CompareImage HotspotsInfinite ScrollLightboxMarqueeQR CodeScroll AreaSeparatorSkeletonSpinnerStackVideoVirtual List Activity HeatmapAnimated NumberBadgeChartClockComparisonCountdown TimerData GridData TableDescription ListDiffEvent CalendarGaugeJSON TreeKanbanKey ValueLevel MeterListMeterSparklineStatStepperTableTagTimelineUptimeValue CardWaveform BlockquoteCode BlockGradient TextHighlightKbdKeyboard MapMarkdownNumber FormatProseTerminalTextTime AgoTruncateTypewriter ButtonButton GroupCalendarCheckboxChipColor PickerComboboxCopy ButtonDate PickerDate Range PickerFieldsetFile UploadFormHotkeyIcon ButtonImage CropperInline EditInputInput GroupKnobLabelMasked InputMulti SelectNumber InputOTP InputPassword InputPin InputRadio GroupRange SliderRatingSearchSegmented ControlSelectSignature PadSliderSortable ListSwitch GroupTag InputTextareaTheme ToggleTime PickerToggleTransfer ListTree Select AlertAnnouncementBannerCommand PaletteConfirmConnection StatusContext MenuConversationDialogDropdown MenuGuided TourHover CardInline MessageLoading OverlayModalNotification PanelPopoverProgressProgress ToastSheetSnackbarSpotlightToastTooltip

Waveform

Audio waveform visualization that doubles as a scrubber. Renders a consumer-computed peaks array as an SVG waveform with an accent played region and glowing playhead, and becomes a full seek control when interactive.

Components Waveform
data hybrid
<arc-waveform>

Overview

Waveform draws an audio clip's shape from a plain array of peak amplitudes — one number per bar, 0 to 1 — and marks playback progress on it: the played region renders in accent with a soft glow, the unplayed region stays muted, and a thin glowing playhead line sits at the current position. Set `interactive` and the same element becomes a scrubber, with pointer dragging, keyboard seeking, and full slider semantics for assistive technology. The component never touches audio. There is no AudioContext, no decoding, no fetching — the consumer computes peaks however it likes (from a decoded buffer, a server-side analysis pass, or a cached sidecar file) and hands them over as the `peaks` property. That split keeps the component a pure function of its props, so it server-renders, and keeps it cheap enough to repeat down a lane of clips in a DAW timeline or a list of voice memos. `position` is a fraction of the total (0 to 1), not seconds. If your player thinks in seconds, divide by the duration on the way in and multiply on the way out — the `time` field on both event details does the multiplication for you whenever `duration` is set. Setting `duration` also renders a monospaced time readout below the track and switches the slider's spoken value from a percentage to elapsed and total time. While scrubbing, `arc-input` fires on every pointer move with the live position, and `arc-change` fires once on release with the committed one — the standard ARC edit/commit contract. Wire the actual seek of your audio source to `arc-change`, and use `arc-input` for live feedback such as a time display or audible scrubbing. Two variants cover the common looks: `bars` draws discrete bar pairs mirrored around the center line, and `mirror` draws a filled min/max envelope.

Guidelines

When to use

  • Compute peaks once per clip and cache them — a few dozen to a few hundred values is plenty; the component clamps each to 0-1
  • Wire the actual seek of your audio source to arc-change, and keep arc-input for cheap live feedback like a time display
  • Set duration whenever you know it, so users get time readouts and screen readers hear times instead of percentages
  • Give every waveform a label — it names the slider for assistive technology, or describes the image when not interactive
  • Use the bars variant for scrubbers and player UI, and the mirror variant for dense timeline lanes where discrete bars would shimmer
  • Set the height with the --waveform-height custom property when the default is too short or too tall for its row

When not to use

  • Do not use Waveform for generic trend data — that is Sparkline’s job; Waveform’s shape language says "audio" and its center-mirrored geometry distorts ordinary series
  • Do not use it as a level indicator or progress bar — Meter shows a single current value against a range; Waveform shows amplitude over time
  • Do not feed it raw sample data — downsample to peaks first; tens of thousands of bars help no one and cost real DOM
  • Do not track position in seconds — the position property is a 0-1 fraction; convert at the edges or read the time field on event details
  • Do not seek your audio source on arc-input — that fires on every pointer move; the committed value arrives once, on arc-change

Features

  • Renders from a consumer-computed peaks array — no AudioContext, no decoding, no audio dependencies
  • Two variants: `bars` (mirrored bar pairs) and `mirror` (filled min/max envelope)
  • Played region in accent with a soft glow; unplayed region muted; thin glowing playhead line
  • Interactive mode adds pointer scrubbing and keyboard seeking (arrows, PageUp/PageDown, Home/End)
  • Standard edit/commit events: `arc-input` continuously while scrubbing, `arc-change` once on release
  • Optional `duration` enables monospaced elapsed/total time readouts and spoken time values
  • Full slider ARIA when interactive; labeled image semantics otherwise
  • Resizes by viewBox scaling — no ResizeObserver, no measuring, fluid at any width
  • Playhead motion uses the motion tokens and honors prefers-reduced-motion
  • Server-renders: the SVG is a pure function of props
  • Empty or missing peaks render an empty track rather than an error

Preview

Bars, interactive — drag or use arrow keys to seek
Mirror envelope, static

Usage

Layout and styling work without JavaScript via the HTML/CSS versions. Interactive features like events and state management require the Web Component or a framework wrapper.

<script type="module" src="@arclux/arc-ui"></script>

<!-- Interactive scrubber with time readouts -->
<arc-waveform id="scrubber" interactive duration="212" label="Track position"></arc-waveform>

<!-- Static clip preview, mirror envelope -->
<arc-waveform id="clip" variant="mirror" position="0.6" label="Vocal take"></arc-waveform>

<script type="module">
  const scrubber = document.getElementById('scrubber');

  // Peaks are a property, not an attribute — you compute them (0..1 per bar).
  scrubber.peaks = await fetchPeaks('track-7');

  scrubber.addEventListener('arc-input', (e) => {
    // Live position while dragging: e.detail.value is 0..1,
    // e.detail.time is seconds because duration is set.
    timeDisplay.textContent = format(e.detail.time);
  });
  scrubber.addEventListener('arc-change', (e) => {
    audio.currentTime = e.detail.time; // seek once, on release
  });

  // Follow playback
  audio.addEventListener('timeupdate', () => {
    scrubber.position = audio.currentTime / audio.duration;
  });
</script>
import { Waveform } from '@arclux/arc-ui-react';

export function TrackScrubber({ peaks, audio }: { peaks: number[]; audio: HTMLAudioElement }) {
  return (
    <Waveform
      peaks={peaks}
      duration={audio.duration}
      position={audio.currentTime / audio.duration}
      interactive
      label="Track position"
      onArcInput={(e) => console.log('Live:', e.detail.time)}
      onArcChange={(e) => { audio.currentTime = e.detail.time; }}
    />
  );
}
<script setup>
import { Waveform } from '@arclux/arc-ui-vue';
import { ref } from 'vue';

const peaks = ref([0.2, 0.5, 0.9, 0.4, 0.7, 0.3, 0.8]);
const position = ref(0);
</script>

<template>
  <Waveform
    :peaks="peaks"
    :position="position"
    :duration="212"
    interactive
    label="Track position"
    @arc-input="(e) => console.log('Live:', e.detail.time)"
    @arc-change="(e) => seek(e.detail.time)"
  />
</template>
<script>
  import { Waveform } from '@arclux/arc-ui-svelte';

  let peaks = [0.2, 0.5, 0.9, 0.4, 0.7, 0.3, 0.8];
  let position = 0;
</script>

<Waveform
  {peaks}
  {position}
  duration={212}
  interactive
  label="Track position"
  on:arc-input={(e) => console.log('Live:', e.detail.time)}
  on:arc-change={(e) => seek(e.detail.time)}
/>
import { Component } from '@angular/core';
import { Waveform } from '@arclux/arc-ui-angular';

@Component({
  imports: [Waveform],
  template: `
    <arc-waveform
      [peaks]="peaks"
      [position]="position"
      duration="212"
      interactive
      label="Track position"
      (arc-input)="onScrub($event)"
      (arc-change)="onSeek($event)"
    ></arc-waveform>
  `,
})
export class TrackScrubberComponent {
  peaks = [0.2, 0.5, 0.9, 0.4, 0.7, 0.3, 0.8];
  position = 0;

  onScrub(e: CustomEvent) { console.log('Live:', e.detail.time); }
  onSeek(e: CustomEvent) { this.position = e.detail.value; }
}
import { Waveform } from '@arclux/arc-ui-solid';
import { createSignal } from 'solid-js';

export function TrackScrubber() {
  const [position, setPosition] = createSignal(0);
  const peaks = [0.2, 0.5, 0.9, 0.4, 0.7, 0.3, 0.8];

  return (
    <Waveform
      peaks={peaks}
      position={position()}
      duration={212}
      interactive
      label="Track position"
      onArcChange={(e) => setPosition(e.detail.value)}
    />
  );
}
import { Waveform } from '@arclux/arc-ui-preact';
import { useState } from 'preact/hooks';

export function TrackScrubber() {
  const [position, setPosition] = useState(0);
  const peaks = [0.2, 0.5, 0.9, 0.4, 0.7, 0.3, 0.8];

  return (
    <Waveform
      peaks={peaks}
      position={position}
      duration={212}
      interactive
      label="Track position"
      onArcChange={(e) => setPosition(e.detail.value)}
    />
  );
}

API

peaks Array []
Peak amplitudes as a number array, each value 0 to 1. Property only (no attribute) — set it from script or a framework binding. Values outside the range are clamped. An empty or missing array renders an empty track.
position number 0
Current playhead position as a fraction of the total, 0 to 1. Not seconds: multiply by `duration` yourself if you track seconds. Updated by the component during scrubbing and reflected as an attribute.
duration number null
Total duration in seconds. Optional; when set, time readouts render below the waveform and the slider announces times instead of percentages.
interactive boolean false
Enables scrubbing. The waveform becomes a focusable slider: click or drag to seek, arrow keys to nudge, Home/End to jump. Without it the waveform is a static image.
variant 'bars' | 'mirror' 'bars'
Rendering style. `bars` draws discrete bar pairs mirrored around the center line; `mirror` draws a filled min/max envelope.
label string ''
Accessible name for the waveform. Announced as the slider label when interactive, or as the image description otherwise.

Events

arc-input detail: { value: number, time: number | null }
Fired continuously while scrubbing (every pointer move and each keyboard nudge). `value` is the position fraction 0-1; `time` is seconds when `duration` is set, otherwise null. Use for live preview — updating a time display or audibly scrubbing.
arc-change detail: { value: number, time: number | null }
Fired once when the seek commits: on pointer release, or with each keyboard nudge. Use for the actual seek on your audio source.

See Also