Polotno Docs
Demos

Highlight headings with a full-width banner

Toggle a full-width background under text using a custom flag and a change listener

A client recently asked: “Can we drop a stripe behind a heading that always stretches the full width of the design? Even when the text moves or resizes?” The answer is yes, and it takes only a tiny helper component and one change listener.

How it works

  • Button – When a text element is selected, the Background Fill button toggles a custom flag on that element.
const TextFullWidthBackground = observer(({ element }) => (
  <Button
    active={element.custom?.fullWidthBackground}
    minimal
    onClick={() => {
      element.set({
        custom: {
          ...element.custom,
          fullWidthBackground: !element.custom?.fullWidthBackground,
        },
      });
    }}
  >
    Background Fill
  </Button>
));
  • Listener – A debounced store.on('change') handler checks every flagged text element, creates (or removes) a rectangle beneath it, and keeps the rectangle in sync with any edits.
const checkBackground = () => {
  // first make sure we remove unrelated backgrounds
  const idsToDelete: string[] = [];
  store.find((element) => {
    const attachedTo = element.custom?.attachedTo;
    if (!attachedTo) return;
    const el = store.getElementById(attachedTo);
    const noBack = !el?.custom?.fullWidthBackground;
    if (!el || noBack) {
      idsToDelete.push(element.id);
    }
  });
  store.deleteElements(idsToDelete);

  store.find((element) => {
    if (!element.custom?.fullWidthBackground) return;

    let backgroundEl = store.find((el) => el.custom?.attachedTo === element.id);
    if (!backgroundEl) {
      backgroundEl = element.page.addElement({
        type: 'figure',
        subType: 'rect',
        fill: 'grey',
        selectable: false,
        draggable: false,
        resizable: false,
        custom: { attachedTo: element.id },
      });
    }

    const elementIndex = element.parent.children.indexOf(element);
    const backgroundIndex = backgroundEl.parent.children.indexOf(backgroundEl);
    if (elementIndex !== backgroundIndex + 1) {
      backgroundEl.page.setElementZIndex(backgroundEl.id, elementIndex);
    }

    backgroundEl.set({
      x: 0,
      y: element.y,
      width: store.width,
      height: element.height,
    });
  });
};

let timeout: any = null;
const requestChange = () => {
  if (timeout) return;
  timeout = setTimeout(() => {
    checkBackground();
    timeout = null;
  }, 10);
};

store.on('change', () => {
  requestChange();
});

Demo

  • Click “Background Fill” on a selected text element
  • See how the background rectangle reacts to text changes

Live demo