| title | Creating Plugins |
|---|---|
| description | |
| nav | 1 |
Plugins allow you to extend Leva with custom input types. They're perfect for domain-specific controls like bezier curves, spring physics, date pickers, or any custom input type your application needs.
A plugin is an object that implements the Plugin interface:
import { Plugin } from 'leva/plugin'
interface MyPluginInput {
value: number
multiplier?: number
}
interface MyPluginSettings {
multiplier: number
}
const myPlugin: Plugin<MyPluginInput, number, MyPluginSettings> = {
component: MyComponent,
normalize: (input) => ({
value: input.value ?? 0,
settings: { multiplier: input.multiplier ?? 1 },
}),
sanitize: (value) => Number(value),
format: (value, settings) => (value * settings.multiplier).toFixed(2),
}The createPlugin function creates a plugin factory function:
import { createPlugin } from 'leva/plugin'
export const myPlugin = createPlugin<MyPluginInput, number, MyPluginSettings>({
component: MyComponent,
normalize: (input) => ({
value: input.value ?? 0,
settings: { multiplier: input.multiplier ?? 1 },
}),
sanitize: (value) => Number(value),
format: (value, settings) => (value * settings.multiplier).toFixed(2),
})Your component receives props from useInputContext:
import { useInputContext, Components } from 'leva/plugin'
import type { LevaInputProps } from 'leva/plugin'
const { Row, Label, Number } = Components
interface MyPluginProps extends LevaInputProps<number, MyPluginSettings> {}
function MyComponent() {
const { label, value, displayValue, onUpdate, settings } = useInputContext<MyPluginProps>()
return (
<Row input>
<Label>{label}</Label>
<Number value={displayValue} onUpdate={(v) => onUpdate(v / settings.multiplier)} />
<span>Γ {settings.multiplier}</span>
</Row>
)
}import { useControls } from 'leva'
import { myPlugin } from './my-plugin'
function MyApp() {
const { value } = useControls({
value: myPlugin({ value: 10, multiplier: 2 }),
})
return <div>{value}</div>
}The React component that renders your input. It receives props through useInputContext.
function MyComponent() {
const props = useInputContext<MyPluginProps>()
// Render your custom UI
return <div>...</div>
}Converts the user input into a normalized { value, settings } object. Called when the input is first registered.
normalize: (input: MyPluginInput, path: string, data: Data) => {
return {
value: input.value ?? defaultValue,
settings: {
multiplier: input.multiplier ?? 1,
},
}
}Validates and sanitizes the value before it's stored. Should throw if the value is invalid.
sanitize: (value: any, settings: MyPluginSettings, prevValue: any, path: string, store: StoreType) => {
const num = Number(value)
if (isNaN(num)) {
throw new Error('Invalid number')
}
return num
}Formats the value for display. The formatted value is available as displayValue in your component.
format: (value: number, settings: MyPluginSettings) => {
return (value * settings.multiplier).toFixed(2)
}Leva provides pre-built components you can use in your plugins:
import { Components } from 'leva/plugin'
const { Row, Label, Number, String, Boolean, Select, Vector, Portal, Overlay, InnerLabel } = ComponentsRow: Container row withinputprop for input rowsLabel: Input label componentNumber: Number inputString: String inputBoolean: Boolean toggleSelect: Select dropdownVector: Vector input (for x, y, z values)Portal: Portal for overlaysOverlay: Overlay componentInnerLabel: Inner label for inputs
import {
useDrag,
useCanvas2d,
useTransform,
useInput,
useValue,
useValues,
useInputSetters,
useInputContext,
useStoreContext,
} from 'leva/plugin'useDrag: Drag gesture hook (see hook docs)useCanvas2d: Canvas with auto-resizeuseTransform: CSS transform helperuseInput: Access input data and methodsuseValue: Subscribe to a single valueuseValues: Subscribe to multiple valuesuseInputSetters: Get setter functionsuseInputContext: Get current input contextuseStoreContext: Get current store
import { createPlugin, useInputContext, Components } from 'leva/plugin'
import type { LevaInputProps } from 'leva/plugin'
const { Row, Label, Number } = Components
interface SliderInput {
value?: number
min?: number
max?: number
step?: number
}
interface SliderSettings {
min: number
max: number
step: number
}
function SliderComponent() {
const { label, value, onUpdate, settings } = useInputContext<LevaInputProps<number, SliderSettings>>()
return (
<Row input>
<Label>{label}</Label>
<input
type="range"
min={settings.min}
max={settings.max}
step={settings.step}
value={value}
onChange={(e) => onUpdate(Number(e.target.value))}
/>
<Number value={value} onUpdate={onUpdate} />
</Row>
)
}
export const slider = createPlugin<SliderInput, number, SliderSettings>({
component: SliderComponent,
normalize: (input) => ({
value: input.value ?? 0,
settings: {
min: input.min ?? 0,
max: input.max ?? 100,
step: input.step ?? 1,
},
}),
sanitize: (value) => {
const num = Number(value)
if (isNaN(num)) throw new Error('Invalid number')
return num
},
})For plugins that work with vectors (like positions, colors, etc.):
import { normalizeVector, sanitizeVector } from 'leva/plugin'
const normalize = (input) => {
const defaultValue = { x: 0, y: 0 }
const defaultSettings = {
x: { min: -100, max: 100, step: 1 },
y: { min: -100, max: 100, step: 1 },
}
return normalizeVector({ ...defaultValue, ...input.value }, defaultSettings)
}
const sanitize = (value, settings, prevValue) => {
return sanitizeVector(value, settings, prevValue)
}Ensure your plugin types are properly exported:
import type { Plugin, LevaInputProps } from 'leva/plugin'
export interface MyPluginInput {
value: number
multiplier?: number
}
export interface MyPluginSettings {
multiplier: number
}
export type MyPluginProps = LevaInputProps<number, MyPluginSettings>- Normalize all inputs: Always provide defaults in
normalize - Validate in sanitize: Throw errors for invalid values
- Use provided components: Leverage Leva's UI components for consistency
- Handle edge cases: Consider empty, null, and undefined values
- Type everything: Use TypeScript for better developer experience
- Test thoroughly: Test with various input values and edge cases
To share your plugin:
- Create a package with your plugin code
- Export the plugin factory function
- Document usage and examples
- Consider publishing to npm (e.g.,
@your-org/leva-plugin-name)
See the official plugins in the Leva repository for complete examples:
packages/plugin-bezierpackages/plugin-springpackages/plugin-plotpackages/plugin-dates
These demonstrate real-world plugin implementations with complex interactions, canvas rendering, and advanced features.