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

Knob

Rotary knob input for continuous parameters, with a glowing 270-degree arc fill, vertical-drag interaction, magnetic detents, and a monospace value readout.

Components Knob
input interactive
<arc-knob>

Overview

Knob is the input a slider cannot be: a rotary control that packs a full parameter range into a compact circular footprint, so a rack of channel strips or a synth panel can put a dozen of them side by side. A 270-degree arc track fills from `min` to the current value, a glowing indicator line marks the position, and the readout below renders in the monospace role with tabular digits so it never shifts width while turning. Interaction follows audio-software convention. Dragging vertically turns the knob — the full range covers about 150 pixels of travel, and holding Shift slows the drag to a tenth for fine adjustment. The mouse wheel and arrow keys step by `step`, Page Up and Page Down jump by ten steps, and Home and End go straight to the rails. The optional `detents` prop names snap values (an array from script, or a comma-separated attribute): each renders as a tick mark around the dial, and a drag snaps magnetically when it lands close — the centre detent on a pan knob, unity on a gain knob. Keyboard and wheel stepping ignore detents, so precise entry is never fought. Knob follows the v3 edit/commit contract: `arc-input` fires continuously while the knob turns and `arc-change` fires once when the turn commits, so a live preview and an expensive save can listen separately. The component participates in forms through ElementInternals, submitting its value under `name`, and the dial is a keyboard-operable `role="slider"` with the full ARIA value set — `format` shapes both the visible readout and the accessible value text, so a screen reader hears "440 Hz" rather than a bare number.

Guidelines

When to use

  • Use Knob where horizontal space is scarce and controls sit in columns — mixer strips, effect panels, tool palettes
  • Provide a `label`; a bare dial gives no clue what parameter it turns
  • Set `format` to include the unit, so both the readout and screen readers announce "440 Hz" rather than "440"
  • Put a detent at the neutral position of a bipolar parameter — 0 on a pan knob, unity on a gain knob
  • Listen to `arc-input` for live audible or visible preview and to `arc-change` for persisting the committed value

When not to use

  • Do not use Knob for a wide-layout single value where a slider fits — a slider shows its whole range at a size a knob cannot
  • Do not use Knob for exact numeric entry — pair it with or replace it by Number Input when users need to type a value
  • Do not scatter detents densely across the range; a magnet every few units makes smooth dragging impossible
  • Do not rely on the dial alone to convey the value — the readout below it is part of the control, so leave it visible

Features

  • A 270-degree SVG arc track with an accent fill from `min` to the current value and a glowing indicator line
  • Synth-style vertical drag with pointer capture; Shift slows the drag to a tenth for fine adjustment
  • Mouse wheel and arrow keys step by `step`, Page Up/Down by ten steps, Home/End to the rails
  • Optional `detents` render tick marks and snap the drag magnetically to named values
  • `format` callback shapes the readout and the ARIA value text, e.g. adding a unit suffix
  • The v3 event contract: `arc-input` continuously while turning, `arc-change` once on commit
  • Form participation via ElementInternals — the value submits under `name` and restores on `form.reset()`
  • Keyboard-operable `role="slider"` dial with `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, and `aria-valuetext`

Preview

Usage

This component requires JavaScript. No pure HTML/CSS version is available — use the Web Component directly or a framework wrapper.

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

<!-- Basic labeled knob -->
<arc-knob label="Volume" value="75" min="0" max="100"></arc-knob>

<!-- Bipolar pan knob with a centre detent -->
<arc-knob label="Pan" value="0" min="-50" max="50" detents="0"></arc-knob>

<!-- Filter cutoff with a formatted readout -->
<arc-knob id="cutoff" label="Cutoff" value="8000" min="200" max="16000" step="100"></arc-knob>

<!-- Disabled state -->
<arc-knob label="Locked" value="30" disabled></arc-knob>

<script>
  const cutoff = document.getElementById('cutoff');
  cutoff.format = (v) => v >= 1000 ? (v / 1000).toFixed(1) + ' kHz' : v + ' Hz';

  // Real-time preview while turning
  cutoff.addEventListener('arc-input', (e) => {
    filterNode.frequency.value = e.detail.value;
  });

  // Commit final value on release
  cutoff.addEventListener('arc-change', (e) => {
    savePatch({ cutoff: e.detail.value });
  });
</script>
import { Knob } from '@arclux/arc-ui-react';
import { useState } from 'react';

function ChannelStrip() {
  const [gain, setGain] = useState(0);
  const [pan, setPan] = useState(0);

  return (
    <div style={{ display: 'flex', gap: 32 }}>
      <Knob
        label="Gain"
        value={gain}
        min={-24}
        max={12}
        step={0.5}
        detents={[0]}
        format={(v) => `${v > 0 ? '+' : ''}${v} dB`}
        onArcInput={(e) => setGain(e.detail.value)}
        onArcChange={(e) => saveGain(e.detail.value)}
      />
      <Knob
        label="Pan"
        value={pan}
        min={-50}
        max={50}
        detents={[0]}
        format={(v) => (v === 0 ? 'C' : v < 0 ? `L${-v}` : `R${v}`)}
        onArcInput={(e) => setPan(e.detail.value)}
      />
    </div>
  );
}
<script setup>
import { Knob } from '@arclux/arc-ui-vue';
import { ref } from 'vue';

const cutoff = ref(8000);
const resonance = ref(35);

const hz = (v) => (v >= 1000 ? (v / 1000).toFixed(1) + ' kHz' : v + ' Hz');
</script>

<template>
  <div style="display:flex; gap:32px;">
    <Knob label="Cutoff" :value="cutoff" :min="200" :max="16000" :step="100"
      :format="hz"
      @arc-input="cutoff = $event.detail.value" />
    <Knob label="Resonance" :value="resonance" :min="0" :max="100"
      :detents="[0, 50, 100]"
      @arc-input="resonance = $event.detail.value" />
  </div>
</template>
<script>
  import { Knob } from '@arclux/arc-ui-svelte';

  let attack = 12;
  let decay = 240;
  let sustain = 70;
  let release = 480;

  const ms = (v) => v + ' ms';
</script>

<div style="display:flex; gap:24px;">
  <Knob label="Attack" value={attack} min={0} max={2000} step={4} format={ms}
    on:arc-input={(e) => attack = e.detail.value} />
  <Knob label="Decay" value={decay} min={0} max={2000} step={4} format={ms}
    on:arc-input={(e) => decay = e.detail.value} />
  <Knob label="Sustain" value={sustain} min={0} max={100}
    on:arc-input={(e) => sustain = e.detail.value} />
  <Knob label="Release" value={release} min={0} max={4000} step={8} format={ms}
    on:arc-input={(e) => release = e.detail.value} />
</div>
import { Component } from '@angular/core';
import { Knob } from '@arclux/arc-ui-angular';

@Component({
  imports: [Knob],
  template: `
    <div style="display:flex; gap:32px;">
      <arc-knob label="Drive" [value]="drive" [min]="0" [max]="100"
        (arc-input)="drive = $event.detail.value"></arc-knob>

      <arc-knob label="Mix" [value]="mix" [min]="0" [max]="100"
        [detents]="[50]"
        (arc-input)="onMixPreview($event.detail.value)"
        (arc-change)="onMixCommit($event.detail.value)"></arc-knob>
    </div>
  `,
})
export class EffectPanelComponent {
  drive = 40;
  mix = 50;

  onMixPreview(val: number) { /* live wet/dry preview */ }
  onMixCommit(val: number) { /* persist to the patch */ }
}
import { Knob } from '@arclux/arc-ui-solid';
import { createSignal } from 'solid-js';

function MasterSection() {
  const [volume, setVolume] = createSignal(75);

  return (
    <div>
      <Knob
        label="Master"
        value={volume()}
        min={0}
        max={100}
        format={(v) => v + '%'}
        onArcInput={(e) => setVolume(e.detail.value)}
        onArcChange={(e) => persistVolume(e.detail.value)}
      />
    </div>
  );
}
import { Knob } from '@arclux/arc-ui-preact';
import { useState } from 'preact/hooks';

function SendControls() {
  const [reverb, setReverb] = useState(20);
  const [delay, setDelay] = useState(0);

  return (
    <div style={{ display: 'flex', gap: 24 }}>
      <Knob label="Reverb" value={reverb} min={0} max={100}
        onArcInput={(e) => setReverb(e.detail.value)} />
      <Knob label="Delay" value={delay} min={0} max={100}
        onArcInput={(e) => setDelay(e.detail.value)} />
    </div>
  );
}
<!-- arc-knob is interactive — requires JS -->
<arc-knob></arc-knob>
<!-- arc-knob is interactive — requires JS -->
<arc-knob></arc-knob>

API

SWEEP number 270
Degrees of arc the knob sweeps; the remaining 90 stay open at the bottom.
DRAG_THROW number 150
Pixels of vertical drag that cover the full range.
DETENT_WINDOW number 0.025
Fraction of the range within which a drag snaps to a detent.
size 'sm' | 'md' | 'lg' 'md'
Control size. `md` is the default; `sm` and `lg` scale the dial.
value number 0
Current knob value. Reflected as an attribute and updated on user interaction.
min number 0
Minimum allowed value at the start of the arc sweep.
max number 100
Maximum allowed value at the end of the arc sweep.
step number 1
Increment granularity. The value snaps to multiples of this number.
name string ''
disabled boolean false
Disables interaction, reducing opacity and blocking pointer events.
label string ''
Label text displayed above the knob in the label typography role.
detents number[] | string []
Snap values, as an array from script or a comma-separated attribute (for example "0,50,100"). While dragging, the value snaps magnetically to a detent within 2.5% of the range, and each detent renders as a tick mark around the dial. Keyboard and wheel stepping ignore detents.
format Function undefined
`(value) => string` shaping the readout and the accessible value text, for example adding a unit suffix. Defaults to the plain number.
readonly boolean false
Prevents dragging, wheel, and key changes while the dial stays focusable and the value still submits.
formAssociated boolean true
properties object { required: { type: Boolean, reflect: true }, readonly: { type: Boolean, reflect: true }, }
Lit merges static properties up the prototype chain, so every consumer gets these without declaring them. `required` participates in constraint validation below; `readonly` reflects for styling and is enforced by each component's interaction handlers (the mixin can't know which gestures mutate state).
autoValidates boolean true
Components that run their own constraint-validation logic (pattern checks, range checks) opt out of the automatic required sync by overriding this to false, and own the whole validity flag set instead.
form
validity
validationMessage
required boolean false

Events

arc-input detail: { value: number }
Fired continuously while the knob is turning — every drag movement, wheel notch, or key step. Use for real-time preview such as a filter cutoff or gain applied live.
arc-change detail: { value: number }
Fired once when a turn commits: on drag release, and after each discrete wheel or key step. Use for persisting the value or triggering an expensive operation.

See Also