JSFiddle - React, Tailwind, and code Playground

CSS

.tg  {
  border-collapse: collapse;
  border-spacing: 0;
}

.tg td {
  font-family: Arial, sans-serif;
  font-size: 14px;
  padding: 10px 5px;
  border-style: solid;
  border-width: 1px;
  overflow: hidden;
  word-break: normal;
}

.tg th {
  font-family: Arial, sans-serif;
  font-size: 14px;
  font-weight: normal;
  padding: 10px 5px;
  border-style: solid;
  border-width: 1px;
  overflow: hidden;
  word-break: normal;
}

JavaScript

// NĂĽtzliche Templating Funktionen
function html2fragment(html){
  const template = document.createElement("template");
  template.innerHTML = html;
  return template.content;
}

function TemplateInstance(root){
  const result = {};
  for(const element of Array.from(root.querySelectorAll('[\\@]'))){
    const name = element.getAttribute('@');
    element.removeAttribute('@');
    result[name] = element;
    element.classList.add('at-'+name);
  }
  result.fragment = root;
  return result;
}

function Template(html){
  const fragment = html2fragment(html);
  return (d=document)=>TemplateInstance(d.importNode(fragment, true));
}

// Tabelle aufbauen
const T_Table = Template(`
  <table class="tg" @="table">
    <thead>
      <th>a</th>
      <th>b</th>
      <th>c</th>
      <th>d</th>
    </thead>
  </table>
`);

const T_Row = Template(`
  <tr @="tr">
    <td class="tg-031e" @="a"></td>
    <td class="tg-031e" @="b"></td>
    <td class="tg-031e" @="c"></td>
  </tr>
`);

const T_GroupCells = Template(`<td @="id" class="tg-031e" rowspan="999999"></td>`);


function dataToTable(data){
  const e_table = T_Table();
  for(const [id,group] of Object.entries(data)){
    const tbody = document.createElement("tbody");
    let firstRow = null;
    for(const row of group){
      const e_row = T_Row();
      firstRow ??= e_row;
      e_row.a.textContent = row.a;
      e_row.b.textContent = row.b;
      e_row.c.textContent = row.c;
      tbody.appendChild(e_row.fragment);
    }
    const e_cells = T_GroupCells();
    e_cells.id.textContent = id;
    // Note, we could get all td in e_cells.fragment and set rowspan to the correct number, it's easier to just put the rows in a tbody and set rowspan to a high number to fill all the rows in the tbody
    firstRow.tr.prepend(e_cells.fragment);
    e_table.table.appendChild(tbody);
  }
  return e_table.fragment;
}

const text =...