Skip to content

BurningTreeC/resizer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

119 Commits
 
 
 
 
 
 
 
 

Repository files navigation

TiddlyWiki Resizer Widget Plugin

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.

Contents

Features

  • 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 filter attribute.
  • 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 with mode="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, and colspan fields define placement.
  • Ghost placeholder cells: Fill unoccupied grid coordinates with empty ghost cells so a declared rows × columns system remains stable.
  • Horizontal and vertical split pairs: Use leftTiddler/rightTiddler for horizontal pairs and topTiddler/bottomTiddler for vertical pairs.
  • Stable pair size: In split-pair mode, 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%, or 50%.
  • 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, and vmax.
  • CSS calc() support: min, max, default, snap, and snapDistance can use supported calc() expressions such as calc(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, and dblClickActions.
  • 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, and grip if 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.

Installation

  1. Open the plugin demo or distribution page.
  2. Drag the plugin into your TiddlyWiki.
  3. Import the plugin tiddlers.
  4. Save and reload your wiki.
  5. 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.

Module Structure

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 ... />.

Quick Start

Resize one tiddler

<$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>

Resize one tiddler with snap points

<$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"
/>

Resize one tiddler and publish a CSS variable

<$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);
}

Resize a horizontal split pair

<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.

Resize a vertical split pair

<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>

Resize a real table column

<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"
/>

Resize a CSS Grid column boundary

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.

Core Concepts

Direction

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.

Modes

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.

Target element vs. state tiddler

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.nextElementSibling

You 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>.

Widget Attributes

Core Attributes

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().

Behavior Attributes

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

Target Attributes

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 Attributes

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 Attributes

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 Attributes

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" />

Table-Column Attributes

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

Grid-Track Attributes

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"
/>

Split-Pair Attributes

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

Event Attributes

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.

Styling Attributes

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

Reset Attributes

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

Mobile/Touch Attributes

Attribute Description Default
hapticFeedback Enable vibration feedback on supported devices. yes
hapticDebug Log haptic availability/results to console. no

Action Variables

Action variables are set before invoking event/action strings. They let you inspect or reuse the current drag state in Wikitext actions.

Generic Variables

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

Snap Variables

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>>

Preset Variables

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.

Table-Column Variables

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.

Grid-Track Variables

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.

Split-Pair Generic Variables

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.

Horizontal Split-Pair Aliases

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.

Vertical Split-Pair Aliases

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.

Double-Click Action Variables

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

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"
/>

Multiple-Target Resizing

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 Resizing

split-pair mode is for adjacent split panes. It is the recommended mode for resizable columns and stacked panes.

How split-pair works

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.

Horizontal split pair

<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>

Horizontal split pair with CSS variables

<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%);
}

Vertical split pair

<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>

Resizer inside the first pane

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.

CSS for stable split-pair layouts

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.

When not to use split-pair

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.

Table-Column Resizing

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>.

Basic table-column example

<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"
/>

Column index detection

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>

Stable table ids

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.

Recommended table CSS

.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.

Save behavior

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"
/>

Grid-Track Resizing

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.

Why freeze on start and end?

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"
/>

Handle placement

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"

CSS Grid Table Procedures

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"
/>

Recommended CSS Grid table styling

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;
}

Placed Tiddler Grid Tables

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"
/>

Ghost cells

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]] }}}

Resize handles in placed tables

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

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.

Snap and constraints

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.

Snap and haptics

<$resizer
  snap="0px 20rem 50%"
  snapHaptic="yes"
  hapticFeedback="yes"
/>

Haptic feedback depends on browser and device support. Desktop browsers commonly ignore vibration.

Preset Cycling

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.

Double-click priority

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

CSS variable publishing lets the widget update custom properties while still writing tiddler state.

Root-level variable

<$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);
}

Parent-level variable

<$resizer
  selector=".sidebar"
  cssVariable="--sidebar-width"
  cssVariableTarget="parent"
/>

This publishes the variable on the target element's parent.

Selector target

<$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"
/>

Split-pair variables

<$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"
/>

Live DOM Resizing

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 Reset

Double-click a resizer handle to reset the value, unless dblClickActions or preset cycling is active.

Reset to default

<$resizer
  direction="horizontal"
  tiddler="$:/state/panel/width"
  default="300px"
  resetTo="default"
/>

Reset to minimum

<$resizer
  direction="horizontal"
  tiddler="$:/state/panel/width"
  min="200px"
  resetTo="min"
/>

Reset to maximum

<$resizer
  direction="horizontal"
  tiddler="$:/state/panel/width"
  max="800px"
  resetTo="max"
/>

Reset to custom value

<$resizer
  direction="horizontal"
  tiddler="$:/state/panel/width"
  resetTo="custom"
  resetValue="420px"
/>

Run custom double-click actions

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>>/>
  """
/>

CSS calc() Support

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)"
/>

Special calc variables

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.

Units and Conversion

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.

Constraints

Normal modes

min and max constrain the target value. For multiple tiddlers, the delta is clamped so all target values stay within their constraints.

Split-pair mode

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.

Table-column mode

min and max constrain the column width. If snap is also set, the snapped value is still subject to the effective min/max range.

Styling

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;
}

Handle style examples

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;
}

Table-column handle styling

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;
}

CSS Grid table handle styling

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;
}

Layout Procedures

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.

horizontal-split-panel

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

vertical-split-panel

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

three-column-panels

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

collapsible-master-detail-panel

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

Suggested resizable-table-column procedure

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"
/>
\end

TiddlyFlex Column Management

For 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.

Add column logic

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].

Remove column logic

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%

Recommended sum functions

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[%].

Debugging and Troubleshooting

Only the first column resizes

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"

Every <td> is resizing independently

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"
/>

Table-column mode does nothing

Check these points:

  • mode="table-column" is set.
  • tableSelector matches exactly one table, or the handle is inside a td/th.
  • tableColumnIndex is zero-based.
  • The table is not being completely re-rendered by unrelated Wikitext while dragging.
  • If the table is generated dynamically, use a stable tableColumnId or data-resizer-id.

Other panes flicker during drag

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.

Percent values are wrong or show 100%

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.

Split-pair does nothing

Check all of these:

  • mode="split-pair" is set.
  • direction is correct.
  • For horizontal: leftTiddler and rightTiddler exist.
  • For vertical: topTiddler and bottomTiddler exist.
  • element resolves to the first pane.
  • The second pane is the next sibling or is supplied via selector.
  • min is not larger than half the pair size.

Live resize conflicts with TiddlyWiki refresh

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.

Snap points are not working

Check these points:

  • snap is set.
  • snapDistance is large enough. Try snapDistance="20px" while testing.
  • Snap points use supported units.
  • min/max are not forcing the value away from the snap point.
  • Complex calc() snap points are valid for the widget's limited evaluator.

Preset cycling does not run

Check double-click priority:

dblClickActions overrides preset cycling.

Also check:

  • presetCycle="yes" is set.
  • presets is 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.

CSS variable does not update

Check:

  • The variable name is correct. sidebar-width becomes --sidebar-width automatically, but explicit --sidebar-width is clearer.
  • cssVariableTarget points to the element your CSS actually reads from.
  • For cssVariableTarget="selector", cssVariableSelector must match at least one element.
  • CSS variables inherit downward, not upward. Publishing on the target itself will not affect parent styling.

Avoid double-writing sizes

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.

Grid-track handles render but do not resize

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.js dispatches pointerdown to executeGridTrackMode() when mode="grid-track".
  • resizer-lifecycle.js parses the gridTrack* attributes.
  • gridSelector matches the grid instance, for example .btc-rgrid-instance-my-grid.
  • The grid element contains a .btc-rgrid-content element whose computed grid-template-columns can be read.
  • gridTrackIndex is not the last column; it must have a right neighbor.

Grid-track handles are visually misplaced

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.

Grid-track resize jumps on first drag or on pointerup

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.

Ghost cells show through real merged cells

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.

Browser Compatibility

  • Modern browsers with pointer events.
  • Mouse, pen, and touch input.
  • Uses getBoundingClientRect() for accurate measurements.
  • Uses visualViewport when 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-columns measurements.

Contributing

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.

License

This plugin is released under the MIT License. See the LICENSE file for details.

Credits

Created for the TiddlyWiki community by BTC.

About

Resize your TiddlyWiki5

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors