CxJS

Svg

import { Svg } from 'cx/svg'; Copied

CxJS has excellent support for Scalable Vector Graphics (SVG) and enables responsive layouts and charts using the concept of bounded objects.

<Svg style="width: 300px; height: 200px; border: 1px solid #ddd">
  <Rectangle fill="#f0f0f0" />
  <rect
    x={20}
    y={20}
    width={80}
    height={60}
    fill="lightblue"
    stroke="steelblue"
  />
  <ellipse cx={180} cy={50} rx={40} ry={30} fill="lightgreen" stroke="green" />
  <line x1={20} y1={120} x2={280} y2={120} stroke="#999" />
  <line x1={20} y1={140} x2={280} y2={180} stroke="coral" stroke-width={2} />
  <text x={150} y={170} text-anchor="middle" style="font-size: 14px">
    SVG Elements
  </text>
</Svg>

The Svg component serves as a container for SVG elements. You can mix two approaches:

  • Native SVG elements (rect, ellipse, line, text) — positioned with standard attributes like x, y, width, height, but you must calculate positions manually
  • CxJS components (Rectangle, Ellipse, Line, Text) — work as bounded objects that automatically adapt to their container size using anchors, offset, and margin properties

Bounded Objects

Use the Svg container instead of the native svg element to enable bounded objects.

The Svg component measures its size and passes bounding box information to child elements. Child elements use parent bounds to calculate their own size and pass it to their children.

Bounds are defined using the anchors, offset, and margin properties. Each property consists of four components t r b l (top, right, bottom, left) in clockwise order. Do not use suffixes like % or px.

Anchors

Anchors define how child bounds are tied to the parent:

  • 0 aligns to the left/top edge
  • 1 aligns to the right/bottom edge
  • Values between 0 and 1 position proportionally
<div class="flex flex-wrap gap-2">
  <Svg style="width: 100px; height: 100px; background: white; border: 1px dotted #ccc">
    <Rectangle anchors="0 1 1 0" style="fill: lightblue" />
    <Text textAnchor="middle" dy="0.4em" style="font-size: 10px">
      0 1 1 0
    </Text>
  </Svg>

  <Svg style="width: 100px; height: 100px; background: white; border: 1px dotted #ccc">
    <Rectangle anchors="0.25 0.75 0.75 0.25" style="fill: lightblue" />
    <Text textAnchor="middle" dy="0.4em" style="font-size: 10px">
      0.25 0.75 0.75 0.25
    </Text>
  </Svg>

  <Svg style="width: 100px; height: 100px; background: white; border: 1px dotted #ccc">
    <Rectangle anchors="0 0.5 1 0" style="fill: lightblue" />
    <Text textAnchor="middle" dy="0.4em" style="font-size: 10px">
      0 0.5 1 0
    </Text>
  </Svg>

  <Svg style="width: 100px; height: 100px; background: white; border: 1px dotted #ccc">
    <Rectangle anchors="0 1 0.5 0" style="fill: lightblue" />
    <Text textAnchor="middle" dy="0.4em" style="font-size: 10px">
      0 1 0.5 0
    </Text>
  </Svg>

  <Svg style="width: 100px; height: 100px; background: white; border: 1px dotted #ccc">
    <Rectangle anchors="0.5 1 1 0.5" style="fill: lightblue" />
    <Text textAnchor="middle" dy="0.4em" style="font-size: 10px">
      0.5 1 1 0.5
    </Text>
  </Svg>
</div>

Offset and Margin

The offset property translates the edges of the bounding box. It always works in the top-to-bottom and left-to-right direction, so use negative values for right and bottom edges.

The margin property works like CSS margin — positive values shrink the box inward, negative values expand it outward.

<div class="flex flex-wrap gap-2">
  <Svg style="width: 100px; height: 100px; background: white; border: 1px dotted #ccc">
    <Rectangle anchors="0 1 1 0" offset="5 -5 -5 5" style="fill: lightblue" />
    <Text textAnchor="middle" dy="-0.1em" style="font-size: 10px">
      A: 0 1 1 0
    </Text>
    <Text textAnchor="middle" dy="0.9em" style="font-size: 10px">
      O: 5 -5 -5 5
    </Text>
  </Svg>

  <Svg style="width: 100px; height: 100px; background: white; border: 1px dotted #ccc">
    <Rectangle
      anchors="0.5 0.5 0.5 0.5"
      offset="-30 30 30 -30"
      style="fill: lightblue"
    />
    <Text textAnchor="middle" dy="-0.1em" style="font-size: 10px">
      A: 0.5 0.5 0.5 0.5
    </Text>
    <Text textAnchor="middle" dy="0.9em" style="font-size: 10px">
      O: -30 30 30 -30
    </Text>
  </Svg>

  <Svg style="width: 100px; height: 100px; background: white; border: 1px dotted #ccc">
    <Rectangle anchors="0 1 1 0" margin={5} style="fill: lightblue" />
    <Text textAnchor="middle" dy="-0.1em" style="font-size: 10px">
      A: 0 1 1 0
    </Text>
    <Text textAnchor="middle" dy="0.9em" style="font-size: 10px">
      M: 5
    </Text>
  </Svg>

  <Svg style="width: 100px; height: 100px; background: white; border: 1px dotted #ccc">
    <Rectangle anchors="0.5 0.5 0.5 0.5" margin={-30} style="fill: lightblue" />
    <Text textAnchor="middle" dy="-0.1em" style="font-size: 10px">
      A: 0.5 0.5 0.5 0.5
    </Text>
    <Text textAnchor="middle" dy="0.9em" style="font-size: 10px">
      M: -30
    </Text>
  </Svg>
</div>

Key difference:

  • offset="5 -5 -5 5" — explicit directional offsets
  • margin={5} — uniform inset (equivalent result)

Interactive Example

Try resizing the container and adjusting the values to see how bounded objects respond. Use the mouse wheel while focused on a field to quickly adjust values:

<div controller={PageController}>
  <LabelsTopLayout>
    <LookupField
      value={bind(m.preset, "full")}
      label="Preset"
      options={presets}
      style="width: 220px"
      required
    />
  </LabelsTopLayout>
  <strong class="block -mb-2">Anchors</strong>
  <LabelsTopLayout>
    <NumberField
      value={bind(m.anchorT, 0)}
      label="Top"
      style="width: 60px"
      minValue={0}
      maxValue={1}
      step={0.1}
      format="n;1"
      reactOn="enter blur wheel"
      trackFocus
      focused={m.focusAnchorT}
    />
    <NumberField
      value={bind(m.anchorR, 1)}
      label="Right"
      style="width: 60px"
      minValue={0}
      maxValue={1}
      step={0.1}
      format="n;1"
      reactOn="enter blur wheel"
      trackFocus
      focused={m.focusAnchorR}
    />
    <NumberField
      value={bind(m.anchorB, 1)}
      label="Bottom"
      style="width: 60px"
      minValue={0}
      maxValue={1}
      step={0.1}
      format="n;1"
      reactOn="enter blur wheel"
      trackFocus
      focused={m.focusAnchorB}
    />
    <NumberField
      value={bind(m.anchorL, 0)}
      label="Left"
      style="width: 60px"
      minValue={0}
      maxValue={1}
      step={0.1}
      format="n;1"
      reactOn="enter blur wheel"
      trackFocus
      focused={m.focusAnchorL}
    />
  </LabelsTopLayout>

  <strong class="block mt-4 -mb-2">Offset</strong>
  <LabelsTopLayout>
    <NumberField
      value={bind(m.offsetT, 10)}
      label="Top"
      style="width: 60px"
      step={5}
      reactOn="enter blur wheel"
      trackFocus
      focused={m.focusOffsetT}
    />
    <NumberField
      value={bind(m.offsetR, -10)}
      label="Right"
      style="width: 60px"
      step={5}
      reactOn="enter blur wheel"
      trackFocus
      focused={m.focusOffsetR}
    />
    <NumberField
      value={bind(m.offsetB, -10)}
      label="Bottom"
      style="width: 60px"
      step={5}
      reactOn="enter blur wheel"
      trackFocus
      focused={m.focusOffsetB}
    />
    <NumberField
      value={bind(m.offsetL, 10)}
      label="Left"
      style="width: 60px"
      step={5}
      reactOn="enter blur wheel"
      trackFocus
      focused={m.focusOffsetL}
    />
  </LabelsTopLayout>

  <div class="flex mt-4">
    <div>
      <Svg
        style={{
          width: bind(m.width, 300),
          height: bind(m.height, 200),
        }}
        padding={1}
      >
        {/* Anchor lines - show where percentage anchors are positioned */}
        <Line
          anchors={tpl(m.anchorT, "{0} 1 {0} 0")}
          style={expr(
            m.focusAnchorT,
            (f) => `stroke: ${f ? "red" : "#ccc"}; stroke-width: ${f ? 2 : 1}`,
          )}
        />
        <Line
          anchors={tpl(m.anchorB, "{0} 1 {0} 0")}
          style={expr(
            m.focusAnchorB,
            (f) => `stroke: ${f ? "red" : "#ccc"}; stroke-width: ${f ? 2 : 1}`,
          )}
        />
        <Line
          anchors={tpl(m.anchorL, "0 {0} 1 {0}")}
          style={expr(
            m.focusAnchorL,
            (f) => `stroke: ${f ? "red" : "#ccc"}; stroke-width: ${f ? 2 : 1}`,
          )}
        />
        <Line
          anchors={tpl(m.anchorR, "0 {0} 1 {0}")}
          style={expr(
            m.focusAnchorR,
            (f) => `stroke: ${f ? "red" : "#ccc"}; stroke-width: ${f ? 2 : 1}`,
          )}
        />
        {/* Offset lines - show pixel displacement from anchor to rectangle edge */}
        <Line
          anchors={expr(
            m.anchorT,
            m.anchorL,
            m.anchorR,
            (t, l, r) => `${t} ${(l + r) / 2} ${t} ${(l + r) / 2}`,
          )}
          offset={tpl(m.offsetT, "0 0 {0} 0")}
          style={expr(
            m.focusOffsetT,
            (f) => `stroke: ${f ? "red" : "#ccc"}; stroke-width: ${f ? 2 : 1}`,
          )}
        />
        <Line
          anchors={expr(
            m.anchorB,
            m.anchorL,
            m.anchorR,
            (b, l, r) => `${b} ${(l + r) / 2} ${b} ${(l + r) / 2}`,
          )}
          offset={tpl(m.offsetB, "0 0 {0} 0")}
          style={expr(
            m.focusOffsetB,
            (f) => `stroke: ${f ? "red" : "#ccc"}; stroke-width: ${f ? 2 : 1}`,
          )}
        />
        <Line
          anchors={expr(
            m.anchorL,
            m.anchorT,
            m.anchorB,
            (l, t, b) => `${(t + b) / 2} ${l} ${(t + b) / 2} ${l}`,
          )}
          offset={tpl(m.offsetL, "0 {0} 0 0")}
          style={expr(
            m.focusOffsetL,
            (f) => `stroke: ${f ? "red" : "#ccc"}; stroke-width: ${f ? 2 : 1}`,
          )}
        />
        <Line
          anchors={expr(
            m.anchorR,
            m.anchorT,
            m.anchorB,
            (r, t, b) => `${(t + b) / 2} ${r} ${(t + b) / 2} ${r}`,
          )}
          offset={tpl(m.offsetR, "0 {0} 0 0")}
          style={expr(
            m.focusOffsetR,
            (f) => `stroke: ${f ? "red" : "#ccc"}; stroke-width: ${f ? 2 : 1}`,
          )}
        />
        {/* Rectangle rendered last to appear on top */}
        <Rectangle
          anchors={tpl(
            m.anchorT,
            m.anchorR,
            m.anchorB,
            m.anchorL,
            "{0} {1} {2} {3}",
          )}
          offset={tpl(
            m.offsetT,
            m.offsetR,
            m.offsetB,
            m.offsetL,
            "{0} {1} {2} {3}",
          )}
          style="fill: lightblue; stroke: steelblue"
        />
      </Svg>
      <Resizer size={bind(m.height, 200)} horizontal />
    </div>
    <Resizer size={bind(m.width, 300)} />
  </div>
</div>
Full
Anchors
Offset

Aspect Ratio

When you can’t give fixed dimensions to an SVG element, use aspectRatio with autoHeight or autoWidth to automatically size the element based on available space.

<Svg style="width: 100%" aspectRatio={4} autoHeight>
  <Rectangle anchors="0 1 1 0" style="fill: lightblue" />
</Svg>

In this example, the height is automatically calculated to be 4 times smaller than the width.

Configuration

PropertyTypeDescription
anchorsstring/numberDefines how bounds are tied to parent. Format: "t r b l".
offsetstring/numberTranslates edges of the bounding box. Format: "t r b l".
marginstring/numberApplies margin to boundaries (CSS-like behavior).
paddingstring/numberPadding applied before passing bounds to children.
aspectRationumberAspect ratio (width/height). Default: 1.618.
autoWidthbooleanCalculate width from height and aspect ratio.
autoHeightbooleanCalculate height from width and aspect ratio.