Floating Panels
Detach panels into movable overlays and dock them back.
Floating panels let users pull part of the workspace out of the tiled layout while keeping it in the same Tilery state. Move and resize a floating panel, then dock it back into the main layout.
This page also shows the difference between floating a whole panel, floating one tab, and opening a native popout window with copied document-head styles.
const initialFloatingLayout: TileryInitialLayout<TabData> = {
type: 'root',
main: {
type: 'group',
direction: 'horizontal',
children: [
{
type: 'panel',
id: 'navigator',
size: 28,
tabs: [
{
id: 'files',
data: {
title: 'Files',
body: 'Docked panels keep using the normal tiled layout tree.',
meta: 'Docked',
},
},
],
},
{
type: 'panel',
id: 'editor',
size: 72,
tabs: [
{
id: 'readme',
data: {
title: 'README.md',
body: 'Floating panels are serialized beside the main layout and layered above it.',
meta: 'Editor',
},
},
],
},
],
},
floating: [
{
type: 'floatingPanel',
id: 'search',
bounds: { x: 44, y: 12, width: 34, height: 48 },
tabs: [
{
id: 'search-tab',
data: {
title: 'Search',
body: 'Drag the empty tab-bar area to move this detached panel, or drag its edges to resize it.',
meta: 'Floating',
},
},
],
},
],
};
<Tilery<TabData>
initialLayout={initialFloatingLayout}
renderTabHeader={renderHeader}
renderTabContent={renderContent}
showActionsButton
/>const runtimeFloatingLayout: TileryInitialLayout<TabData> = {
type: 'group',
direction: 'horizontal',
children: [
{
type: 'panel',
id: 'explorer',
size: 32,
tabs: [
{
id: 'explorer-tab',
data: {
title: 'Explorer',
body: 'This panel can be detached through the button above.',
meta: 'Source',
},
},
],
},
{
type: 'panel',
id: 'workspace',
size: 68,
tabs: [
{
id: 'workspace-tab',
data: {
title: 'Workspace',
body: 'Docking inserts the detached panel back into the tiled tree.',
meta: 'Target',
},
},
],
},
],
};
const tileryRef = useRef<TileryController | null>(null);
const floatExplorer = () => {
tileryRef.current?.floatPanel('explorer', {
x: 10,
y: 14,
width: 34,
height: 52,
resizable: false,
});
};
const dockExplorer = () => {
tileryRef.current?.dockPanel('explorer', {
splitPanel: 'workspace',
direction: 'left',
size: 34,
});
};
const focusExplorer = () => {
tileryRef.current?.focusPanel('explorer');
};
<Tilery<TabData>
ref={tileryRef as React.Ref<TileryController>}
initialLayout={runtimeFloatingLayout}
renderTabHeader={renderHeader}
renderTabContent={renderContent}
showActionsButton
/>const tabFloatingLayout: TileryInitialLayout<TabData> = {
type: 'group',
direction: 'horizontal',
children: [
{
type: 'panel',
id: 'editor',
size: 64,
activeTabId: 'notes-tab',
tabs: [
{
id: 'readme-tab',
data: {
title: 'README.md',
body: 'The editor panel keeps one tab when another tab is extracted.',
meta: 'Editor',
},
},
{
id: 'notes-tab',
data: {
title: 'Notes.md',
body: 'floatTab() creates a new floating panel that contains this tab.',
meta: 'Extractable',
},
},
],
},
{
type: 'panel',
id: 'preview',
size: 36,
tabs: [
{
id: 'preview-tab',
data: {
title: 'Preview',
body: 'Move the notes tab back into the editor to remove its temporary panel.',
meta: 'Target',
},
},
],
},
],
};
const tileryRef = useRef<TileryController | null>(null);
const floatNotes = () => {
tileryRef.current?.floatTab('notes-tab', {
panelId: 'notes-floating',
bounds: { x: 12, y: 16, width: 38, height: 48 },
});
};
const popoutNotes = () => {
tileryRef.current?.popoutTab('notes-tab', {
panelId: 'notes-popout',
floatingBounds: { x: 12, y: 16, width: 38, height: 48 },
windowBounds: { left: 160, top: 110, width: 680, height: 460 },
});
};
const moveNotesBack = () => {
tileryRef.current?.moveTab('notes-tab', {
panel: 'editor',
index: 1,
});
};
<Tilery<TabData>
ref={tileryRef as React.Ref<TileryController>}
initialLayout={tabFloatingLayout}
renderTabHeader={renderHeader}
renderTabContent={renderContent}
showActionsButton
/>const runtimeFloatingLayout: TileryInitialLayout<TabData> = {
type: 'group',
direction: 'horizontal',
children: [
{
type: 'panel',
id: 'explorer',
size: 32,
tabs: [
{
id: 'explorer-tab',
data: {
title: 'Explorer',
body: 'This panel can be detached through the button above.',
meta: 'Source',
},
},
],
},
{
type: 'panel',
id: 'workspace',
size: 68,
tabs: [
{
id: 'workspace-tab',
data: {
title: 'Workspace',
body: 'Docking inserts the detached panel back into the tiled tree.',
meta: 'Target',
},
},
],
},
],
};
const tileryRef = useRef<TileryController | null>(null);
const popoutWorkspace = () => {
tileryRef.current?.popoutPanel('workspace', {
floatingBounds: { x: 12, y: 14, width: 42, height: 56 },
windowBounds: { left: 120, top: 90, width: 760, height: 540 },
});
};
const returnWorkspace = () => {
tileryRef.current?.returnPanelToFloating('workspace');
};
const dockWorkspace = () => {
tileryRef.current?.dockPanel('workspace', {
splitPanel: 'explorer',
direction: 'right',
size: 68,
});
};
<Tilery<TabData>
ref={tileryRef as React.Ref<TileryController>}
initialLayout={runtimeFloatingLayout}
renderTabHeader={renderHeader}
renderTabContent={renderContent}
showActionsButton
/>const popoutStylingLayout: TileryInitialLayout<TabData> = {
type: 'group',
direction: 'horizontal',
children: [
{
type: 'panel',
id: 'scoped-files',
size: 34,
tabs: [
{
id: 'scoped-files-tab',
data: {
title: 'Files',
body: 'This in-page workspace inherits theme variables from a wrapper around Tilery.',
meta: 'Wrapper theme',
},
},
],
},
{
type: 'panel',
id: 'scoped-workspace',
size: 66,
tabs: [
{
id: 'scoped-workspace-tab',
data: {
title: 'Workspace',
body: 'When this panel is popped out, the window receives copied stylesheet rules, but it does not inherit this wrapper element or its inline variables.',
meta: 'Popout styling',
},
},
],
},
],
};
const popoutScopedThemeStyle = {
height: '100%',
'--tilery-bg': '#10201f',
'--tilery-panel-bg': '#162b2a',
'--tilery-tabbar-bg': '#0f2423',
'--tilery-tab-active-bg': '#1f3f3d',
'--tilery-tab-active-fg': '#effcf8',
'--tilery-accent': '#38c7a6',
'--tilery-drop-bg': 'rgba(56, 199, 166, 0.16)',
'--tilery-drop-border': 'rgba(56, 199, 166, 0.55)',
} as CSSProperties;
const tileryRef = useRef<TileryController | null>(null);
const popoutWorkspace = () => {
tileryRef.current?.popoutPanel('scoped-workspace', {
floatingBounds: { x: 16, y: 18, width: 42, height: 52 },
windowBounds: { left: 180, top: 120, width: 720, height: 500 },
});
};
const returnWorkspace = () => {
tileryRef.current?.returnPanelToFloating('scoped-workspace');
};
<Tilery<TabData>
ref={tileryRef as React.Ref<TileryController>}
initialLayout={popoutStylingLayout}
renderTabHeader={renderHeader}
renderTabContent={renderContent}
showActionsButton
/>Related
- Programmatic Control — Use float, dock, popout, and bounds APIs.
- Styling — Make native popout styles survive the separate document.