Panekit is a headless window manager toolkit for Svelte 5.
The idea behind Panekit originates from old school MDI style GUI from the era of Java Swing and Visual Fox Pro, with a modern touch in the form of tiling modes (Coming soon!) and a declarative style.
If you are planning to use this, beware of the following:
This library has a peer dependency on svelte 5, specifically 5.29 or newer, as it uses attachments to work.
npm i panekit
First, wrap your application with the PanekitProvider. This creates the context needed for window management and sets up a portal target (inside the provider) where panes will be rendered.
<script>
import { PanekitProvider } from 'panekit';
// CSS is optional, but recommended
import "panekit/index.css"
</script>
<PanekitProvider>
<div class="h-dvh w-dvw">
<!-- Your app content -->
</div>
</PanekitProvider>
Panes use a state-driven architecture with PaneState instances that manage all pane properties reactively.
<script>
import { Pane, PaneState } from 'panekit';
// Create a PaneState instance with configuration
const myPane = new PaneState({
size: { width: 400, height: 300 },
constrainToPortal: true,
portalId: 'main-panel'
});
let count = $state(0);
</script>
<Pane.Root paneState={myPane} class="rounded-md border bg-white shadow-md">
<Pane.Handle class="flex items-center justify-center">
Window Title
</Pane.Handle>
<Pane.Content class="flex items-center justify-center">
<button onclick={() => (count += 1)}>
Count: {count}
</button>
</Pane.Content>
</Pane.Root>
PaneState is a reactive class that manages all pane properties. All properties are reactive using runes and can be read or updated at any time. these properties will also be properly tracked if used inside an $effect.
Constructor Options:
new PaneState({
// Identity
id?: string, // Custom pane ID (auto-generated UUID if not provided)
// Layout
size?: { width: number; height: number }, // Initial size (default: 200x200)
position?: { x: number; y: number }, // Initial position (default: centered)
maximised?: boolean, // Start maximized (default: false)
// Portal configuration
portalId?: string, // Target portal ID
constrainToPortal?: boolean, // Keep pane within portal bounds (default: false)
constrainTo?: HTMLElement | string, // Constrain to specific element/selector
// Interaction
canDrag?: boolean, // Enable dragging (default: true)
canResize?: boolean, // Enable resizing (default: true)
dragModifier?: DragModifier, // Modifier key for full-pane drag (default: 'altKey')
// Resize configuration
resizeHandles?: ResizeHandle[], // Which handles to show (default: all 8)
resizeHandleSize?: number, // Handle size in pixels (default: 8)
resizeHandleOffset?: number, // Handle offset from edge (default: 8)
invisibleResizeHandles?: boolean, // Hide handles visually (default: true)
minWidth?: number, // Minimum width constraint
minHeight?: number, // Minimum height constraint
maxWidth?: number, // Maximum width constraint
maxHeight?: number // Maximum height constraint
})
Reactive Properties:
// All properties are reactive and can be read/written
pane.size // { width: number, height: number }
pane.position // { x: number, y: number } | undefined
pane.maximised // boolean
pane.focused // boolean (read-only, managed by manager)
pane.isDragging // boolean (read-only)
pane.isResizing // boolean (read-only)
Methods:
pane.maximize() // Maximize the pane to fill its portal
pane.restore() // Restore to pre-maximized size and position
pane.focus() // Bring pane to front
pane.blur() // Remove focus
Example - Dynamic Configuration:
<script>
const pane = new PaneState({
constrainToPortal: true,
minWidth: 200,
minHeight: 150
});
// Reactively update properties
function toggleMaximize() {
if (pane.maximised) {
pane.restore();
} else {
pane.maximize();
}
}
function makeSmaller() {
pane.size = {
width: pane.size.width - 50,
height: pane.size.height - 50
};
}
</script>
<Pane.Root paneState={pane}>
<Pane.Handle>
Size: {pane.size.width}x{pane.size.height}
</Pane.Handle>
<Pane.Content>
<button onclick={toggleMaximize}>
{pane.maximised ? 'Restore' : 'Maximize'}
</button>
<button onclick={makeSmaller}>Make Smaller</button>
<p>Dragging: {pane.isDragging ? 'Yes' : 'No'}</p>
</Pane.Content>
</Pane.Root>
PanekitProviderThe root provider component that must wrap your application.
Props:
dragModifier?: DragModifier - Global modifier key for full-pane dragging ('altKey' | 'ctrlKey' | 'shiftKey' | 'metaKey'). Default: 'altKey'Pane.RootThe main pane container. Requires a paneState prop.
Props:
paneState: PaneState - (Required) The PaneState instance managing this paneBehavior:
constrainToPortal is enabledPane.HandleThe draggable header/title bar of the pane.
Props:
Pane.ContentThe main content area of the pane.
Props:
Pane.PortalTargetPortal target component for rendering panes. The provider includes one by default, but you can create additional targets.
Props:
portalId?: string - Unique identifier for this portal targetYou can create multiple portal targets to render panes in different areas:
<script>
import { Pane, PaneState } from 'panekit';
const leftPane = new PaneState({
portalId: 'left-panel',
constrainToPortal: true
});
const rightPane = new PaneState({
portalId: 'right-panel',
constrainToPortal: true,
size: { width: 300, height: 250 }
});
</script>
<PanekitProvider>
<div class="flex h-screen">
<div class="flex-1">
<Pane.PortalTarget portalId="left-panel" />
</div>
<div class="flex-1">
<Pane.PortalTarget portalId="right-panel" />
</div>
</div>
<!-- Panes render in their respective portals -->
<Pane.Root paneState={leftPane}>
<Pane.Handle>Left Side Window</Pane.Handle>
<Pane.Content>Content here</Pane.Content>
</Pane.Root>
<Pane.Root paneState={rightPane}>
<Pane.Handle>Right Side Window</Pane.Handle>
<Pane.Content>Content here</Pane.Content>
</Pane.Root>
</PanekitProvider>
The library provides a pane manager for programmatic control:
<script>
import { usePM, PaneState } from 'panekit';
const paneManager = usePM();
const myPane = new PaneState({ id: 'my-pane' });
function focusPane() {
paneManager.focusPane('my-pane');
}
function blurAllPanes() {
paneManager.blurAll();
}
function maximizePane() {
paneManager.maximizePane('my-pane');
}
// Query panes
const allPanes = paneManager.panes; // Get all panes
const pane = paneManager.getPaneById('my-pane'); // Get specific pane
const portalPanes = paneManager.getPanesByPortal('left-panel'); // Get panes in portal
</script>
Panekit is headless and provides minimal default styling. It's designed with utility classes in mind, classes are deduplicated and merged via the cn helper internally, so svelte 5 cslx classes should still work just fine.
You can style based on data attributes if needed. each component has data attributes you can hook into for styling:
[data-pane] - Applied to pane root elements[data-pane-handle] - Applied to handle elements [data-pane-content] - Applied to content elements[data-pane-portal-target] - Applied to portal target elementsMore data attributes will be added so that you can style based on drag state, resize state, focus state and so on. I am just hammered with work so I didn't do it yet.
NOTE: The way we currently generate CSS for the library is a bit wonky and may create conflicts, if anyone has a better idea on how to do this better (maybe I should have just used plain CSS...) I am all ears.
I honestly have no idea.