Skip to main content

Custom Modes

Custom modes let you build an entirely new layout and rendering strategy for your dataset, then ship it as a named value in content.mode.

When to Use a Custom Mode​

SituationPrefer custom mode?Why
Built-in table/grid/board/carousel/chart cannot express the layoutYesThe entire content renderer needs to change
A field needs a new visual treatmentNoUse custom renderAs instead
A reusable domain field type is neededNoUse a custom field type
The requirement is mostly configuration of a built-in modeNoStay with built-in config

1. Build the Mode Component​

A mode component receives a standard props contract:

// src/modes/TimelineMode.tsx
import React from 'react';
import type { Entity } from '@widgemo/widgemo-core';

interface TimelineModeProps {
data: Entity[];
config?: Record<string, unknown>;
actions?: unknown[];
onInteractionEvent?: (ctx: unknown) => void;
}

export function TimelineMode({ data, config = {} }: TimelineModeProps) {
const { dateField = 'date', sortOrder = 'desc' } = config as {
dateField?: string;
sortOrder?: 'asc' | 'desc';
};

const sorted = [...data].sort((a, b) => {
const d =
new Date(String(a[dateField])).getTime() -
new Date(String(b[dateField])).getTime();
return sortOrder === 'asc' ? d : -d;
});

return (
<ol
style={{
listStyle: 'none',
padding: 0,
margin: 0,
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
}}
>
{sorted.map((entity, i) => (
<li
key={i}
style={{ display: 'flex', gap: '0.75rem', alignItems: 'flex-start' }}
>
<time style={{ minWidth: 90, color: '#6b7280', fontSize: '0.8rem' }}>
{String(entity[dateField])}
</time>
<span style={{ fontWeight: 500 }}>
{String(entity.title ?? entity.name ?? '—')}
</span>
</li>
))}
</ol>
);
}

Mode Props Contract​

PropTypeDescription
dataEntity[]Data in the current render scope
configRecord<string, unknown>Mode-specific config from modeConfig.<name>
actionsActionConfig[]Forwarded item actions
gesturesGestureConfig[]Forwarded gestures
onInteractionEvent(ctx) => voidInteraction emitter for standardized contexts

2. Register at App Boot​

import { widgemoRegistry } from '@widgemo/widgemo-core';
import { TimelineMode } from './modes/TimelineMode';

widgemoRegistry.registerWidgemoMode({
name: 'timeline',
component: TimelineMode,
defaultConfig: {
dateField: 'date',
sortOrder: 'desc',
orientation: 'vertical',
showLines: true,
color: '#007bff',
},
});

3. Use in Config​

const config = {
zones: {
header: { title: 'Project Timeline' },
content: {
mode: 'timeline',
modeConfig: {
timeline: {
dateField: 'created_at',
sortOrder: 'asc',
},
},
item: { fields: […], layout: { type: 'auto' } },
},
},
};

Interactions in Custom Modes​

Built-in gesture types available through config:

  • item-click
  • item-drag-start
  • item-drop

Gesture handler flow:

  • If the matching gesture defines onTrigger, Widgemo calls onTrigger.
  • If onTrigger is omitted, Widgemo falls back to interactions.onEvent.

For gesture configuration details, see Content Config: Gestures.

const config = {
interactions: {
onEvent: (ctx) => {
console.log('fallback interaction', ctx.kind, ctx.entity?.id);
},
},
zones: {
content: {
mode: 'timeline',
gestures: [
{
type: 'item-click',
onTrigger: (ctx) => {
openEntity(ctx.entity?.id);
},
},
{ type: 'item-drag-start' },
{ type: 'item-drop' },
],
item: { fields: [{ key: 'name' }], layout: { type: 'auto' } },
},
},
};

Mode-Local Interactions​

You can implement additional mode-specific UI interactions directly in your custom mode component (for example hover effects, drag-over highlights, and scroll-driven behaviors).

Use built-in gestures for standardized interaction handling. Use mode-local event handlers for mode-specific interactions.

export function TimelineMode({ data = [], onInteractionEvent }: TimelineModeProps) {
return (
<ol>
{data.map((entity, index) => (
<li
key={String(entity.id ?? index)}
onMouseEnter={() => highlightRow(entity.id)}
onWheel={(event) => syncTimelineScroll(event.deltaY)}
onDoubleClick={() => onInteractionEvent?.({
kind: 'item-click',
zone: 'content',
entity,
data,
interactionId: 'timeline-double-click',
interactionLabel: 'Timeline Double Click',
})}
>
{String(entity.name ?? entity.title ?? index)}
</li>
))}
</ol>
);
}

For lifecycle interception guidance, see Extension API: When to Use Which.

Best Practices​

  • Start from a built-in mode and move to a custom mode only when the layout strategy truly differs.
  • Keep mode names namespaced when they are application-specific (acme.timeline).
  • Keep defaultConfig limited to mode-local defaults.
  • Forward standardized interactions through onInteractionEvent when possible.

Collision Behavior​

Collision semantics are defined in Extension API: Registration Collisions.

Use namespaced names (for example, acme.timeline) to reduce accidental collisions.

See Also​