A flexible, pointer-based resizer widget for TiddlyWiki. It can resize one tiddler, multiple tiddlers, live DOM elements, paired split panes, real HTML table columns, and CSS Grid column tracks. It supports horizontal and vertical resizing, CSS units, calc() expressions, min/max constraints, optional snap points, double-click preset cycling, CSS variable publishing, pointer/touch input, haptic feedback, double-click reset, action hooks, a dedicated split-pair mode for adjacent panels, a table-column mode for real HTML tables, and a grid-track mode for CSS Grid table-like layouts.
The widget is intended for TiddlyWiki layouts where sizes are stored in state/config tiddlers and reused as CSS values. Normal single and multiple modes resize target values directly. The split-pair mode treats two adjacent panes as one coupled pair: the first pane grows by the drag delta, the second pane shrinks by the same amount, and the pair's total size stays stable. The table-column mode resizes the real <col> element of an HTML table instead of trying to resize each <td> cell individually. The grid-track mode resizes the boundary between two adjacent CSS Grid column tracks, which is the right model for resizable CSS Grid tables with merged cells, ghost cells, and tiddler-driven placement.
The refactored plugin keeps the public widget entry point compatible with the original monolithic widget while moving internal behavior into focused modules. Existing usage should continue to work, and the newer features are additive and opt-in.
- Features
- Installation
- Module Structure
- Quick Start
- Core Concepts
- Widget Attributes
- Action Variables
- Single-Target Resizing
- Multiple-Target Resizing
- Split-Pair Resizing
- Table-Column Resizing
- Grid-Track Resizing
- CSS Grid Table Procedures
- Placed Tiddler Grid Tables
- Snap Points
- Preset Cycling
- CSS Variable Publishing
- Live DOM Resizing
- Double-Click Reset
- CSS
calc()Support - Units and Conversion
- Constraints
- Styling
- Layout Procedures
- TiddlyFlex Column Management
- Debugging and Troubleshooting
- Browser Compatibility
- Contributing
- License
- Credits
- Single-target resizing: Resize one tiddler field, usually
text, and use the saved value as a CSS size. - Multiple-target resizing: Resize several tiddlers at once using the
filterattribute. - Split-pair resizing: Resize two adjacent panes as a coupled pair with
mode="split-pair". - Table-column resizing: Resize the real
<col>of an HTML table withmode="table-column". - Grid-track resizing: Resize the boundary between two adjacent CSS Grid column tracks with
mode="grid-track". - Resizable CSS Grid tables: Build table-like CSS Grid layouts with merged cells, explicit row/column placement, and handles on cell boundaries.
- Tiddler-driven placed grid tables: Generate cells from tagged tiddlers whose
row,col,rowspan, andcolspanfields define placement. - Ghost placeholder cells: Fill unoccupied grid coordinates with empty ghost cells so a declared
rows × columnssystem remains stable. - Horizontal and vertical split pairs: Use
leftTiddler/rightTiddlerfor horizontal pairs andtopTiddler/bottomTiddlerfor vertical pairs. - Stable pair size: In
split-pairmode, the first pane grows while the second pane shrinks, preserving the pair's total width or height. - Snap points: Optionally snap sizes to configured values such as
0px,16rem,33%, or50%. - Double-click preset cycling: Cycle through named size presets on double-click or double-tap.
- CSS variable publishing: Publish the current value to CSS custom properties such as
--btc-sidebar-width. - CSS unit support: Supports
px,%,em,rem,vh,vw,vmin, andvmax. - CSS
calc()support:min,max,default,snap, andsnapDistancecan use supportedcalc()expressions such ascalc(100% - 350px). - Unit preservation: Existing tiddler units are detected and preserved where appropriate.
- Smart pixel conversion: Internal drag calculations are performed in pixels, then converted back to the chosen or original unit.
- Min/max constraints: Prevents panes from shrinking below or growing beyond configured limits.
- Live DOM preview: Optionally updates DOM styles during drag for immediate visual feedback.
- Action hooks: Provides
actions,onBeforeResizeStart,onResizeStart,onResize,onResizeEnd,onReset, anddblClickActions. - Rich action variables: Provides pixel, unit, percentage-of-parent, delta, parent-size, handle-size, snap, preset, table-column, and split-pair variables.
- Pointer events: Works with mouse, pen, and touch through pointer events.
- Pointer capture and document-level tracking: Dragging continues even if the pointer leaves the handle.
- Touch support: Prevents unwanted scroll gestures during drag.
- Haptic feedback: Optional vibration feedback on touch devices.
- Double-click reset: Reset to default/min/max/custom values, or run custom double-click actions.
- Handle styles: Supports visual styles such as
solid,dots,lines,chevron, andgripif the plugin CSS defines them. - Visible portion mode: Optionally calculates resize based on the visible portion of clipped elements.
- Aspect ratio support: Can maintain aspect ratio for live DOM manipulation.
- Compatibility-first refactor: The public widget tiddler remains
$:/plugins/BTC/resizer/modules/widgets/resizer.js, while internal logic is modularized.
- Open the plugin demo or distribution page.
- Drag the plugin into your TiddlyWiki.
- Import the plugin tiddlers.
- Save and reload your wiki.
- Use
<$resizer ... />wherever you need a draggable resize handle.
Typical plugin tiddler:
$:/plugins/BTC/resizer
The widget module itself is expected at:
$:/plugins/BTC/resizer/modules/widgets/resizer.js
The refactored version also includes library modules under:
$:/plugins/BTC/resizer/modules/utils/
$:/plugins/BTC/resizer/modules/interactions/
$:/plugins/BTC/resizer/modules/widgets/
All module tiddlers must be present. Do not import only resizer.js without the required library modules.
The current refactor keeps the public widget shell small and installs behavior onto the same ResizerWidget.prototype from focused modules.
| Module | Purpose |
|---|---|
$:/plugins/BTC/resizer/modules/widgets/resizer.js |
Public widget entry point. Requires and installs the other modules. |
$:/plugins/BTC/resizer/modules/widgets/resizer-render.js |
DOM rendering, handle creation, double-click/double-tap handling, and haptic helper. |
$:/plugins/BTC/resizer/modules/widgets/resizer-lifecycle.js |
Attribute parsing, refresh handling, cleanup, and widget lifecycle methods. |
$:/plugins/BTC/resizer/modules/interactions/event-handlers.js |
Pointer handling, resize operation lifecycle, split-pair flow, grid-track dispatch, live resize, and cleanup of active drags. |
$:/plugins/BTC/resizer/modules/interactions/grid-track.js |
Optional CSS Grid column-boundary resizing for mode="grid-track". Freezes computed grid tracks at drag start/end and saves resolved track widths. |
$:/plugins/BTC/resizer/modules/utils/global-manager.js |
Shared document-level pointermove, pointerup, and pointercancel manager. |
$:/plugins/BTC/resizer/modules/utils/units.js |
Viewport measurement, font-size measurement, unit conversion, calc() evaluation, value formatting, target resolution, tiddler value helpers, and constraints. |
$:/plugins/BTC/resizer/modules/utils/feature-adapters.js |
Adapter methods that attach optional feature modules to the widget prototype. |
$:/plugins/BTC/resizer/modules/utils/snap.js |
Optional snap-point parsing and snapping logic. |
$:/plugins/BTC/resizer/modules/utils/presets.js |
Optional double-click preset cycling. |
$:/plugins/BTC/resizer/modules/utils/table-column.js |
Optional real <col> table-column resizing. |
$:/plugins/BTC/resizer/modules/utils/css-variable.js |
Optional CSS custom property publishing. |
This is still a compatibility refactor, not a behavior rewrite. Existing call sites should continue to use <$resizer ... />.
<$resizer
direction="horizontal"
tiddler="$:/state/sidebar/width"
field="text"
unit="px"
min="200px"
max="800px"
default="350px"
/>Then use the stored value in your layout:
<div style.width={{$:/state/sidebar/width}}>
Sidebar content
</div><$resizer
direction="horizontal"
tiddler="$:/state/sidebar/width"
selector=".my-sidebar"
property="flexBasis"
unit="rem"
min="0px"
max="40rem"
snap="0px 14rem 22rem 34rem"
snapDistance="12px"
snapHaptic="yes"
live="yes"
/><$resizer
direction="horizontal"
tiddler="$:/state/sidebar/width"
selector=".my-sidebar"
property="flexBasis"
unit="rem"
cssVariable="--btc-sidebar-width"
cssVariableTarget="root"
live="yes"
/>Use the published variable:
.my-sidebar {
flex: 0 0 var(--btc-sidebar-width, 22rem);
}<div class="my-horizontal-split">
<div class="my-pane" style.width={{$:/state/split/left}}>Left</div>
<$resizer
class="my-splitter"
direction="horizontal"
mode="split-pair"
unit="%"
element="previousSibling"
leftTiddler="$:/state/split/left"
rightTiddler="$:/state/split/right"
leftField="text"
rightField="text"
min="15%"
splitPairLiveResize="yes"
splitPairSave="end"
/>
<div class="my-pane" style.width={{$:/state/split/right}}>Right</div>
</div>For this structure, element="previousSibling" makes the left pane the primary target. If your resizer is rendered inside the left pane, use element="parent" instead.
<div class="my-vertical-split">
<div class="my-pane" style.height={{$:/state/split/top}}>Top</div>
<$resizer
class="my-horizontal-splitter"
direction="vertical"
mode="split-pair"
unit="%"
element="previousSibling"
topTiddler="$:/state/split/top"
bottomTiddler="$:/state/split/bottom"
topField="text"
bottomField="text"
min="15%"
splitPairLiveResize="yes"
splitPairSave="end"
/>
<div class="my-pane" style.height={{$:/state/split/bottom}}>Bottom</div>
</div><table class="my-table" data-resizer-id="demo-table">
<tr>
<th>Name</th>
<th>Status</th>
<th>Notes</th>
</tr>
<tr>
<td>A</td>
<td>Open</td>
<td>Resizable notes column</td>
</tr>
</table>
<$resizer
direction="horizontal"
mode="table-column"
tableSelector=".my-table"
tableColumnIndex="2"
unit="px"
min="80px"
max="500px"
snap="120px 180px 240px 320px"
tableColumnLiveResize="yes"
tableColumnSave="end"
/>mode="grid-track" is for CSS Grid table-like layouts. The handle is usually rendered inside a grid cell and placed visually at the right boundary of that cell. The widget resizes the boundary between gridTrackIndex and gridTrackIndex + 1: the left track grows while the right track shrinks, so the pair total remains stable.
<div class="btc-rgrid-table btc-rgrid-instance-demo" style="--btc-rgrid-columns-template: var(--btc-rgrid-col-1, 25%) var(--btc-rgrid-col-2, 25%) var(--btc-rgrid-col-3, 25%) var(--btc-rgrid-col-4, 25%);">
<div class="btc-rgrid-content">
<div class="btc-rgrid-cell" style="grid-area: 1 / 1 / span 1 / span 1;">
<div class="btc-rgrid-cell-content">A</div>
<$resizer
mode="grid-track"
class="btc-rgrid-cell-resizer"
direction="horizontal"
gridSelector=".btc-rgrid-instance-demo"
gridTrackIndex="1"
gridTrackStatePrefix="$:/state/grid/demo"
gridTrackMin="4%"
gridTrackMax="90%"
gridTrackLive="yes"
gridTrackSave="end"
gridTrackLiveUnit="px"
gridTrackSaveUnit="px"
gridTrackFreezeOnStart="yes"
gridTrackFreezeOnEnd="yes"
/>
</div>
</div>
</div>For real use, prefer the CSS Grid table procedures later in this README instead of hand-writing the grid every time.
direction controls which pointer axis is used and which CSS property is assumed by default.
| Direction | Pointer delta | Default property | Parent measurement |
|---|---|---|---|
horizontal |
clientX movement |
width |
parent width |
vertical |
clientY movement |
height |
parent height |
For flexbox layouts, explicitly use property="flexBasis" when possible.
| Mode | Purpose |
|---|---|
single |
Resize one tiddler or one target value. |
multiple |
Resize multiple tiddlers selected by filter. |
split-pair |
Resize two adjacent panes as a coupled pair. |
table-column |
Resize a real HTML table column via <colgroup> and <col>. |
grid-track |
Resize a CSS Grid column boundary by changing adjacent column track variables. |
The widget separates two ideas:
- Target DOM element: the actual element measured or optionally live-resized.
- State tiddler: the tiddler field where the resulting value is stored.
For normal resizing, tiddler or filter controls which tiddler(s) are written. For DOM measurement, use element or selector.
For split-pair, the target DOM element should be the first pane in the pair:
horizontal: targetElement = left pane
vertical: targetElement = top pane
The second pane is normally found via:
primaryElement.nextElementSiblingYou can override this with rightSelector or bottomSelector.
For table-column, the target is the table column itself. The widget creates or reuses a <colgroup> and updates the corresponding <col>.
| Attribute | Description | Default |
|---|---|---|
direction |
Resize direction: horizontal or vertical. |
horizontal |
mode |
Resize mode: single, multiple, split-pair, table-column, or grid-track. |
single |
tiddler |
Target tiddler for single mode. Also used as fallback primary tiddler in split-pair. |
— |
filter |
Filter expression or title list for multiple tiddlers. | — |
field |
Field to update in normal modes. | text |
unit |
Output unit for generated values. | px |
default |
Default value if no tiddler value exists. Supports supported units and calc(). |
200px or 50% depending on unit |
min |
Minimum value. Supports supported units and calc(). |
— |
max |
Maximum value. Supports supported units and calc(). |
— |
| Attribute | Description | Default |
|---|---|---|
invert |
Invert drag direction. Use yes or no. |
no |
live |
Directly update target DOM element during drag. | no |
position |
Parent-size measurement style: absolute uses getBoundingClientRect(), relative uses offset size. |
absolute |
property |
CSS property to modify for live DOM resizing. | width for horizontal, height for vertical |
aspectRatio |
Maintain aspect ratio during live DOM resize, e.g. 16:9 or 1.5. |
— |
visiblePortion |
Use only visible part of target element for measurement. Useful for clipped sidebars/panels. | no |
disable |
Disable pointer interaction. Adds disabled class/attribute. | no |
| Attribute | Description | Default |
|---|---|---|
selector |
CSS selector for the target DOM element. | — |
element |
Relative target element: parent, parent.parent, previousSibling, nextSibling. |
depends on handle position |
handlePosition |
Handle insertion/interpretation: before, after, overlay. |
after |
element is especially important in split-pair mode. It should resolve to the first pane of the pair, not to the whole container.
Correct for split-pair:
targetElement = left/top pane
targetElement.parentElement = whole split container
targetElement.nextElementSibling = right/bottom pane
Wrong for split-pair:
targetElement = whole split container
Snap points are optional. They are active only when snap is set.
| Attribute | Description | Default |
|---|---|---|
snap |
List of snap values. Values may be separated by spaces, commas, semicolons, pipes, or newlines. Supports calc(). |
— |
snapDistance |
Maximum distance from a snap point before snapping occurs. Supports units and calc(). |
8px |
snapHaptic |
Trigger a short vibration when snapping, if hapticFeedback="yes" and the browser supports vibration. |
no |
Example:
<$resizer
tiddler="$:/state/sidebar/width"
unit="rem"
snap="0px 14rem 22rem 34rem calc(100vw - 20rem)"
snapDistance="12px"
/>Preset cycling is optional. It is active only when presetCycle="yes" and presets is set. It runs on double-click/double-tap, unless dblClickActions is set. Existing dblClickActions always take priority.
| Attribute | Description | Default |
|---|---|---|
presetCycle |
Enable built-in double-click preset cycling. Use yes or no. |
no |
presets |
Preset list. Use name:value pairs separated by semicolons, pipes, or newlines. Plain values are also accepted. |
— |
presetTiddler |
Optional tiddler that stores the active preset name. | — |
presetField |
Field for presetTiddler. |
text |
presetIndexTiddler |
Optional tiddler that stores the active preset index. | — |
presetIndexField |
Field for presetIndexTiddler. |
text |
Example:
<$resizer
tiddler="$:/state/sidebar/width"
unit="rem"
presetCycle="yes"
presets="closed:0px;narrow:14rem;normal:22rem;wide:34rem"
presetTiddler="$:/state/sidebar/mode"
presetIndexTiddler="$:/state/sidebar/preset-index"
/>CSS variable publishing is optional and additive. It does not replace tiddler writes or live DOM styles.
| Attribute | Description | Default |
|---|---|---|
cssVariable |
CSS custom property name to publish, e.g. --btc-sidebar-width. A missing leading -- is added automatically. |
— |
cssVariableSecondary |
Secondary variable name for secondary split-pair values. | — |
cssVariableTarget |
Where to publish: target, parent, root, selector, or a direct CSS selector such as .layout. |
target |
cssVariableSelector |
Selector used when cssVariableTarget="selector". |
— |
leftCssVariable |
Horizontal split-pair variable for the left pane. | — |
rightCssVariable |
Horizontal split-pair variable for the right pane. | — |
topCssVariable |
Vertical split-pair variable for the top pane. | — |
bottomCssVariable |
Vertical split-pair variable for the bottom pane. | — |
Examples:
<$resizer cssVariable="--btc-sidebar-width" cssVariableTarget="root" />
<$resizer cssVariable="btc-panel-size" cssVariableTarget="parent" />
<$resizer cssVariable="--btc-size" cssVariableTarget="selector" cssVariableSelector=".my-layout" />These attributes are used when mode="table-column".
| Attribute | Description | Default |
|---|---|---|
tableSelector |
CSS selector for the table to resize. If omitted, the widget tries to find the closest table from the clicked cell/handle. | — |
tableColumnIndex |
Zero-based column index. If omitted, the index is inferred from the clicked td/th. |
0 fallback |
tableColumnId |
Stable table id used in generated state tiddler names. | table data-resizer-id, table id, table class, or table |
tableColumnTiddler |
Explicit state tiddler for the column width. | generated from prefix/table/id/index |
tableColumnTiddlerPrefix |
Prefix used for generated state tiddlers. | $:/state/resizer/table-columns/ |
tableColumnLiveResize |
Apply width to the <col> while dragging. |
value of live |
tableColumnSave |
Save during drag or only on pointerup. Use drag or end. |
drag |
Generated tiddler format:
<tableColumnTiddlerPrefix><tableId>/<columnIndex>
For example:
$:/state/resizer/table-columns/demo-table/2
These attributes are used when mode="grid-track". This mode is for CSS Grid column boundaries, not real HTML <table> elements. It is especially useful for CSS Grid table procedures with merged cells.
| Attribute | Description | Default |
|---|---|---|
gridSelector |
CSS selector for the grid instance whose column variables should be changed. | — |
gridTrackIndex |
One-based boundary index. Resizes column N and column N+1. |
1 |
gridTrackStatePrefix |
Prefix for saved column state tiddlers. Column N is stored as <prefix>/col-N. |
$:/state/grid |
gridTrackField |
Field to write on generated state tiddlers. | text |
gridTrackUnit |
Logical unit for constraints and defaults. | value of unit or % |
gridTrackMin |
Minimum size for either track in the resized pair. | value of min or 4% |
gridTrackMax |
Optional maximum size for the left track in the resized pair. | value of max |
gridTrackSnap |
Optional snap values for the left track. | value of snap |
gridTrackSnapDistance |
Distance from snap point before snapping. | value of snapDistance or 0px |
gridTrackCssVariablePrefix |
Prefix for CSS custom properties. Column N becomes <prefix>N. |
--btc-rgrid-col- |
gridTrackLive |
Apply track CSS variables during drag. | yes |
gridTrackSave |
Save track state during drag, on end, or never: drag, end, none. |
end |
gridTrackLiveUnit |
Unit written to CSS variables while dragging. px is recommended for stability. |
px |
gridTrackSaveUnit |
Unit saved to state tiddlers. px is recommended after manual resizing. |
px |
gridTrackFreezeOnStart |
Freeze all computed grid tracks to exact pixel variables on pointerdown. | yes |
gridTrackFreezeOnEnd |
Re-read and save browser-resolved computed tracks on pointerup. | yes |
The grid-track mode deliberately changes the pair of adjacent tracks, not the visual cell element. A handle on the right edge of a cell spanning columns 2-4 should use gridTrackIndex="4", because the resizable boundary is after column 4.
Example:
<$resizer
mode="grid-track"
class="btc-rgrid-cell-resizer"
direction="horizontal"
gridSelector=".btc-rgrid-instance-my-grid"
gridTrackIndex="2"
gridTrackStatePrefix="$:/state/my-grid"
gridTrackMin="4%"
gridTrackMax="90%"
gridTrackLive="yes"
gridTrackSave="end"
gridTrackLiveUnit="px"
gridTrackSaveUnit="px"
gridTrackFreezeOnStart="yes"
gridTrackFreezeOnEnd="yes"
/>These attributes are only used when mode="split-pair".
| Attribute | Direction | Description | Default/fallback |
|---|---|---|---|
leftTiddler |
horizontal | Tiddler storing the left pane size. | tiddler or first filter result |
rightTiddler |
horizontal | Tiddler storing the right pane size. | second filter result |
leftField |
horizontal | Field to write on leftTiddler. |
field or text |
rightField |
horizontal | Field to write on rightTiddler. |
field or text |
leftSelector |
horizontal | CSS selector for the left pane. | resolved target element |
rightSelector |
horizontal | CSS selector for the right pane. | leftElement.nextElementSibling |
topTiddler |
vertical | Tiddler storing the top pane size. | tiddler or first filter result |
bottomTiddler |
vertical | Tiddler storing the bottom pane size. | second filter result |
topField |
vertical | Field to write on topTiddler. |
field or text |
bottomField |
vertical | Field to write on bottomTiddler. |
field or text |
topSelector |
vertical | CSS selector for the top pane. | resolved target element |
bottomSelector |
vertical | CSS selector for the bottom pane. | topElement.nextElementSibling |
splitPairLiveResize |
both | Apply DOM style changes while dragging. Also updates flex-basis for flex layouts. |
value of live |
splitPairSave |
both | Save split-pair tiddler values during drag or only at the end. Use end for smooth resizing without repeated TiddlyWiki refreshes. |
end |
| Attribute | Description |
|---|---|
actions |
Action string to execute whenever values change. |
onBeforeResizeStart |
Action string executed after start measurements are collected but before normal drag handling. |
onResizeStart |
Action string executed when resize starts. |
onResize |
Action string executed during resize. |
onResizeEnd |
Action string executed when resize ends. |
onReset |
Action string executed after reset. |
dblClickActions |
Custom double-click actions. Overrides preset cycling and built-in reset behavior. |
| Attribute | Description | Default |
|---|---|---|
class |
Additional CSS class(es) for the resizer handle. | empty |
handleStyle |
Visual style: solid, dots, lines, chevron, grip. |
solid |
disable |
Adds tc-resizer-disabled and prevents interaction. |
no |
| Attribute | Description | Default |
|---|---|---|
resetTo |
Reset target: default, min, max, or custom. Ignored when dblClickActions is set. Also comes after preset cycling if presetCycle="yes". |
default |
resetValue |
Custom reset value when resetTo="custom". |
— |
smoothReset |
Animate reset transition. | yes |
onReset |
Actions after reset. | — |
Double-click priority is:
1. dblClickActions, if set
2. presetCycle="yes" with presets, if set
3. built-in reset behavior
| Attribute | Description | Default |
|---|---|---|
hapticFeedback |
Enable vibration feedback on supported devices. | yes |
hapticDebug |
Log haptic availability/results to console. | no |
Action variables are set before invoking event/action strings. They let you inspect or reuse the current drag state in Wikitext actions.
| Variable | Description | Available in |
|---|---|---|
<<tv-action-value>> |
Numeric value in the widget's output unit. | all action callbacks |
<<tv-action-value-pixels>> |
Current value in pixels. | all action callbacks |
<<tv-action-formatted-value>> |
Current value with unit suffix. | all action callbacks |
<<tv-action-value-percent-of-parent>> |
Current value as percentage of frozen parent size. | resize callbacks, split-pair callbacks |
<<tv-action-formatted-value-percent-of-parent>> |
Percentage-of-parent with % suffix. |
resize callbacks, split-pair callbacks |
<<tv-action-direction>> |
horizontal or vertical. |
all action callbacks |
<<tv-action-property>> |
CSS property being modified. | resize callbacks |
<<tv-action-parent-size>> |
Frozen parent size at drag start, in pixels. | resize callbacks |
<<tv-action-handle-size>> |
Computed handle size in pixels. | resize callbacks |
<<tv-action-delta-x>> |
Horizontal pointer delta in pixels. | onResize |
<<tv-action-delta-y>> |
Vertical pointer delta in pixels. | onResize |
<<tv-action-delta-pixels>> |
Directional delta in pixels, clamped where applicable. | resize callbacks |
<<tv-action-delta-percent-of-parent>> |
Directional delta as percentage of parent size. | resize callbacks |
<<tv-action-formatted-delta-percent-of-parent>> |
Delta percentage with % suffix. |
resize callbacks |
<<tv-action-phase>> |
Current phase such as resize, resize-start, resize-end, or actions. |
split-pair callbacks |
When snap points are active, the operation may expose snap-related state. The most useful values are:
| Variable/state | Description |
|---|---|
operation.snapResult.snapped |
true if the current update snapped to a point. |
operation.snapResult.snapPoint |
Raw configured snap point, such as 22rem or 50%. |
operation.snapResult.pixelValue |
Snapped pixel value. |
operation.snapResult.distance |
Distance from the unsnapped value to the chosen snap point. |
If you expose these in custom callbacks, prefer naming them consistently, for example:
<<tv-action-snapped>>
<<tv-action-snap-point>>
<<tv-action-snap-distance>>
When built-in preset cycling runs, these variables are set:
| Variable | Description |
|---|---|
<<tv-action-preset-name>> |
Name of the applied preset. |
<<tv-action-preset-value>> |
Value of the applied preset. |
<<tv-action-preset-index>> |
Zero-based index of the applied preset. |
When mode="table-column" updates a column, these variables are set:
| Variable | Description |
|---|---|
<<tv-action-table-column>> |
Set to yes for table-column updates. |
<<tv-action-table-column-index>> |
Zero-based column index. |
<<tv-action-table-column-tiddler>> |
State tiddler used for the column width. |
<<tv-action-value-pixels>> |
Current column width in pixels. |
<<tv-action-formatted-value>> |
Current column width with unit suffix. |
When mode="grid-track" is active, the implementation works with these concepts internally and may expose them to callbacks if action-variable support is extended for grid-track mode.
| Variable/concept | Description |
|---|---|
gridTrackIndex |
One-based left column index of the resized boundary pair. |
gridTrackIndex + 1 |
Right column index of the resized boundary pair. |
gridTrackStatePrefix |
Prefix used for saved column tiddlers. |
--btc-rgrid-col-N |
CSS variable updated for column N. |
| computed track pixels | Browser-resolved grid track sizes read from grid-template-columns. |
Current recommended practice is to let gridTrackFreezeOnStart="yes" and gridTrackFreezeOnEnd="yes" stabilize all browser-computed column widths. This prevents first-drag and end-of-drag jumps caused by switching between percentage/default tracks and explicit pixel tracks.
These exist in mode="split-pair" for both horizontal and vertical layouts.
| Variable | Description |
|---|---|
<<tv-action-split-pair>> |
Set to yes in split-pair mode. |
<<tv-action-split-pair-direction>> |
horizontal or vertical. |
<<tv-action-primary-tiddler>> |
Left/top tiddler title. |
<<tv-action-secondary-tiddler>> |
Right/bottom tiddler title. |
<<tv-action-primary-field>> |
Left/top field. |
<<tv-action-secondary-field>> |
Right/bottom field. |
<<tv-action-primary-value-pixels>> |
Left/top current size in pixels. |
<<tv-action-secondary-value-pixels>> |
Right/bottom current size in pixels. |
<<tv-action-primary-value-percent-of-parent>> |
Left/top current size as percentage of parent. |
<<tv-action-secondary-value-percent-of-parent>> |
Right/bottom current size as percentage of parent. |
<<tv-action-formatted-primary-value-percent-of-parent>> |
Left/top percentage with %. |
<<tv-action-formatted-secondary-value-percent-of-parent>> |
Right/bottom percentage with %. |
<<tv-action-primary-formatted-value>> |
Left/top formatted value in widget unit. |
<<tv-action-secondary-formatted-value>> |
Right/bottom formatted value in widget unit. |
<<tv-action-primary-start-value-pixels>> |
Left/top size at drag start. |
<<tv-action-secondary-start-value-pixels>> |
Right/bottom size at drag start. |
<<tv-action-pair-size-pixels>> |
Sum of both panes at drag start. |
<<tv-action-pair-size-percent-of-parent>> |
Pair size as percentage of parent. |
<<tv-action-formatted-pair-size-percent-of-parent>> |
Pair percentage with %. |
<<tv-action-min-value-pixels>> |
Computed minimum value in pixels. |
<<tv-action-max-value-pixels>> |
Computed maximum value in pixels, if any. |
<<tv-action-requested-delta-pixels>> |
Raw requested delta before pair clamping. |
<<tv-action-requested-delta-percent-of-parent>> |
Raw requested delta as parent percentage. |
Only set for direction="horizontal".
| Variable | Description |
|---|---|
<<tv-action-left-tiddler>> |
Left tiddler. |
<<tv-action-right-tiddler>> |
Right tiddler. |
<<tv-action-left-field>> |
Left field. |
<<tv-action-right-field>> |
Right field. |
<<tv-action-left-value-pixels>> |
Left size in pixels. |
<<tv-action-right-value-pixels>> |
Right size in pixels. |
<<tv-action-left-value-percent-of-parent>> |
Left size as percentage of parent. |
<<tv-action-right-value-percent-of-parent>> |
Right size as percentage of parent. |
<<tv-action-formatted-left-value-percent-of-parent>> |
Left percentage with %. |
<<tv-action-formatted-right-value-percent-of-parent>> |
Right percentage with %. |
<<tv-action-left-formatted-value>> |
Left formatted value. |
<<tv-action-right-formatted-value>> |
Right formatted value. |
Only set for direction="vertical".
| Variable | Description |
|---|---|
<<tv-action-top-tiddler>> |
Top tiddler. |
<<tv-action-bottom-tiddler>> |
Bottom tiddler. |
<<tv-action-top-field>> |
Top field. |
<<tv-action-bottom-field>> |
Bottom field. |
<<tv-action-top-value-pixels>> |
Top size in pixels. |
<<tv-action-bottom-value-pixels>> |
Bottom size in pixels. |
<<tv-action-top-value-percent-of-parent>> |
Top size as percentage of parent. |
<<tv-action-bottom-value-percent-of-parent>> |
Bottom size as percentage of parent. |
<<tv-action-formatted-top-value-percent-of-parent>> |
Top percentage with %. |
<<tv-action-formatted-bottom-value-percent-of-parent>> |
Bottom percentage with %. |
<<tv-action-top-formatted-value>> |
Top formatted value. |
<<tv-action-bottom-formatted-value>> |
Bottom formatted value. |
When using dblClickActions, these variables are available:
| Variable | Description |
|---|---|
<<tv-action-value>> |
Current value. |
<<tv-action-value-pixels>> |
Current value in pixels. |
<<tv-action-direction>> |
Direction. |
<<tv-action-parent-size>> |
Parent size in pixels. |
<<tv-action-handle-size>> |
Handle size in pixels. |
Single-target resizing writes one value into one tiddler field.
<$resizer
direction="horizontal"
tiddler="$:/state/panel/width"
field="text"
unit="px"
min="200px"
max="800px"
default="350px"
/>Use the value:
<div class="panel" style.width={{$:/state/panel/width}}>
Panel
</div>Vertical example:
<$resizer
direction="vertical"
tiddler="$:/state/header/height"
field="text"
unit="px"
min="48px"
max="240px"
default="96px"
/>For flexbox, prefer:
<$resizer
direction="horizontal"
tiddler="$:/state/sidebar/width"
selector=".sidebar"
property="flexBasis"
live="yes"
/>Use filter to resize several tiddlers at once.
<$resizer
direction="horizontal"
filter="$:/metrics/storyright $:/metrics/storywidth $:/metrics/tiddlerwidth"
unit="px"
min="300px"
max="calc(100vw - 350px)"
/>You can also use a normal TiddlyWiki filter expression:
<$resizer
direction="horizontal"
filter="[tag[layout-metric]]"
unit="px"
min="100px"
max="800px"
/>In multiple mode, each target's original unit is detected and preserved where possible. Internally, each value is converted to pixels, constrained, and then converted back.
split-pair mode is for adjacent split panes. It is the recommended mode for resizable columns and stacked panes.
At drag start, the widget measures:
parent size
primary pane size
secondary pane size
pair size = primary + secondary
During drag:
primary = primaryStart + delta
secondary = pairStart - primary
So the pair stays stable while the divider moves.
<div class="split split-horizontal">
<div class="pane" style.width={{$:/state/split/left}}>Left</div>
<$resizer
class="splitter splitter-vertical"
direction="horizontal"
mode="split-pair"
unit="%"
element="previousSibling"
leftTiddler="$:/state/split/left"
rightTiddler="$:/state/split/right"
leftField="text"
rightField="text"
min="15%"
splitPairLiveResize="yes"
splitPairSave="end"
/>
<div class="pane" style.width={{$:/state/split/right}}>Right</div>
</div><div class="split split-horizontal btc-split">
<div class="pane left-pane">Left</div>
<$resizer
class="splitter splitter-vertical"
direction="horizontal"
mode="split-pair"
unit="%"
element="previousSibling"
leftTiddler="$:/state/split/left"
rightTiddler="$:/state/split/right"
leftCssVariable="--btc-left-width"
rightCssVariable="--btc-right-width"
cssVariableTarget="root"
min="15%"
splitPairLiveResize="yes"
splitPairSave="end"
/>
<div class="pane right-pane">Right</div>
</div>.left-pane {
flex: 0 0 var(--btc-left-width, 50%);
}
.right-pane {
flex: 0 0 var(--btc-right-width, 50%);
}<div class="split split-vertical">
<div class="pane" style.height={{$:/state/split/top}}>Top</div>
<$resizer
class="splitter splitter-horizontal"
direction="vertical"
mode="split-pair"
unit="%"
element="previousSibling"
topTiddler="$:/state/split/top"
bottomTiddler="$:/state/split/bottom"
topField="text"
bottomField="text"
min="15%"
splitPairLiveResize="yes"
splitPairSave="end"
/>
<div class="pane" style.height={{$:/state/split/bottom}}>Bottom</div>
</div>If the resizer is rendered inside the left/top pane, use:
element="parent"Example for dynamic TiddlyWiki columns:
<$resizer
class="tc-flexbox-story-column-resizer-flexbox"
direction="horizontal"
mode="split-pair"
min="15%"
default=<<cellWidth>>
unit="%"
element="parent"
leftTiddler={{{ [<stateTiddlerPrefix>addsuffix<colIndex>] }}}
rightTiddler={{{ [<stateTiddlerPrefix>addsuffix<nextColIndex>] }}}
leftField="text"
rightField="text"
splitPairLiveResize="yes"
splitPairSave="end"
/>For this pattern, the target element must be the left/top pane itself. The widget then finds the right/bottom pane with targetElement.nextElementSibling.
For horizontal flex columns:
.split-horizontal {
display: flex;
flex-direction: row;
width: 100%;
min-width: 0;
}
.split-horizontal > .pane {
flex-grow: 0;
flex-shrink: 0;
min-width: 0;
box-sizing: border-box;
}For vertical flex panes:
.split-vertical {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.split-vertical > .pane {
flex-grow: 0;
flex-shrink: 0;
min-height: 0;
box-sizing: border-box;
}splitPairLiveResize="yes" updates the DOM styles and flex-basis during drag. Combine it with splitPairSave="end" so the widget saves the final tiddler values only once on pointer release. This avoids repeated TiddlyWiki refreshes while dragging and gives the smoothest split-pair resizing.
Do not use split-pair if you only want to change one stored value. Use normal single mode instead.
Do not also run Wikitext onResize actions that rewrite the same pair tiddlers unless you intentionally want custom override logic. Otherwise you will have two systems writing sizes at once.
mode="table-column" resizes an HTML table column by creating or reusing a <colgroup> and changing the width of the correct <col>.
This is the correct model for real table resizing. Do not resize every <td> in a column individually. Cells are table contents; the column model belongs in <colgroup>.
<table class="my-table" data-resizer-id="issues">
<tr>
<th>Issue</th>
<th>Status</th>
<th>Notes</th>
</tr>
<tr>
<td>#1</td>
<td>Open</td>
<td>Long note text</td>
</tr>
</table>
<$resizer
direction="horizontal"
mode="table-column"
tableSelector=".my-table"
tableColumnIndex="2"
unit="px"
min="80px"
max="500px"
tableColumnLiveResize="yes"
tableColumnSave="end"
/>If tableColumnIndex is omitted, the widget tries to infer the column index from the clicked td or th.
This is useful when the resizer handle is rendered inside a cell:
<th>
Notes
<$resizer
mode="table-column"
direction="horizontal"
unit="px"
min="80px"
max="500px"
tableColumnLiveResize="yes"
/>
</th>Use tableColumnId or data-resizer-id for stable state tiddler names:
<table class="my-table" data-resizer-id="project-table">or:
<$resizer
mode="table-column"
tableSelector=".my-table"
tableColumnId="project-table"
/>Without a stable id, class names can become part of the generated tiddler title. That works, but it is less predictable.
.my-table {
table-layout: fixed;
width: 100%;
}
.my-table th,
.my-table td {
overflow: hidden;
text-overflow: ellipsis;
}The widget also sets table.style.tableLayout = "fixed" during live column resizing if no table layout is already set.
Use tableColumnSave="drag" if you want the state tiddler updated continuously.
Use tableColumnSave="end" if you want a smoother drag with one final write on pointerup.
<$resizer
mode="table-column"
tableColumnSave="end"
tableColumnLiveResize="yes"
/>mode="grid-track" is for CSS Grid layouts where columns are defined by CSS variables such as:
--btc-rgrid-columns-template: var(--btc-rgrid-col-1, 25%) var(--btc-rgrid-col-2, 25%) var(--btc-rgrid-col-3, 25%) var(--btc-rgrid-col-4, 25%);A grid-track handle resizes a boundary between two adjacent tracks. If the handle uses gridTrackIndex="2", then column 2 and column 3 are resized as a pair:
column 2 grows -> column 3 shrinks
column 2 shrinks -> column 3 grows
The pair total is preserved, so the grid stays stable. This is different from resizing a single DOM element. The visual handle may be placed on the edge of a cell, but the resized thing is the underlying grid track boundary.
A CSS Grid table often starts with percentage defaults such as 25% 25% 25% 25%. After manual resizing, pixel tracks are more stable because they match the browser's computed layout exactly. gridTrackFreezeOnStart="yes" freezes all current tracks to exact pixel variables before the drag moves. gridTrackFreezeOnEnd="yes" saves the final browser-resolved track values on pointerup.
Recommended settings:
<$resizer
mode="grid-track"
gridTrackLiveUnit="px"
gridTrackSaveUnit="px"
gridTrackFreezeOnStart="yes"
gridTrackFreezeOnEnd="yes"
/>The handle should be visually placed at a grid boundary. For CSS Grid table procedures, render the handle inside the cell that ends at that boundary. For the last visual cell in a row, do not render a handle because there is no column to the right.
For a cell with col="2" and colSpan="3", the right boundary is after column 4:
boundaryIndex = col + colSpan - 1
So the handle should use:
gridTrackIndex="4"The plugin can be used with Wikitext procedures that create CSS Grid table-like layouts. These are not native HTML <table> elements; they are CSS Grid layouts that support merged cells naturally.
A minimal procedure layer usually has:
btc.rgrid.track.table
btc.rgrid.track.cell
btc.rgrid.track.header
btc.rgrid.track.handle
The table procedure defines the grid and state prefix:
<$transclude
$variable="btc.rgrid.track.table"
gridId="my-grid"
columns="4"
statePrefix="$:/state/my-grid"
content="""
<$transclude $variable="btc.rgrid.track.cell" col="1" row="1" content="A"/>
<$transclude $variable="btc.rgrid.track.cell" col="2" row="1" colSpan="2" content="B spans two columns"/>
<$transclude $variable="btc.rgrid.track.cell" col="4" row="1" last="yes" content="C"/>
"""
/>A cell procedure should calculate:
boundaryIndex = col + colSpan - 1
and render a handle unless the cell is the last visual cell in the row:
<$resizer
mode="grid-track"
class="btc-rgrid-cell-resizer"
direction="horizontal"
gridSelector=<<btcRgridSelector>>
gridTrackIndex=<<boundaryIndex>>
gridTrackStatePrefix=<<btcRgridStatePrefix>>
gridTrackMin=<<btcRgridMinColSize>>
gridTrackMax=<<btcRgridMaxColSize>>
gridTrackLive="yes"
gridTrackSave="end"
gridTrackLiveUnit="px"
gridTrackSaveUnit="px"
gridTrackFreezeOnStart="yes"
gridTrackFreezeOnEnd="yes"
/>For exact handle positioning, keep the actual CSS Grid gap at 0 and create visual spacing with an inner cell box. This keeps grid boundaries mathematically exact while still giving visual breathing room between cells.
.btc-rgrid-content {
display: grid;
grid-template-columns: var(--btc-rgrid-columns-template);
gap: 0;
padding: calc(var(--btc-rgrid-cell-gap) / 2);
}
.btc-rgrid-cell {
position: relative;
padding: calc(var(--btc-rgrid-cell-gap) / 2);
overflow: visible;
}
.btc-rgrid-cell-content {
border: 1px solid var(--btc-rgrid-border);
border-radius: 4px;
padding: 0.75em 0.95em;
}
.btc-rgrid-cell > .tc-resizer.btc-rgrid-cell-resizer {
position: absolute;
top: calc(var(--btc-rgrid-cell-gap) / 2);
right: calc(var(--btc-rgrid-handle-size) / -2);
bottom: calc(var(--btc-rgrid-cell-gap) / 2);
width: var(--btc-rgrid-handle-size);
cursor: ew-resize;
}A placed grid table reads cell tiddlers from a tag. Each tiddler defines its own coordinate and span fields:
tags: MyTable
row: 2
col: 1
rowspan: 1
colspan: 2
order: 10
The text of the tiddler is rendered as the cell content. Additional tags control visual role:
| Tag | Effect |
|---|---|
Header |
Header styling. |
Footer |
Footer styling. |
Muted |
Muted/background styling. |
The table procedure defines the coordinate system:
<$transclude
$variable="btc.rgrid.placed.table"
gridId="project-table"
tag="MyTable"
columns="4"
rows="6"
statePrefix="$:/state/project-table"
showTitles="no"
/>Placed grid tables should render ghost cells for empty coordinates. A ghost cell is an empty placeholder that keeps the declared rows × columns system visible and stable. Ghost cells should only render where no real cell covers that coordinate. If a real cell spans two columns and two rows, the ghosts underneath those covered coordinates should not be rendered.
Conceptually:
1. For each coordinate row/col, check whether any tagged real cell covers it.
2. If no real cell covers it, render a ghost cell.
3. Render real cells with explicit `grid-area` values.
4. Render resize handles on ghost and real cells unless their boundary is the last column.
Use grid-area for both ghosts and real cells:
style.grid-area={{{ [<row>addsuffix[ / ]addsuffix<col>addsuffix[ / span ]addsuffix<rowspan>addsuffix[ / span ]addsuffix<colspan>] }}}For ghost cells:
style.grid-area={{{ [<row>addsuffix[ / ]addsuffix<col>addsuffix[ / span 1 / span 1]] }}}Real cells and ghost cells can both render grid-track handles. For a real cell:
boundaryIndex = col + colspan - 1
For a ghost cell:
boundaryIndex = col
Do not render a handle when boundaryIndex >= columns, because there is no column to the right.
This lets placed grid tables resize like manually-authored grid tables, while preserving the tiddler-driven row, col, rowspan, and colspan model.
Snap points let the drag value lock to useful sizes when the pointer is close enough.
Example:
<$resizer
tiddler="$:/state/sidebar/width"
unit="rem"
min="0px"
max="42rem"
snap="0px 14rem 22rem 34rem"
snapDistance="10px"
/>Useful snap patterns:
snap="0px 16rem 24rem 32rem"
snap="25% 33.333333% 50% 66.666667% 75%"
snap="calc(100vw - 40rem) calc(100vw - 24rem)"Separators accepted by the parser:
space, comma, semicolon, pipe, newline
Because calc() may contain spaces, keep complex calc() expressions simple and test them in the browser console if a snap point does not behave as expected.
The resize flow applies min/max constraints and snap logic as part of the value calculation. If a snap point lies outside min/max, the final value may still be constrained.
<$resizer
snap="0px 20rem 50%"
snapHaptic="yes"
hapticFeedback="yes"
/>Haptic feedback depends on browser and device support. Desktop browsers commonly ignore vibration.
Preset cycling lets a double-click or double-tap step through named layout states.
<$resizer
tiddler="$:/state/sidebar/width"
selector=".sidebar"
property="flexBasis"
unit="rem"
live="yes"
presetCycle="yes"
presets="closed:0px;narrow:14rem;normal:22rem;wide:34rem"
presetTiddler="$:/state/sidebar/mode"
presetIndexTiddler="$:/state/sidebar/preset-index"
/>The parser accepts:
closed:0px;narrow:14rem;normal:22rem;wide:34rem
or:
closed:0px
narrow:14rem
normal:22rem
wide:34rem
Plain unnamed presets also work:
presets="0px;14rem;22rem;34rem"In that case, preset names are generated from the zero-based index.
Preset cycling does not break existing custom double-click behavior. The order is:
1. dblClickActions, if present
2. presetCycle="yes" with usable presets
3. built-in reset behavior
So if you want preset cycling, do not also set dblClickActions on the same handle unless you intentionally want to override the built-in preset feature.
CSS variable publishing lets the widget update custom properties while still writing tiddler state.
<$resizer
tiddler="$:/state/sidebar/width"
selector=".sidebar"
property="flexBasis"
unit="rem"
cssVariable="--btc-sidebar-width"
cssVariableTarget="root"
live="yes"
/>.sidebar {
flex: 0 0 var(--btc-sidebar-width, 22rem);
}<$resizer
selector=".sidebar"
cssVariable="--sidebar-width"
cssVariableTarget="parent"
/>This publishes the variable on the target element's parent.
<$resizer
selector=".sidebar"
cssVariable="--sidebar-width"
cssVariableTarget="selector"
cssVariableSelector=".layout-root"
/>You can also pass a selector directly as cssVariableTarget:
<$resizer
selector=".sidebar"
cssVariable="--sidebar-width"
cssVariableTarget=".layout-root"
/><$resizer
mode="split-pair"
direction="horizontal"
element="previousSibling"
leftTiddler="$:/state/left"
rightTiddler="$:/state/right"
leftCssVariable="--left-width"
rightCssVariable="--right-width"
cssVariableTarget="root"
splitPairLiveResize="yes"
/>For vertical layouts:
<$resizer
mode="split-pair"
direction="vertical"
topCssVariable="--top-height"
bottomCssVariable="--bottom-height"
/>Normal live="yes" directly updates the selected target's CSS property while dragging.
<$resizer
direction="horizontal"
selector=".tc-sidebar"
property="width"
tiddler="$:/state/sidebar/width"
live="yes"
/>For flex layouts, prefer:
property="flexBasis"For split-pair, prefer:
splitPairLiveResize="yes"
splitPairSave="end"This updates both panes and their flex-basis while dragging, but saves the final state tiddlers only once at the end of the drag.
For table-column, prefer:
tableColumnLiveResize="yes"
tableColumnSave="end"This updates the <col> while dragging, then writes the final width once.
Double-click a resizer handle to reset the value, unless dblClickActions or preset cycling is active.
<$resizer
direction="horizontal"
tiddler="$:/state/panel/width"
default="300px"
resetTo="default"
/><$resizer
direction="horizontal"
tiddler="$:/state/panel/width"
min="200px"
resetTo="min"
/><$resizer
direction="horizontal"
tiddler="$:/state/panel/width"
max="800px"
resetTo="max"
/><$resizer
direction="horizontal"
tiddler="$:/state/panel/width"
resetTo="custom"
resetValue="420px"
/>dblClickActions overrides preset cycling and built-in reset behavior.
<$resizer
direction="horizontal"
tiddler="$:/state/panel/width"
dblClickActions="""
<$action-log message="Panel double-clicked" value=<<tv-action-value>>/>
"""
/>The widget supports calc() in min, max, default, snap points, and snapDistance.
<$resizer
direction="horizontal"
tiddler="$:/state/panel/width"
default="calc(50vw - 100px)"
min="calc(200px + handleWidth)"
max="calc(100vw - 350px - handleWidth)"
snap="calc(100vw - 40rem) 50% 75%"
snapDistance="calc(handleSize + 4px)"
/>| Variable | Meaning |
|---|---|
handleSize |
Computed width or height of the handle, depending on direction. |
handleWidth |
Alias for handleSize. |
handleHeight |
Alias for handleSize. |
Examples:
<$resizer min="calc(handleSize + 20px)" />
<$resizer max="calc(100% - handleSize)" />
<$resizer default="calc(50% - handleSize / 2)" />The calc() evaluator is intentionally limited. It supports nested calc(), parentheses, +, -, *, /, supported units, and the handle variables above. It is not a complete browser-grade CSS parser.
Supported units:
px, %, em, rem, vh, vw, vmin, vmax
The widget performs drag math in pixels because pointer movement is measured in pixels. It then converts the result back to the configured or original unit.
Precision rules:
| Unit | Formatting |
|---|---|
% |
stable percentage precision; use six decimals for Wikitext-generated layout values |
px |
pixel value with decimal/subpixel precision |
em, rem |
font-relative decimal precision |
vh, vw, vmin, vmax |
viewport-relative decimal precision |
For Wikitext-generated percentages, use fixed[6] before appending %:
cellWidth={{{ [[100]divide<columns>fixed[6]addsuffix[%]] }}}
This avoids noisy floating-point values such as 33.333333333333336% while preserving enough precision for stable layouts.
min and max constrain the target value. For multiple tiddlers, the delta is clamped so all target values stay within their constraints.
min applies to both panes in the pair. The first pane cannot grow so far that the second pane becomes smaller than min.
Conceptually:
primaryMin = min
secondaryMin = min
primaryMax = pairSize - secondaryMin
If max is set, it additionally limits the primary pane.
For most split-pair layouts, prefer setting only min first. Add max only when you need a hard maximum for the first pane.
min and max constrain the column width. If snap is also set, the snapped value is still subject to the effective min/max range.
The widget renders a div with class:
tc-resizer
plus any additional class from the class attribute.
During interaction:
| Class | Applied to | Meaning |
|---|---|---|
tc-resizer-active |
resizer handle | The handle is being dragged. |
tc-resizer-disabled |
resizer handle | The handle is disabled. |
tc-resizing |
body |
A resize operation is active. |
Example styling:
.tc-resizer {
position: relative;
z-index: 1000;
background: transparent;
touch-action: none;
user-select: none;
}
.tc-resizer[data-direction="horizontal"] {
width: 8px;
cursor: ew-resize;
}
.tc-resizer[data-direction="vertical"] {
height: 8px;
cursor: ns-resize;
}
.tc-resizer:hover,
.tc-resizer-active {
background: rgba(127, 127, 127, 0.25);
}
.tc-resizer-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.tc-resizing * {
user-select: none !important;
}The widget adds:
data-handle-style="grip"when handleStyle="grip" is used. You can style it with CSS:
.tc-resizer[data-handle-style="grip"]::after {
content: "";
position: absolute;
inset: 2px;
border-radius: 999px;
background: currentColor;
opacity: 0.25;
}If you render a resizer inside a table header, keep the handle small and positioned at the right edge:
th .tc-resizer[data-direction="horizontal"] {
position: absolute;
top: 0;
right: -4px;
width: 8px;
height: 100%;
cursor: col-resize;
}
th {
position: relative;
}For mode="grid-track", the handle is a normal .tc-resizer with an additional class such as btc-rgrid-cell-resizer. Keep the visible line simple and kill any old double-line ::after grip decoration.
.btc-rgrid-cell > .tc-resizer.btc-rgrid-cell-resizer {
position: absolute;
z-index: 30;
top: calc(var(--btc-rgrid-cell-gap) / 2);
right: calc(var(--btc-rgrid-handle-size) / -2);
bottom: calc(var(--btc-rgrid-cell-gap) / 2);
width: var(--btc-rgrid-handle-size);
cursor: ew-resize;
background: transparent;
touch-action: none;
user-select: none;
}
.btc-rgrid-cell > .tc-resizer.btc-rgrid-cell-resizer::before {
content: "";
position: absolute;
top: 12%;
bottom: 12%;
left: 50%;
transform: translateX(-50%);
width: 1px;
border-radius: 999px;
background: var(--btc-rgrid-handle-line);
opacity: 0.38;
}
.btc-rgrid-cell > .tc-resizer.btc-rgrid-cell-resizer::after {
content: none !important;
display: none !important;
}The plugin may include prebuilt layout procedures. Their exact availability depends on the installed plugin tiddlers, but typical procedures include horizontal split panels, vertical split panels, three-column panels, collapsible master-detail panels, and table-column helpers.
Creates a left/right layout with a draggable divider.
<<horizontal-split-panel
leftContent:"Left content"
rightContent:"Right content"
width:"50%"
minWidth:"100px"
maxWidth:"80%"
stateTiddler:"$:/state/hsplit/width"
class:"my-panel"
leftClass:"left-panel"
rightClass:"right-panel"
splitterClass:"splitter"
>>| Parameter | Description | Default |
|---|---|---|
leftContent |
Content for the left panel. | empty |
rightContent |
Content for the right panel. | empty |
width |
Initial width of the left panel. | 50% |
minHeight |
Minimum height of the whole container. | 100% |
minWidth |
Minimum width of the left panel. | 100px |
maxWidth |
Maximum width of the left panel. | 80% |
stateTiddler |
Tiddler storing the left panel width. | $:/state/hsplit/width |
class |
Extra container classes. | empty |
leftClass |
Extra left panel classes. | empty |
rightClass |
Extra right panel classes. | empty |
splitterClass |
Extra splitter classes. | empty |
Creates a top/bottom layout with a draggable divider.
<<vertical-split-panel
topContent:"Top content"
bottomContent:"Bottom content"
height:"50%"
panelHeight:"100%"
minHeight:"100px"
maxHeight:"80%"
stateTiddler:"$:/state/vsplit/height"
class:"my-panel"
topClass:"top-panel"
bottomClass:"bottom-panel"
splitterClass:"splitter"
>>| Parameter | Description | Default |
|---|---|---|
topContent |
Content for the top panel. | empty |
bottomContent |
Content for the bottom panel. | empty |
panelHeight |
Height of the entire panel container. | 100% |
height |
Initial height of the top panel. | 50% |
minHeight |
Minimum height of the top panel. | 100px |
maxHeight |
Maximum height of the top panel. | 80% |
stateTiddler |
Tiddler storing the top panel height. | $:/state/vsplit/height |
class |
Extra container classes. | empty |
topClass |
Extra top panel classes. | empty |
bottomClass |
Extra bottom panel classes. | empty |
splitterClass |
Extra splitter classes. | empty |
Creates a three-column layout with resizable side panels and a flexible center panel.
<<three-column-panels
leftContent:"Left panel content"
centerContent:"Center panel content"
rightContent:"Right panel content"
leftWidth:"200px"
rightWidth:"200px"
minWidth:"150px"
maxWidth:"400px"
minHeight:"100%"
leftStateTiddler:"$:/state/three-col/left"
rightStateTiddler:"$:/state/three-col/right"
class:"my-three-col"
>>| Parameter | Description | Default |
|---|---|---|
leftContent |
Left panel content. | empty |
centerContent |
Center panel content. | empty |
rightContent |
Right panel content. | empty |
leftWidth |
Initial left panel width. | 200px |
rightWidth |
Initial right panel width. | 200px |
minWidth |
Minimum width for side panels. | 150px |
maxWidth |
Maximum width for side panels. | 400px |
minHeight |
Minimum height of the container. | 100% |
leftStateTiddler |
Tiddler storing left width. | $:/state/three-col/left |
rightStateTiddler |
Tiddler storing right width. | $:/state/three-col/right |
class |
Extra container classes. | empty |
Creates a resizable master/detail layout with collapse state.
<<collapsible-master-detail-panel
masterContent:"Master panel content"
detailContent:"Detail panel content"
collapsed:"no"
size:"300px"
minSize:"200px"
maxSize:"500px"
minHeight:"100%"
stateTiddler:"$:/state/cmd/size"
collapseStateTiddler:"$:/state/cmd/collapsed"
class:"my-master-detail"
>>| Parameter | Description | Default |
|---|---|---|
masterContent |
Master panel content. | empty |
detailContent |
Detail panel content. | empty |
collapsed |
Initial collapsed state. | no |
size |
Initial master panel width. | 300px |
minSize |
Minimum master panel width. | 200px |
maxSize |
Maximum master panel width. | 500px |
minHeight |
Minimum container height. | 100% |
stateTiddler |
Tiddler storing size. | $:/state/cmd/size |
collapseStateTiddler |
Tiddler storing collapsed state. | $:/state/cmd/collapsed |
class |
Extra container classes. | empty |
If you add a Wikitext procedure around mode="table-column", keep it thin. The widget already handles the table logic.
\procedure resizable-table-column(tableSelector columnIndex min:"80px" max:"500px" unit:"px")
<$resizer
mode="table-column"
direction="horizontal"
tableSelector=<<tableSelector>>
tableColumnIndex=<<columnIndex>>
unit=<<unit>>
min=<<min>>
max=<<max>>
tableColumnLiveResize="yes"
tableColumnSave="end"
/>
\endFor dynamic TiddlyFlex columns, the add/remove toolbar buttons should preserve the current layout by redistributing width equally across the affected columns.
This is not proportional scaling. It is equal-delta redistribution.
When adding a column:
newColumnWidth = 100 / newColumnCount
cellDiff = -newColumnWidth / existingColumnCount
oldColumn = oldColumn + cellDiff
newColumn = newColumnWidth
Example:
Before:
50% | 50%
Add third column:
newColumnWidth = 33.333333%
cellDiff = -33.333333 / 2 = -16.666667%
After:
33.333333% | 33.333333% | 33.333333%
With custom widths:
Before:
70% | 30%
Add third column:
newColumnWidth = 33.333333%
cellDiff = -16.666667%
After:
53.333333% | 13.333333% | 33.333333%
The fallback/default width for a missing existing column should be 100 / existingColumnCount, rounded with fixed[6].
When removing the last column:
removedColumnWidth = stored width of the last column, or fallback width
cellDiff = removedColumnWidth / remainingColumnCount
remainingColumn = remainingColumn + cellDiff
Example:
Before:
33.333333% | 33.333333% | 33.333333%
Remove last column:
removedColumnWidth = 33.333333%
cellDiff = 33.333333 / 2 = 16.666667%
After:
50% | 50%
With custom widths:
Before:
53.333333% | 13.333333% | 33.333333%
Remove last column:
removedColumnWidth = 33.333333%
cellDiff = 16.666667%
After:
70% | 30%
Summing functions should return numeric percentages without the % suffix:
\function tdff.sum.visible.columns.widths()
[subfilter<tdff.tiddlyflex-enlist-columns>]
:map[<stateTiddlerPrefix>addsuffix[col-]addsuffix<currentTiddler>get[text]removesuffix[%]else<cellWidth>removesuffix[%]]
:reduce[<currentTiddler>add<accumulator>]
\end
\function tdff.sum.visible.columns.plus.one.widths()
[subfilter<tdff.tiddlyflex-enlist-columns.plus-one>]
:map[<stateTiddlerPrefix>addsuffix[col-]addsuffix<currentTiddler>get[text]removesuffix[%]else<cellWidth>removesuffix[%]]
:reduce[<currentTiddler>add<accumulator>]
\end
\function tdff.sum.visible.columns.minus.one.widths()
[subfilter<tdff.tiddlyflex-enlist-columns.minus-one>]
:map[<stateTiddlerPrefix>addsuffix[col-]addsuffix<currentTiddler>get[text]removesuffix[%]else<cellWidth>removesuffix[%]]
:reduce[<currentTiddler>add<accumulator>]
\end
Always format written percentage values with fixed[6]addsuffix[%].
Check that element resolves to the left/top pane, not the whole container.
For split-pair, this must be true:
targetElement = left/top pane
targetElement.nextElementSibling = right/bottom pane
targetElement.parentElement = full container
If the resizer is inside the pane, use:
element="parent"If the resizer is between panes, use:
element="previousSibling"Use mode="table-column" instead. Table columns should be resized through <colgroup> and <col>, not by writing widths to every cell.
<$resizer
mode="table-column"
tableSelector=".my-table"
tableColumnIndex="1"
/>Check these points:
mode="table-column"is set.tableSelectormatches exactly one table, or the handle is inside atd/th.tableColumnIndexis zero-based.- The table is not being completely re-rendered by unrelated Wikitext while dragging.
- If the table is generated dynamically, use a stable
tableColumnIdordata-resizer-id.
Flexbox may be shrinking or rebalancing panes. Use fixed flex behavior:
.pane {
flex-grow: 0;
flex-shrink: 0;
min-width: 0;
min-height: 0;
box-sizing: border-box;
}For horizontal layouts, set width/flex-basis. For vertical layouts, set height/flex-basis.
You are probably measuring a pane relative to itself instead of the whole split container. Use the split-pair percentage variables:
<<tv-action-value-percent-of-parent>>
<<tv-action-primary-value-percent-of-parent>>
<<tv-action-secondary-value-percent-of-parent>>
These are calculated against the frozen parent size from drag start.
Check all of these:
mode="split-pair"is set.directionis correct.- For horizontal:
leftTiddlerandrightTiddlerexist. - For vertical:
topTiddlerandbottomTiddlerexist. elementresolves to the first pane.- The second pane is the next sibling or is supplied via selector.
minis not larger than half the pair size.
If you see visual jitter while dragging split-pair columns, use:
splitPairLiveResize="yes"
splitPairSave="end"For table columns, use:
tableColumnLiveResize="yes"
tableColumnSave="end"This keeps the drag visually live by mutating DOM styles, but delays state tiddler writes until pointer release. That avoids a TiddlyWiki refresh on every pointermove.
Check these points:
snapis set.snapDistanceis large enough. TrysnapDistance="20px"while testing.- Snap points use supported units.
min/maxare not forcing the value away from the snap point.- Complex
calc()snap points are valid for the widget's limited evaluator.
Check double-click priority:
dblClickActions overrides preset cycling.
Also check:
presetCycle="yes"is set.presetsis not empty.- The target tiddler exists or can be written.
- The browser is not converting double-tap into zoom behavior. The handle uses pointer/touch prevention, but mobile browser behavior can vary.
Check:
- The variable name is correct.
sidebar-widthbecomes--sidebar-widthautomatically, but explicit--sidebar-widthis clearer. cssVariableTargetpoints to the element your CSS actually reads from.- For
cssVariableTarget="selector",cssVariableSelectormust match at least one element. - CSS variables inherit downward, not upward. Publishing on the target itself will not affect parent styling.
In split-pair mode, the widget already writes both paired tiddlers. Do not also use onResize actions that write those same tiddlers, unless you are intentionally overriding the built-in pair logic.
In table-column mode, avoid separate actions that rewrite the same generated tableColumnTiddler during drag unless you need custom behavior.
Check these points:
mode="grid-track"is set on the handle.- The installed package includes
$:/plugins/BTC/resizer/modules/interactions/grid-track.js. event-handlers.jsdispatches pointerdown toexecuteGridTrackMode()whenmode="grid-track".resizer-lifecycle.jsparses thegridTrack*attributes.gridSelectormatches the grid instance, for example.btc-rgrid-instance-my-grid.- The grid element contains a
.btc-rgrid-contentelement whose computedgrid-template-columnscan be read. gridTrackIndexis not the last column; it must have a right neighbor.
Avoid using CSS Grid gap for the structural spacing if you need exact handle alignment. Use gap: 0 on the grid and create visual spacing with wrapper padding and an inner .btc-rgrid-cell-content box. The handle should be anchored to the exact grid boundary, not to a visual gap calculated by the browser.
Use the stable pixel-freeze settings:
gridTrackLiveUnit="px"
gridTrackSaveUnit="px"
gridTrackFreezeOnStart="yes"
gridTrackFreezeOnEnd="yes"These settings freeze the browser-computed grid tracks before drag and save browser-resolved track widths at the end.
Do not render all ghosts blindly underneath real cells. Generate ghosts only for coordinates not covered by any real tiddler cell. A real cell with row="2" col="2" rowspan="2" colspan="3" covers six coordinates, and no ghosts should render for those coordinates.
- Modern browsers with pointer events.
- Mouse, pen, and touch input.
- Uses
getBoundingClientRect()for accurate measurements. - Uses
visualViewportwhen available for viewport units. - Uses the Vibration API for optional haptic feedback where supported.
- CSS variable publishing requires CSS custom property support.
- Table-column mode uses standard
<colgroup>and<col>elements. - Grid-track mode uses standard CSS Grid, CSS custom properties, pointer events, and computed
grid-template-columnsmeasurements.
Contributions are welcome. Useful areas for improvement include:
- additional handle styles,
- more prebuilt layout procedures,
- stronger visual debugging helpers,
- tests for split-pair edge cases,
- tests for table-column mode with
colspan, - tests for grid-track mode with merged cells and ghost cells,
- improved placed-grid table procedures and validators,
- exposing snap action variables directly in all callbacks,
- optional ghost preview mode,
- improved documentation examples for complex TiddlyWiki layouts.
This plugin is released under the MIT License. See the LICENSE file for details.
Created for the TiddlyWiki community by BTC.