Color is not consistent for border and background when changing opacity

I’m trying to reduce opacity of not selected steps to draw attention to the selected one.

The only problem I face — not consistent border and background colors when applying opacity. Both of them have the same color “#FF5252”, however when I change opacity they look differently, the border is brighter.

Here is the code I’m using

const $ = go.GraphObject.make

    const diagram = new go.Diagram(diagramRef.current, {
      "undoManager.isEnabled": true,
      model: new go.GraphLinksModel({
        linkKeyProperty: "key",
        nodeDataArray: [
          { key: 1, text: "Start", color: "#B2F5EA", category: "Start" },
          { key: 2, text: "Process 1", color: "#E9D8FD" },
          { key: 3, text: "Decision", color: "#FED7D7", category: "Decision" },
          { key: 4, text: "Process 2", color: "#E9D8FD" },
          { key: 5, text: "Process 3", color: "#E9D8FD" },
          { key: 6, text: "End", color: "#FEB2B2", category: "End" },
        ],
        linkDataArray: [
          { key: -1, from: 1, to: 2 },
          { key: -2, from: 2, to: 3 },
          { key: -3, from: 3, to: 4, text: "Yes" },
          { key: -4, from: 3, to: 5, text: "No" },
          { key: -5, from: 4, to: 6 },
          { key: -6, from: 5, to: 6 },
        ],
      }),
      // Enable panning (similar to Hand Tool in Figma)
      allowHorizontalScroll: true,
      allowVerticalScroll: true,
      scrollMode: go.Diagram.InfiniteScroll,
      // Enable node dragging
      allowMove: true,
      allowDrop: true,
      // Set initial scale and position
      initialContentAlignment: go.Spot.Center,
      "animationManager.isEnabled": false,
      // Add grid background
      grid: $(
        go.Panel,
        "Grid",
        { gridCellSize: new go.Size(20, 20) },
        $(go.Shape, "LineH", { stroke: "lightgray", strokeWidth: 0.5 }),
        $(go.Shape, "LineV", { stroke: "lightgray", strokeWidth: 0.5 }),
      ),
    })

    // Enable panning tool explicitly after diagram creation
    diagram.toolManager.panningTool.isEnabled = true
    diagram.toolManager.draggingTool.isEnabled = true

    // Also, let's make sure the diagram has the right settings for dragging
    diagram.allowDrag = true

    // Define node templates
    diagram.nodeTemplateMap.add(
      "",
      $(
        go.Node,
        "Auto",
        {
          width: 120,
          height: 60,
          selectionAdorned: true,
          resizable: true,
          resizeObjectName: "SHAPE",
          // Add shadow effect
          shadowVisible: true,
          shadowOffset: new go.Point(3, 3),
          shadowBlur: 5,
          movable: true, // Explicitly set movable to true
          opacity: 1, // Default opacity
        },
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
        $(
          go.Shape,
          "Rectangle",
          {
            name: "SHAPE",
            fill: "white",
            stroke: "#333333",
            strokeWidth: 1,
            portId: "",
            fromLinkable: true,
            toLinkable: true,
            cursor: "pointer",
          },
          new go.Binding("fill", "color"),
        ),
        $(
          go.TextBlock,
          {
            margin: 8,
            font: "14px sans-serif",
            editable: true,
          },
          new go.Binding("text").makeTwoWay(),
        ),
      ),
    )

    // Create a tooltip for the validation icon
    function createValidationTooltip() {
      return $(
        go.Adornment,
        "Auto",
        $(go.Shape, "RoundedRectangle", { fill: "#FFFFCC", stroke: "#999999" }),
        $(go.TextBlock, { margin: 5, text: "Validation warning: This step requires attention" }),
      )
    }

    // Start node template with validation icon
    diagram.nodeTemplateMap.add(
      "Start",
      $(
        go.Node,
        "Spot",
        {
            movable: true, // Explicitly set movable to true
          selectionAdorned: true,
          opacity: 1, // Default opacity,
          resizeObjectName: "MAIN_SHAPE",
          selectionObjectName: "MAIN_SHAPE",
          locationObjectName: "MAIN_SHAPE",
        },
        $(
          go.Panel,
          "Auto",
          {
            name: "MAIN_SHAPE",
            width: 120,
            height: 60,
          },
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
          $(
            go.Shape,
            "RoundedRectangle",
            {
              fill: "#B2F5EA",
              stroke: "#333333",
              strokeWidth: 1,
              portId: "",
              fromLinkable: true,
              toLinkable: true,
              cursor: "pointer",
            },
            new go.Binding("fill", "color"),
          ),
          $(
            go.TextBlock,
            {
              margin: 8,
              font: "14px sans-serif",
              editable: true,
            },
            new go.Binding("text").makeTwoWay(),
          ),
        ),
        // Validation icon panel positioned at the top right
        $(
          go.Panel,
          "Auto",
          {
            alignment: new go.Spot(1, 0, -5, 5), // Position at top right with 5px offset
            width: 22,
            height: 22,
            isActionable: true,
            cursor: "pointer",
            click: (e, panel) => {
              // Simple alert for demonstration
              alert("Validation issues detected in this step!")
            },
            toolTip: createValidationTooltip(),
          },
          $(go.Shape, "Triangle", {
            name: "VALIDATION_ICON",
            width: 22,
            height: 22,
            fill: "white",
            strokeWidth: 0,
            isPanelMain: true,
            spot1: go.Spot.TopLeft,
            spot2: go.Spot.BottomRight,
          }),
          $(go.Shape, "Triangle", {
            width: 20,
            height: 18,
            fill: "#FF5252",
            strokeWidth: 2,
            stroke: "#FF5252",
          }),
          $(go.Shape, {
            geometryString: "M10,6 L10,12 M10,14 L10,16", // Simple exclamation mark
            stroke: "white",
            strokeWidth: 2,
          }),
        ),
      ),
    )

    // Decision node template
    diagram.nodeTemplateMap.add(
      "Decision",
      $(
        go.Node,
        "Auto",
        {
          width: 120,
          height: 120,
          selectionAdorned: true,
          movable: true, // Explicitly set movable to true
          opacity: 1, // Default opacity
        },
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
        $(
          go.Shape,
          "Diamond",
          {
            fill: "#FED7D7",
            stroke: "#333333",
            strokeWidth: 1,
            portId: "",
            fromLinkable: true,
            toLinkable: true,
            cursor: "pointer",
          },
          new go.Binding("fill", "color"),
        ),
        $(
          go.TextBlock,
          {
            margin: 8,
            font: "14px sans-serif",
            editable: true,
          },
          new go.Binding("text").makeTwoWay(),
        ),
      ),
    )

    // End node template
    diagram.nodeTemplateMap.add(
      "End",
      $(
        go.Node,
        "Auto",
        {
          width: 120,
          height: 60,
          selectionAdorned: true,
          movable: true, // Explicitly set movable to true
          opacity: 1, // Default opacity
        },
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
        $(
          go.Shape,
          "RoundedRectangle",
          {
            fill: "#FEB2B2",
            stroke: "#333333",
            strokeWidth: 1,
            portId: "",
            fromLinkable: true,
            toLinkable: true,
            cursor: "pointer",
          },
          new go.Binding("fill", "color"),
        ),
        $(
          go.TextBlock,
          {
            margin: 8,
            font: "14px sans-serif",
            editable: true,
          },
          new go.Binding("text").makeTwoWay(),
        ),
      ),
    )

    // Define link template
    diagram.linkTemplate = $(
      go.Link,
      {
        routing: go.Link.AvoidsNodes,
        curve: go.Link.JumpOver,
        corner: 5,
        toShortLength: 4,
        relinkableFrom: true,
        relinkableTo: true,
        reshapable: true,
        resegmentable: true,
        // mouse-overs subtly highlight links:
        mouseEnter: (e, link) => (link.findObject("HIGHLIGHT").stroke = "rgba(30,144,255,0.2)"),
        mouseLeave: (e, link) => (link.findObject("HIGHLIGHT").stroke = "transparent"),
        selectionAdorned: true,
      },
      new go.Binding("points").makeTwoWay(),
      $(
        go.Shape, // highlight shape, normally transparent
        { isPanelMain: true, strokeWidth: 8, stroke: "transparent", name: "HIGHLIGHT" },
      ),
      $(
        go.Shape, // the link path shape
        { isPanelMain: true, stroke: "#333333", strokeWidth: 2 },
      ),
      $(
        go.Shape, // the arrowhead
        { toArrow: "standard", stroke: null, fill: "#333333" },
      ),
      $(
        go.Panel,
        "Auto", // the link label, normally not visible
        { visible: false, name: "LABEL", segmentIndex: 2, segmentFraction: 0.5 },
        new go.Binding("visible", "text", (t) => !!t),
        $(
          go.Shape,
          "RoundedRectangle", // the label shape
          { fill: "#F8F8F8", stroke: null },
        ),
        $(
          go.TextBlock, // the label
          {
            textAlign: "center",
            font: "10pt sans-serif",
            stroke: "#333333",
            margin: 4,
          },
          new go.Binding("text", "text"),
        ),
      ),
    )

    // Add selection changed listener to handle opacity changes
    diagram.addDiagramListener("ChangedSelection", (e) => {
      const selectedNode = diagram.selection.first()

      // Iterate through all nodes
      diagram.nodes.each((node) => {
        // If there's a selected node
        if (selectedNode) {
          // Set opacity based on whether the node is selected
          node.opacity = node === selectedNode ? 1 : 0.5
        } else {
          // If no nodes are selected, set all nodes to opacity 1
          node.opacity = 1
        }
      })
    })

    // When the diagram model changes, update app state
    diagram.addDiagramListener("Modified", (e) => {
      const model = diagram.model.toJson()
      // You could save the model to state or localStorage here
    })

I don’t understand the question. Of course if you decrease a GraphObject’s opacity, it will not look as vivid. Although what happens with the brightness depends on the colors of the object and the colors of what’s behind it.

@walter, sorry for the confusion, I wanted to draw attention particularly to the warning icon, its background and border. They have the same color “#FF5252”, however, when I change opacity of the whole node, border is brighter than background for some reason.

I expect it to be monochromatic, in the same way when opacity is not changed.

When there’s translucency, you probably don’t want to have a Shape with both Shape.fill and Shape.stroke, since that will cause double-painting in the areas that they overlap. Of course when the brushes are opaque, one can’t tell (unless they are different colors, of course). But when they are translucent, the double paint will make that common area more solid.

@walter thank you