potentio
v0.0.2
Published
Unstyled & accessible knob primitive for React and Vue
Maintainers
Readme
Potentio
Unstyled & accessible knob primitive for React and Vue.
Based on the excellent react-knob-headless by satelllte.
Features
- 🎨 Headless - no styles included, fully customizable
- 🎯 Framework support - React and Vue 3 components
- ♿ Accessible - follows ARIA slider pattern
- 🎹 Keyboard controls - arrow keys, page up/down, home/end
- 🖱️ Pointer controls - mouse and touch support
- 📐 Flexible - linear or non-linear value mapping
- 🔄 Shared logic - React and Vue components share the same core logic
Installation
npm install potentioRunning the Demo
To see both React and Vue implementations in action:
# From the root directory
npm run demo
# Or manually:
cd demo
npm install
npm run devThen open http://localhost:5173 in your browser.
Usage
React
import React, { useState } from "react";
import {
ReactKnobHeadless,
ReactKnobHeadlessLabel,
ReactKnobHeadlessOutput,
useReactKnobKeyboardControls,
} from "potentio";
function App() {
const [value, setValue] = useState(50);
const { onKeyDown } = useReactKnobKeyboardControls({
valueRaw: value,
valueMin: 0,
valueMax: 100,
step: 1,
stepLarger: 10,
onValueRawChange: setValue,
});
return (
<ReactKnobHeadless
aria-label="Volume"
valueRaw={value}
valueMin={0}
valueMax={100}
dragSensitivity={0.006}
valueRawRoundFn={Math.round}
valueRawDisplayFn={(v) => `${v}%`}
onValueRawChange={setValue}
onKeyDown={onKeyDown}
>
{/* Your custom knob UI */}
<div
className="knob-indicator"
style={{
transform: `rotate(${(value / 100) * 270 - 135}deg)`,
}}
/>
</ReactKnobHeadless>
);
}Vue
<template>
<VueKnobHeadless
aria-label="Volume"
:value-raw="value"
:value-min="0"
:value-max="100"
:drag-sensitivity="0.006"
:value-raw-round-fn="Math.round"
:value-raw-display-fn="(v) => `${v}%`"
:on-value-raw-change="setValue"
:wheel-sensitivity="0.01"
@keydown="onKeyDown"
>
<!-- Your custom knob UI -->
<div
class="knob-indicator"
:style="{
transform: `rotate(${(value / 100) * 270 - 135}deg)`,
}"
/>
</VueKnobHeadless>
</template>
<script setup>
import { ref } from "vue";
import {
VueKnobHeadless,
VueKnobHeadlessLabel,
VueKnobHeadlessOutput,
useVueKnobKeyboardControls,
} from "potentio";
const value = ref(50);
const setValue = (newValue) => {
value.value = newValue;
};
const { onKeyDown } = useVueKnobKeyboardControls({
valueRaw: value.value,
valueMin: 0,
valueMax: 100,
step: 1,
stepLarger: 10,
onValueRawChange: setValue,
});
</script>API
Props
| Prop | Type | Description | Required |
| --------------------- | ------------------------------------------------- | ------------------------------------- | -------- |
| valueRaw | number | Current raw value | ✓ |
| valueMin | number | Minimum value | ✓ |
| valueMax | number | Maximum value | ✓ |
| dragSensitivity | number | Sensitivity of drag gesture | ✓ |
| valueRawRoundFn | (value: number) => number | Function to round the raw value | ✓ |
| valueRawDisplayFn | (value: number) => string | Function to format display text | ✓ |
| onValueRawChange | (value: number) => void | Callback when value changes | ✓ |
| axis | 'x' \| 'y' \| 'xy' | Drag axis (default: 'y') | |
| orientation | 'horizontal' \| 'vertical' | DEPRECATED: Use axis instead | |
| includeIntoTabOrder | boolean | Include in tab order (default: false) | |
| mapTo01 | (x: number, min: number, max: number) => number | Map value to 0-1 range | |
| mapFrom01 | (x: number, min: number, max: number) => number | Map from 0-1 range to value | |
| disabled | boolean | Disable all interactions | |
Controls
Mouse/Touch
- Drag: Click and drag to adjust value
- Mouse Wheel: Scroll to adjust value (Vue only)
Keyboard
- Arrow Up/Right: Increase by
step - Arrow Down/Left: Decrease by
step - Page Up: Increase by
stepLarger - Page Down: Decrease by
stepLarger - Home: Set to minimum
- End: Set to maximum
Vue-specific Props
| Prop | Type | Description | Default |
| ------------------ | -------- | -------------------------- | ------- |
| wheelSensitivity | number | Sensitivity of mouse wheel | 0.01 |
Complete Example
React with Label and Output
import React, { useState } from "react";
import {
ReactKnobHeadless,
ReactKnobHeadlessLabel,
ReactKnobHeadlessOutput,
useReactKnobKeyboardControls,
} from "potentio";
function VolumeControl() {
const [volume, setVolume] = useState(50);
const knobId = "volume-knob";
const labelId = "volume-label";
const { onKeyDown } = useReactKnobKeyboardControls({
valueRaw: volume,
valueMin: 0,
valueMax: 100,
step: 1,
stepLarger: 10,
onValueRawChange: setVolume,
});
return (
<div className="volume-control">
<ReactKnobHeadlessLabel id={labelId}>Volume</ReactKnobHeadlessLabel>
<ReactKnobHeadless
id={knobId}
aria-labelledby={labelId}
valueRaw={volume}
valueMin={0}
valueMax={100}
dragSensitivity={0.006}
valueRawRoundFn={Math.round}
valueRawDisplayFn={(v) => `${v}%`}
onValueRawChange={setVolume}
onKeyDown={onKeyDown}
className="knob"
>
<div className="knob-track" />
<div className="knob-thumb" />
</ReactKnobHeadless>
<ReactKnobHeadlessOutput htmlFor={knobId}>
{volume}%
</ReactKnobHeadlessOutput>
</div>
);
}Vue with Label and Output
<template>
<div class="volume-control">
<VueKnobHeadlessLabel :id="labelId"> Volume </VueKnobHeadlessLabel>
<VueKnobHeadless
:id="knobId"
:aria-labelledby="labelId"
:value-raw="volume"
:value-min="0"
:value-max="100"
:drag-sensitivity="0.006"
:value-raw-round-fn="Math.round"
:value-raw-display-fn="(v) => `${v}%`"
:on-value-raw-change="setVolume"
:wheel-sensitivity="0.004"
@keydown="onKeyDown"
class="knob"
>
<div class="knob-track" />
<div class="knob-thumb" />
</VueKnobHeadless>
<VueKnobHeadlessOutput :html-for="knobId">
{{ volume }}%
</VueKnobHeadlessOutput>
</div>
</template>
<script setup>
import { ref } from "vue";
import {
VueKnobHeadless,
VueKnobHeadlessLabel,
VueKnobHeadlessOutput,
useVueKnobKeyboardControls,
} from "potentio";
const volume = ref(50);
const knobId = "volume-knob";
const labelId = "volume-label";
const setVolume = (newValue) => {
volume.value = newValue;
};
const { onKeyDown } = useVueKnobKeyboardControls({
valueRaw: volume.value,
valueMin: 0,
valueMax: 100,
step: 1,
stepLarger: 10,
onValueRawChange: setVolume,
});
</script>Available Exports
// React components
import {
ReactKnobHeadless, // Main knob component
ReactKnobHeadlessLabel, // Accessible label component
ReactKnobHeadlessOutput, // Output display component
useReactKnobKeyboardControls, // Keyboard controls hook
} from "potentio";
// Vue components
import {
VueKnobHeadless, // Main knob component
VueKnobHeadlessLabel, // Accessible label component
VueKnobHeadlessOutput, // Output display component
useVueKnobKeyboardControls, // Keyboard controls composable
} from "potentio";
// Shared utilities (if needed for custom implementations)
import {
calculateDragValue, // Core drag calculation
calculateKeyboardValue, // Keyboard value calculation
getDragAxis, // Axis utility
getAriaOrientation, // ARIA orientation utility
} from "potentio";Known Issues
- Mouse wheel support is only available in the Vue version
- Horizontal axis (
axis: 'x') and XY axis (axis: 'xy') may have issues in the Vue version. PRs welcome!
License
MIT
