Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Customization

This guide covers theming, styling, and advanced customization of the DataLogicEditor.

Theming

System Theme (Default)

By default, the editor follows the system theme preference:

<DataLogicEditor value={expression} />

Explicit Theme

Override with the theme prop:

// Always dark
<DataLogicEditor value={expression} theme="dark" />

// Always light
<DataLogicEditor value={expression} theme="light" />

Theme Resolution

The component sets data-theme on its own .logic-editor root element based on the theme prop (or system preference when the prop is omitted). It ignores data-theme on a parent or ancestor element, so wrapping the editor in <div data-theme="dark"> has no effect. To force a theme, use the theme prop:

<DataLogicEditor value={expression} theme="dark" />

Dynamic Theme Switching

function ThemedEditor() {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  return (
    <div>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
      <DataLogicEditor value={expression} theme={theme} />
    </div>
  );
}

CSS Customization

Container Styling

Use the className prop for container styling:

<DataLogicEditor value={expression} className="custom-editor" />
.custom-editor {
  border: 2px solid #3b82f6;
  border-radius: 12px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

CSS Variables

The component’s theme variables are scoped to its .logic-editor root element (not :root), so they do not leak into the rest of your app. To override them, target the same scope. The dark theme applies through .logic-editor[data-theme="dark"].

The primary axis is the signal palette: a node is coloured by the type of value it produces, not by its operator category. Everything else sits on a neutral substrate, and the accent colour is reserved for selection, root and focus. These are the token names with their light-theme values:

.logic-editor {
  /* Signal palette: colour = the value that flows out of a node.
     Each has a matching --sig-*-bg used for fills. */
  --sig-bool-true: #1a7f37;
  --sig-bool-false: #cf222e;
  --sig-bool-rest: #57708a;   /* boolean-valued, not yet evaluated */
  --sig-number: #0959c0;
  --sig-string: #8a5a00;
  --sig-collection: #8250df;  /* arrays and objects */
  --sig-data: #1b7c83;        /* var / val / exists: the data tap */
  --sig-temporal: #bf3989;    /* datetimes and durations */
  --sig-null: #6e7781;

  /* Substrate */
  --board: #eef1f5;           /* canvas */
  --board-grid: rgba(20, 40, 70, 0.05);
  --surface: #ffffff;         /* node bodies, panels */
  --surface-2: #f6f8fb;
  --chip: #ffffff;
  --hairline: #d6dde6;
  --hairline-2: #e6ebf1;

  /* Ink */
  --ink: #0e1826;
  --ink-2: #33475e;
  --muted: #5b6a7d;
  --faint: #9aa9ba;           /* non-text only: idle wires, dot grid */

  /* Structural accent: selection, root, focus ring */
  --accent: #4b56d6;
  --accent-soft: #e7e9fb;
  --accent-hover: #3a44c0;

  /* Type */
  --font-ui: 'Space Grotesk', ui-sans-serif, -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', 'Cascadia Code',
    'Consolas', monospace;

  /* Shape, elevation, motion */
  --radius-sm: 7px;  --radius-md: 10px; --radius-lg: 14px;
  --shadow-sm: 0 1px 2px rgba(16, 30, 54, 0.05);
  --shadow-md: 0 1px 2px rgba(16, 30, 54, 0.06), 0 2px 6px rgba(16, 30, 54, 0.06);
  --shadow-lg: 0 8px 30px rgba(16, 30, 54, 0.14), 0 2px 8px rgba(16, 30, 54, 0.08);
  --motion-fast: 120ms; --motion-base: 180ms; --motion-slow: 260ms;
}

The dark theme redefines the same tokens under .logic-editor[data-theme="dark"] (for example --board: #0a0f16, --surface: #10161f, --ink: #e6edf5).

The stylesheet keeps older token names (--bg-primary, --bg-secondary, --text-primary, --border-primary, --accent-blue, --node-bg, --syntax-*, --debug-*, and the --success-* / --error-* / --warning-* families) as aliases mapped onto the tokens above, so existing overrides keep working. Prefer the tokens above for new work.

Fonts

The default stacks name Space Grotesk and JetBrains Mono, but the package does not ship the font files. Either install them yourself:

npm install @fontsource/space-grotesk @fontsource/jetbrains-mono
import '@fontsource/space-grotesk';
import '@fontsource/jetbrains-mono';

or point the two tokens at fonts you already load:

.logic-editor {
  --font-ui: 'Inter', system-ui, sans-serif;
  --font-mono: 'Fira Code', ui-monospace, monospace;
}

Without either step the stacks fall back to the system UI and monospace fonts.

Node Styling

Target specific node types. Prefix each selector with .logic-editor: the editor scopes its own React Flow rules the same way, so your overrides reach only the editor and leave other React Flow canvases on the page alone:

/* All nodes */
.logic-editor .react-flow__node {
  font-family: 'Inter', sans-serif;
}

/* Operator nodes (and, or, if, var, val, ==, +, etc.) */
.logic-editor .react-flow__node-operator {
  border-width: 2px;
}

/* Literal nodes (strings, numbers, booleans, null) */
.logic-editor .react-flow__node-literal {
  font-weight: bold;
}

/* Structure nodes (JSON objects/arrays in templating mode) */
.logic-editor .react-flow__node-structure {
  font-style: italic;
}

There are three node types: operator, literal, and structure. Variables (var / val) render as operator nodes, so a .react-flow__node-variable selector matches nothing.

Edge Styling

Customize connection lines. The editor’s own edge rule is .logic-editor .react-flow__edge-path, so use the same selector and load your stylesheet after @goplasmatic/datalogic-ui/styles.css:

.logic-editor .react-flow__edge-path {
  stroke: #6b7280;
  stroke-width: 2px;
}

.logic-editor .react-flow__edge.selected .react-flow__edge-path {
  stroke: #3b82f6;
}

Layout Customization

Container Dimensions

The editor fills its parent, so give the parent a height:

// Fixed height
<div style={{ height: '500px' }}>
  <DataLogicEditor value={expression} />
</div>

// Viewport height
<div style={{ height: '100vh' }}>
  <DataLogicEditor value={expression} />
</div>

// Flexbox
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
  <header>...</header>
  <div style={{ flex: 1 }}>
    <DataLogicEditor value={expression} />
  </div>
</div>

Using Utilities

Custom Flow Rendering

For complete control, use the utility functions with your own React Flow instance. The package’s styles.css styles React Flow only inside .logic-editor, so a canvas of your own needs React Flow’s stylesheet:

import '@xyflow/react/dist/style.css';
import { ReactFlow, Background, Controls } from '@xyflow/react';
import { jsonLogicToNodes, applyTreeLayout } from '@goplasmatic/datalogic-ui';

function CustomEditor({ expression }) {
  const { nodes: rawNodes, edges } = jsonLogicToNodes(expression);
  const nodes = applyTreeLayout(rawNodes, edges);

  return (
    <ReactFlow
      nodes={nodes}
      edges={edges}
      fitView
      nodesDraggable={false}
      nodesConnectable={false}
    >
      <Background />
      <Controls />
    </ReactFlow>
  );
}

The nodes carry the types operator, literal and structure. Without a nodeTypes map for them, React Flow draws its default node and logs a warning; the next section shows a custom node component.

Custom Node Types

Create custom node components:

import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
import { CATEGORY_COLORS, type OperatorNodeData } from '@goplasmatic/datalogic-ui';

function CustomOperatorNode({ data }: NodeProps<Node<OperatorNodeData>>) {
  const color = CATEGORY_COLORS[data.category];

  return (
    <div
      style={{
        background: color,
        padding: '12px 20px',
        borderRadius: '8px',
        color: 'white',
      }}
    >
      <Handle type="target" position={Position.Top} />
      <div>{data.label}</div>
      <div style={{ fontSize: '0.75em', opacity: 0.8 }}>{data.operator}</div>
      <Handle type="source" position={Position.Bottom} />
    </div>
  );
}

const customNodeTypes = {
  operator: CustomOperatorNode,
  // ... other custom types
};

Node data describes the expression, not its value: there is no result field on any node shape. Evaluated values live in the trace steps (evaluateWithTrace), keyed by the engine’s node ids, and the trace’s pointers map each of those ids to a JSON Pointer into the rule. A custom renderer that shows results keeps its own map from those pointers to its nodes.

Category Colors

CATEGORY_COLORS is a palette for your own legends, pickers and custom node renderers. The shipped nodes do not use it: they are coloured by the value type they produce, through the --sig-* tokens above. Its keys are the operator categories plus literal:

import { CATEGORY_COLORS } from '@goplasmatic/datalogic-ui';

// Default colors
console.log(CATEGORY_COLORS);
// {
//   variable: '#6366f1',
//   comparison: '#14b8a6',
//   logical: '#8b5cf6',
//   arithmetic: '#22c55e',
//   string: '#06b6d4',
//   array: '#7c3aed',
//   object: '#a855f7',
//   control: '#f59e0b',
//   datetime: '#0ea5e9',
//   validation: '#94a3b8',
//   utility: '#64748b',
//   error: '#ef4444',
//   flagd: '#f97316',
//   tensor: '#db2777',
//   literal: '#64748b'
// }

// Use in custom components
function Legend() {
  return (
    <div>
      {Object.entries(CATEGORY_COLORS).map(([category, color]) => (
        <div key={category} style={{ display: 'flex', alignItems: 'center' }}>
          <span style={{ background: color, width: 16, height: 16 }} />
          <span>{category}</span>
        </div>
      ))}
    </div>
  );
}

Responsive Design

Size the container per breakpoint:

function ResponsiveEditor({ expression }) {
  return (
    <div className="editor-wrapper">
      <DataLogicEditor value={expression} />
    </div>
  );
}
.editor-wrapper {
  width: 100%;
  height: 300px;
}

@media (min-width: 768px) {
  .editor-wrapper {
    height: 500px;
  }
}

@media (min-width: 1024px) {
  .editor-wrapper {
    height: 700px;
  }
}

Performance Tips

Memoization

The editor treats each new value object as a new rule (unless it is the echo of its own onChange): the canvas remounts and refits the view. Keep the expression in state or memoize it so a parent re-render does not pass a fresh literal:

import { useMemo } from 'react';

function OptimizedEditor({ config }) {
  const expression = useMemo(() => ({
    "and": [
      { ">=": [{ "var": "age" }, config.minAge] },
      { "var": "active" }
    ]
  }), [config.minAge]);

  return <DataLogicEditor value={expression} />;
}

Debounced Data Updates

The trace re-runs when the content of data changes. For data that changes on every keystroke or animation frame, defer it:

import { useDeferredValue } from 'react';

function DebugWithDeferred({ expression, data }) {
  const deferredData = useDeferredValue(data);

  return (
    <DataLogicEditor
      value={expression}
      data={deferredData}
    />
  );
}