Created
January 28, 2026 18:31
-
-
Save hardiksondagar/9d232a9746ed09d5946c652b314f7593 to your computer and use it in GitHub Desktop.
Auto-Responsive Multi-Row Table
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Auto-Responsive Multi-Row Table</title> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: #0f0f0f; | |
| color: #e0e0e0; | |
| padding: 30px 20px; | |
| line-height: 1.4; | |
| } | |
| h1 { | |
| text-align: center; | |
| margin-bottom: 20px; | |
| color: #fff; | |
| font-size: 1.3rem; | |
| } | |
| /* Controls */ | |
| .controls { | |
| max-width: 900px; | |
| margin: 0 auto 20px; | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 16px; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 16px; | |
| background: #1a1a1a; | |
| border-radius: 10px; | |
| border: 1px solid #333; | |
| } | |
| .control-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .control-group label { | |
| font-size: 0.75rem; | |
| color: #888; | |
| text-transform: uppercase; | |
| letter-spacing: 0.3px; | |
| } | |
| .control-group input[type="number"] { | |
| width: 60px; | |
| padding: 6px 8px; | |
| border: 1px solid #444; | |
| border-radius: 4px; | |
| background: #252525; | |
| color: #fff; | |
| font-size: 0.85rem; | |
| text-align: center; | |
| } | |
| .control-group input[type="range"] { | |
| width: 160px; | |
| accent-color: #60a5fa; | |
| } | |
| .btn { | |
| padding: 8px 14px; | |
| border: none; | |
| border-radius: 5px; | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .btn-primary { | |
| background: #60a5fa; | |
| color: #000; | |
| } | |
| .btn-primary:hover { | |
| background: #93c5fd; | |
| } | |
| .btn-secondary { | |
| background: #333; | |
| color: #ccc; | |
| } | |
| .btn-secondary:hover { | |
| background: #444; | |
| } | |
| /* Resizable container */ | |
| .resize-wrapper { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| padding: 4px; | |
| background: linear-gradient(135deg, #333, #222); | |
| border-radius: 12px; | |
| } | |
| .container { | |
| background: #1a1a1a; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| resize: horizontal; | |
| min-width: 240px; | |
| max-width: 100%; | |
| width: 900px; | |
| border: 1px solid #333; | |
| } | |
| .resize-hint { | |
| text-align: center; | |
| font-size: 0.65rem; | |
| color: #555; | |
| padding: 6px; | |
| background: #1a1a1a; | |
| border-radius: 0 0 10px 10px; | |
| } | |
| /* Header rows */ | |
| .table-header { | |
| display: flex; | |
| flex-direction: column; | |
| background: #252525; | |
| border-bottom: 2px solid #444; | |
| position: sticky; | |
| top: 0; | |
| z-index: 10; | |
| } | |
| .header-row { | |
| display: grid; | |
| padding: 8px 10px; | |
| gap: 6px; | |
| } | |
| .header-row:not(:last-child) { | |
| border-bottom: 1px solid #333; | |
| } | |
| .header-row span { | |
| font-size: 0.58rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.3px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| /* Color gradient for row depth */ | |
| .header-row:nth-child(1) span { color: #60a5fa; } | |
| .header-row:nth-child(2) span { color: #818cf8; } | |
| .header-row:nth-child(3) span { color: #a78bfa; } | |
| .header-row:nth-child(4) span { color: #c084fc; } | |
| .header-row:nth-child(5) span { color: #e879f9; } | |
| .header-row:nth-child(6) span { color: #f472b6; } | |
| .header-row:nth-child(7) span { color: #fb7185; } | |
| .header-row .row-num-label { | |
| color: #555 !important; | |
| font-size: 0.5rem; | |
| } | |
| /* Data container */ | |
| .data-container { | |
| max-height: 500px; | |
| overflow-y: auto; | |
| } | |
| /* Data records */ | |
| .record { | |
| display: flex; | |
| flex-direction: column; | |
| border-bottom: 1px solid #2a2a2a; | |
| } | |
| .record:nth-child(odd) { | |
| background: #1e1e1e; | |
| } | |
| .record:nth-child(even) { | |
| background: #242424; | |
| } | |
| .record:last-child { | |
| border-bottom: none; | |
| } | |
| .record-row { | |
| display: grid; | |
| padding: 6px 10px; | |
| gap: 6px; | |
| align-items: center; | |
| } | |
| /* Row depth styling - progressively lighter/smaller */ | |
| .record-row:nth-child(1) .cell { font-size: 0.76rem; color: #fff; font-weight: 500; } | |
| .record-row:nth-child(2) .cell { font-size: 0.7rem; color: #aaa; font-weight: 400; } | |
| .record-row:nth-child(3) .cell { font-size: 0.66rem; color: #888; font-weight: 400; } | |
| .record-row:nth-child(4) .cell { font-size: 0.62rem; color: #777; font-weight: 400; } | |
| .record-row:nth-child(5) .cell { font-size: 0.58rem; color: #666; font-weight: 400; } | |
| .record-row:nth-child(6) .cell { font-size: 0.56rem; color: #606060; font-weight: 400; } | |
| .record-row:nth-child(7) .cell { font-size: 0.54rem; color: #585858; font-weight: 400; } | |
| .record-row:first-child { | |
| padding-top: 8px; | |
| } | |
| .record-row:last-child { | |
| padding-bottom: 8px; | |
| } | |
| /* Row number */ | |
| .row-num { | |
| font-family: 'SF Mono', Monaco, monospace; | |
| font-size: 0.7rem; | |
| color: #555; | |
| text-align: center; | |
| } | |
| .record-row:not(:first-child) .row-num { | |
| visibility: hidden; | |
| } | |
| /* Cell text handling */ | |
| .cell { | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| /* Hidden measuring element */ | |
| #measureCanvas { | |
| position: absolute; | |
| visibility: hidden; | |
| height: 0; | |
| overflow: hidden; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Sales Pipeline Report</h1> | |
| <!-- Controls --> | |
| <div class="controls"> | |
| <div class="control-group"> | |
| <label>Rows</label> | |
| <button class="btn btn-secondary" onclick="changeRows(-1)">−</button> | |
| <input type="number" id="rowCount" value="10" min="1" max="20" onchange="render()"> | |
| <button class="btn btn-primary" onclick="changeRows(1)">+</button> | |
| </div> | |
| <div class="control-group"> | |
| <label>Columns</label> | |
| <button class="btn btn-secondary" onclick="changeCols(-1)">−</button> | |
| <input type="number" id="colCount" value="4" min="2" max="15" onchange="render()"> | |
| <button class="btn btn-primary" onclick="changeCols(1)">+</button> | |
| </div> | |
| <div class="control-group"> | |
| <label>Cell Spacing</label> | |
| <input type="range" id="spacingSlider" min="1" max="2" step="0.1" value="1.3" oninput="updateSpacing()"> | |
| <span id="spacingValue" style="font-size:0.7rem;color:#888;width:40px;">1.3x</span> | |
| </div> | |
| </div> | |
| <!-- Resizable Table Container --> | |
| <div class="resize-wrapper"> | |
| <div class="container" id="tableContainer"> | |
| <div class="table-header" id="tableHeader"></div> | |
| <div class="data-container" id="dataContainer"></div> | |
| </div> | |
| <div class="resize-hint">↔ Drag right edge to resize — columns per row adjusts automatically based on content width</div> | |
| </div> | |
| <!-- Hidden element to measure text --> | |
| <div id="measureCanvas"></div> | |
| <script> | |
| // Large sample dataset: 20 rows × 15 columns | |
| const columns = [ | |
| 'Opp ID', 'Account', 'Contact', 'Amount', 'Stage', 'Close Date', | |
| 'Owner', 'Region', 'Product', 'Source', 'Probability', 'Created', | |
| 'Industry', 'Employees', 'Notes' | |
| ]; | |
| const data = [ | |
| ['OPP-1042', 'Acme Corp', 'John Miller', '$45,000', 'Negotiation', '2024-03-15', 'Sarah Chen', 'N. America', 'Enterprise', 'Webinar', '75%', '2024-01-10', 'Technology', '500-1000', 'Hot lead'], | |
| ['OPP-1043', 'TechStart Inc', 'Emily Wong', '$28,500', 'Proposal', '2024-03-22', 'Mike Torres', 'EMEA', 'Pro Plan', 'Referral', '60%', '2024-01-15', 'Software', '50-100', 'Fast mover'], | |
| ['OPP-1044', 'Global Dynamics', 'Robert Park', '$125,000', 'Discovery', '2024-04-10', 'Lisa Wang', 'APAC', 'Enterprise', 'Outbound', '40%', '2024-02-01', 'Manufacturing', '1000+', 'Enterprise deal'], | |
| ['OPP-1045', 'Innovate Labs', 'Anna Schmidt', '$67,000', 'Closed Won', '2024-02-28', 'Sarah Chen', 'EMEA', 'Pro Plan', 'Partner', '100%', '2023-12-05', 'Biotech', '100-500', 'Renewal Q2'], | |
| ['OPP-1046', 'CloudScale', 'David Johnson', '$89,000', 'Qualification', '2024-04-05', 'Mike Torres', 'N. America', 'Enterprise', 'Inbound', '30%', '2024-02-10', 'Cloud Services', '500-1000', 'Competitor switch'], | |
| ['OPP-1047', 'DataStream', 'Maria Garcia', '$156,000', 'Negotiation', '2024-03-30', 'Lisa Wang', 'LATAM', 'Enterprise', 'Conference', '70%', '2024-01-20', 'Analytics', '100-500', 'Multi-year'], | |
| ['OPP-1048', 'NetSecure', 'James Wilson', '$34,000', 'Proposal', '2024-04-15', 'Sarah Chen', 'N. America', 'Pro Plan', 'Webinar', '55%', '2024-02-05', 'Security', '50-100', 'Security focus'], | |
| ['OPP-1049', 'FinanceHub', 'Linda Chen', '$210,000', 'Discovery', '2024-05-01', 'Mike Torres', 'APAC', 'Enterprise', 'Referral', '35%', '2024-02-15', 'Finance', '1000+', 'Compliance req'], | |
| ['OPP-1050', 'MediaWorks', 'Tom Brown', '$52,000', 'Closed Won', '2024-02-15', 'Lisa Wang', 'EMEA', 'Pro Plan', 'Inbound', '100%', '2023-11-20', 'Media', '100-500', 'Upsell opportunity'], | |
| ['OPP-1051', 'HealthFirst', 'Susan Lee', '$145,000', 'Negotiation', '2024-04-20', 'Sarah Chen', 'N. America', 'Enterprise', 'Partner', '65%', '2024-01-25', 'Healthcare', '500-1000', 'HIPAA compliant'], | |
| ['OPP-1052', 'EduLearn', 'Michael Adams', '$23,000', 'Proposal', '2024-03-28', 'Mike Torres', 'EMEA', 'Starter', 'Outbound', '50%', '2024-02-08', 'Education', '50-100', 'Pilot program'], | |
| ['OPP-1053', 'RetailMax', 'Jennifer White', '$78,000', 'Qualification', '2024-04-25', 'Lisa Wang', 'N. America', 'Pro Plan', 'Conference', '25%', '2024-02-20', 'Retail', '500-1000', 'Seasonal push'], | |
| ['OPP-1054', 'LogiTech', 'Kevin Zhang', '$92,000', 'Discovery', '2024-05-10', 'Sarah Chen', 'APAC', 'Enterprise', 'Webinar', '30%', '2024-02-12', 'Logistics', '100-500', 'Integration needed'], | |
| ['OPP-1055', 'GreenEnergy', 'Patricia Moore', '$118,000', 'Proposal', '2024-04-08', 'Mike Torres', 'EMEA', 'Enterprise', 'Inbound', '60%', '2024-01-30', 'Energy', '1000+', 'Sustainability'], | |
| ['OPP-1056', 'AutoDrive', 'Richard Taylor', '$205,000', 'Negotiation', '2024-04-30', 'Lisa Wang', 'N. America', 'Enterprise', 'Partner', '70%', '2024-02-18', 'Automotive', '1000+', 'Multi-location'], | |
| ['OPP-1057', 'FoodChain', 'Nancy Davis', '$41,000', 'Closed Won', '2024-03-01', 'Sarah Chen', 'LATAM', 'Pro Plan', 'Referral', '100%', '2023-12-15', 'Food & Bev', '100-500', 'Expansion'], | |
| ['OPP-1058', 'TravelEase', 'Daniel Martinez', '$63,000', 'Qualification', '2024-05-15', 'Mike Torres', 'EMEA', 'Pro Plan', 'Outbound', '20%', '2024-02-22', 'Travel', '50-100', 'New vertical'], | |
| ['OPP-1059', 'BuildCorp', 'Laura Anderson', '$175,000', 'Discovery', '2024-05-20', 'Lisa Wang', 'N. America', 'Enterprise', 'Conference', '35%', '2024-02-25', 'Construction', '500-1000', 'Project-based'], | |
| ['OPP-1060', 'TeleComm', 'Chris Robinson', '$88,000', 'Proposal', '2024-04-12', 'Sarah Chen', 'APAC', 'Enterprise', 'Inbound', '55%', '2024-02-01', 'Telecom', '1000+', 'Integration'], | |
| ['OPP-1061', 'InsureSafe', 'Amanda Clark', '$134,000', 'Negotiation', '2024-04-18', 'Mike Torres', 'N. America', 'Enterprise', 'Webinar', '65%', '2024-01-28', 'Insurance', '500-1000', 'Compliance'] | |
| ]; | |
| // Cache for measured column widths | |
| let columnWidths = []; | |
| let spacingMultiplier = 1.3; | |
| function updateSpacing() { | |
| spacingMultiplier = parseFloat(document.getElementById('spacingSlider').value); | |
| document.getElementById('spacingValue').textContent = spacingMultiplier.toFixed(1) + 'x'; | |
| calculateColumnWidths(); | |
| render(); | |
| } | |
| function measureText(text, fontSize = '12px', fontFamily = '-apple-system, BlinkMacSystemFont, sans-serif') { | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.font = `${fontSize} ${fontFamily}`; | |
| return ctx.measureText(text).width; | |
| } | |
| function calculateColumnWidths() { | |
| const colCount = getColCount(); | |
| const rowCount = getRowCount(); | |
| columnWidths = []; | |
| for (let c = 0; c < colCount; c++) { | |
| // Measure header | |
| let maxWidth = measureText(columns[c], '10px') + 24; // header font + padding | |
| // Measure data cells (sample first few rows) | |
| const sampleRows = Math.min(rowCount, 10); | |
| for (let r = 0; r < sampleRows; r++) { | |
| const cellWidth = measureText(data[r][c], '12px') + 24; | |
| maxWidth = Math.max(maxWidth, cellWidth); | |
| } | |
| // Add minimum and cap maximum - spacing multiplier controls breathing room | |
| columnWidths.push(Math.max(70 * spacingMultiplier, Math.min(maxWidth * spacingMultiplier, 180))); | |
| } | |
| return columnWidths; | |
| } | |
| function calculateColsPerRow(containerWidth) { | |
| const colCount = getColCount(); | |
| const availableWidth = containerWidth - 60; // subtract row number column + padding | |
| // Calculate how many columns fit | |
| let totalWidth = 0; | |
| let colsPerRow = 0; | |
| for (let i = 0; i < colCount; i++) { | |
| const colWidth = columnWidths[i] || 80; | |
| if (totalWidth + colWidth <= availableWidth) { | |
| totalWidth += colWidth; | |
| colsPerRow++; | |
| } else { | |
| break; | |
| } | |
| } | |
| // Ensure at least 2 columns per row, max = all columns | |
| return Math.max(2, Math.min(colsPerRow, colCount)); | |
| } | |
| function getRowCount() { | |
| return Math.min(20, Math.max(1, parseInt(document.getElementById('rowCount').value) || 6)); | |
| } | |
| function getColCount() { | |
| return Math.min(15, Math.max(2, parseInt(document.getElementById('colCount').value) || 12)); | |
| } | |
| function changeRows(delta) { | |
| const input = document.getElementById('rowCount'); | |
| input.value = Math.min(20, Math.max(1, parseInt(input.value) + delta)); | |
| render(); | |
| } | |
| function changeCols(delta) { | |
| const input = document.getElementById('colCount'); | |
| input.value = Math.min(15, Math.max(2, parseInt(input.value) + delta)); | |
| calculateColumnWidths(); | |
| render(); | |
| } | |
| function chunkArray(arr, size) { | |
| const chunks = []; | |
| for (let i = 0; i < arr.length; i += size) { | |
| chunks.push(arr.slice(i, i + size)); | |
| } | |
| return chunks; | |
| } | |
| function render() { | |
| const container = document.getElementById('tableContainer'); | |
| const containerWidth = container.offsetWidth; | |
| const rowCount = getRowCount(); | |
| const colCount = getColCount(); | |
| // Auto-calculate columns per row based on width and data | |
| const colsPerRow = calculateColsPerRow(containerWidth); | |
| // Get active columns | |
| const activeColumns = columns.slice(0, colCount); | |
| // Split columns into chunks based on calculated colsPerRow | |
| const columnChunks = chunkArray(activeColumns, colsPerRow); | |
| const numSubRows = columnChunks.length; | |
| // Grid template: row number + columns per row | |
| const gridTemplate = `40px repeat(${colsPerRow}, 1fr)`; | |
| // Build header - one row per chunk | |
| let headerHTML = ''; | |
| columnChunks.forEach((chunk, i) => { | |
| headerHTML += ` | |
| <div class="header-row" style="grid-template-columns: ${gridTemplate}"> | |
| <span class="row-num-label">${i === 0 ? '#' : ''}</span> | |
| ${chunk.map(col => `<span>${col}</span>`).join('')} | |
| ${Array(colsPerRow - chunk.length).fill('<span></span>').join('')} | |
| </div> | |
| `; | |
| }); | |
| document.getElementById('tableHeader').innerHTML = headerHTML; | |
| // Build data rows | |
| let dataHTML = ''; | |
| for (let i = 0; i < rowCount; i++) { | |
| const row = data[i].slice(0, colCount); | |
| const rowChunks = chunkArray(row, colsPerRow); | |
| dataHTML += `<div class="record">`; | |
| rowChunks.forEach((chunk, j) => { | |
| dataHTML += ` | |
| <div class="record-row" style="grid-template-columns: ${gridTemplate}"> | |
| <span class="row-num">${i + 1}</span> | |
| ${chunk.map(val => `<span class="cell">${val}</span>`).join('')} | |
| ${Array(colsPerRow - chunk.length).fill('<span class="cell"></span>').join('')} | |
| </div> | |
| `; | |
| }); | |
| dataHTML += `</div>`; | |
| } | |
| document.getElementById('dataContainer').innerHTML = dataHTML; | |
| } | |
| // Watch for container resize (manual drag) | |
| const resizeObserver = new ResizeObserver(entries => { | |
| for (let entry of entries) { | |
| render(); | |
| } | |
| }); | |
| // Initial setup | |
| calculateColumnWidths(); | |
| resizeObserver.observe(document.getElementById('tableContainer')); | |
| render(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment