controls
Define which areas can initiate dragging
The controls
plugin defines which parts of an element can be used to start dragging. Think modal headers, card handles, or toolbar grips - only specific areas should be draggable while the rest remains interactive.
controls({ allow: ControlFrom.selector('.handle') }); // Only handle can drag
controls({ block: ControlFrom.selector('.button') }); // Buttons can't drag
controls({
allow: ControlFrom.selector('.header'),
block: ControlFrom.selector('.close'),
}); // Header drags, close button doesn't
Basic Usage
<script>
import { controls, ControlFrom } from '@neodrag/svelte';
</script>
<!-- Only header can initiate dragging -->
<div
{@attach draggable([
controls({ allow: ControlFrom.selector('.header') }),
])}
>
<div class="header">Drag handle</div>
<div class="content">Content (not draggable)</div>
</div>
<!-- Anywhere except buttons -->
<div
{@attach draggable([
controls({ block: ControlFrom.selector('button') }),
])}
>
<p>Drag anywhere in this area</p>
<button>Can't drag from here</button>
</div>
Complex Control Scenarios
Modal with Multiple Rules
<script>
import {
controls,
ControlFrom,
bounds,
BoundsFrom,
} from '@neodrag/svelte';
</script>
<div
{@attach draggable([
controls({
allow: ControlFrom.selector('.modal-header'),
block: ControlFrom.selector('.modal-close, .modal-body button'),
}),
bounds(BoundsFrom.viewport()),
])}
>
<div class="modal-header">
<h3>Modal Title</h3>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
<p>Modal content</p>
<button>Action Button</button>
</div>
</div>
Dynamic Control Zones
For controls that change during interaction, specify when to recalculate zones:
// Recompute zones on every drag event (for dynamic UIs)
controls(
{
allow: ControlFrom.selector('.handle'),
block: ControlFrom.selector('.disabled'),
},
(ctx) => ctx.hook === 'drag',
);
// Only recompute at drag start (better performance)
controls(
{
allow: ControlFrom.selector('.handle'),
},
(ctx) => ctx.hook === 'start',
);
// Recompute during setup and start
controls(
{
allow: ControlFrom.selector('.handle'),
},
(ctx) => ctx.hook === 'setup' || ctx.hook === 'start',
);
Conditional Control Zones
controls(
{
allow: (root) => {
if (isEditMode) {
return ControlFrom.selector('.edit-handle')(root);
} else {
return ControlFrom.selector('.view-handle')(root);
}
},
},
(ctx) => {
// Recompute when mode changes
return ctx.hook === 'setup' || ctx.hook === 'start';
},
);
Context-Aware Controls
controls(
{
allow: ControlFrom.selector('.handle'),
block: (root) => {
// Block controls based on current state
return ControlFrom.selector(
isCtrlPressed ? '.ctrl-block' : '.normal-block',
)(root);
},
},
(ctx) => {
// Recompute on key events during drag
return ctx.hook === 'drag';
},
);
How It Works
The controls plugin manages drag initiation permissions:
- Finds control zones on setup by querying the DOM
- Recomputes zones based on
shouldRecompute
function during different hooks - Checks click position in
shouldStart
hook - Determines permission based on which zones contain the click point
- Returns boolean to allow/prevent drag start
Zone recomputation: Control zones are recalculated when shouldRecompute
returns true for the current hook.
Zone priority logic:
- If
allow
zones exist and click is outside all of them → block - If click is in both allow and block zones → use
priority
option - Smaller (nested) zones take precedence over larger ones
Performance Considerations
When using dynamic control zones:
// ❌ Expensive - queries DOM on every drag event
controls(
{
allow: ControlFrom.selector('.complex-selector:nth-child(odd)'),
},
(ctx) => ctx.hook === 'drag',
);
// ✅ Better - cache zones, only recompute when needed
controls(
{
allow: ControlFrom.selector('.handle'),
},
(ctx) => {
// Only recompute if DOM structure might have changed
return ctx.hook === 'setup' || ctx.hook === 'start';
},
);
// ✅ Best - static zones for better performance
controls({
allow: ControlFrom.selector('.handle'),
}); // Uses default setup-only recomputation
API Reference
function controls(
options?: {
allow?: ReturnType<
typeof ControlFrom.selector | typeof ControlFrom.elements
>;
block?: ReturnType<
typeof ControlFrom.selector | typeof ControlFrom.elements
>;
priority?: 'allow' | 'block';
} | null,
shouldRecompute?: (ctx: {
hook: 'setup' | 'start' | 'drag' | 'end';
}) => boolean,
): Plugin;
Parameters:
options
- Control configuration objectallow
- Elements that can initiate draggingblock
- Elements that cannot initiate draggingpriority
- Which takes precedence in overlapping zones (‘allow’ | ‘block’)
shouldRecompute
- When to recalculate control zones. Receives a context object with the current hook name.- Default:
(ctx) => ctx.hook === 'setup'
(recompute on plugin initialization only) 'setup'
- Plugin initialization'start'
- Drag start'drag'
- During dragging'end'
- Drag end
- Default:
ControlFrom utilities:
ControlFrom.selector(cssSelector)
- Elements matching CSS selectorControlFrom.elements(nodeList)
- Specific DOM elements
Returns: A plugin object for use with draggable.