← Component Library
Data Components

Data Patterns

Ways to present and explore data — a filterable table, a heat map, a calendar, and a scheduler. Each with a live demo and the HTML & CSS.

01 · Filterable Table

Search rows as you type

An ordinary table plus a search box that hides non-matching rows. Type in the box to filter the demo. The HTML and CSS are below; a few lines of JavaScript do the filtering.

NameRoleCity
Ada LovelaceEngineerLondon
Grace HopperEngineerNew York
Mae JemisonScientistDecatur
Katherine JohnsonMathematicianHampton

HTML

<input id="filter" type="search" placeholder="Filter by name or role…">
<table>
  <thead><tr><th>Name</th><th>Role</th><th>City</th></tr></thead>
  <tbody id="rows">
    <tr><td>Ada Lovelace</td><td>Engineer</td><td>London</td></tr>
    <tr><td>Grace Hopper</td><td>Engineer</td><td>New York</td></tr>
    <tr><td>Mae Jemison</td><td>Scientist</td><td>Decatur</td></tr>
    <tr><td>Katherine Johnson</td><td>Mathematician</td><td>Hampton</td></tr>
  </tbody>
</table>

CSS

table { width: 100%; border-collapse: collapse; }
th { background: #f1f5f9; text-align: left; padding: 10px 12px; }
td { padding: 10px 12px; border-top: 1px solid #eef1f5; }

JavaScript

const filter = document.getElementById('filter');
const rows = document.querySelectorAll('#rows tr');
filter.addEventListener('input', () => {
  const q = filter.value.toLowerCase();
  rows.forEach(r => {
    r.style.display = r.textContent.toLowerCase().includes(q) ? '' : 'none';
  });
});
02 · Heat Map

Show intensity with color

A grid where each cell's color reflects a value — like a GitHub contribution graph. Darker = higher. It's just a CSS grid of cells with different background shades.

Less More

Click a cell to read its intensity level.

HTML

<div class="heat">
  <!-- one cell per value, shade by level (12 per row) -->
  <i style="background:#e0f2fe"></i><i style="background:#7dd3fc"></i><i style="background:#e0f2fe"></i><i style="background:#0284c7"></i><i style="background:#38bdf8"></i><i style="background:#e5e7eb"></i><i style="background:#7dd3fc"></i><i style="background:#0284c7"></i><i style="background:#e0f2fe"></i><i style="background:#38bdf8"></i><i style="background:#e5e7eb"></i><i style="background:#7dd3fc"></i>
  <i style="background:#38bdf8"></i><i style="background:#e5e7eb"></i><i style="background:#0284c7"></i><i style="background:#7dd3fc"></i><i style="background:#e0f2fe"></i><i style="background:#38bdf8"></i><i style="background:#0284c7"></i><i style="background:#e5e7eb"></i><i style="background:#7dd3fc"></i><i style="background:#e0f2fe"></i><i style="background:#38bdf8"></i><i style="background:#0284c7"></i>
  <i style="background:#7dd3fc"></i><i style="background:#0284c7"></i><i style="background:#e0f2fe"></i><i style="background:#e5e7eb"></i><i style="background:#38bdf8"></i><i style="background:#7dd3fc"></i><i style="background:#e0f2fe"></i><i style="background:#0284c7"></i><i style="background:#38bdf8"></i><i style="background:#7dd3fc"></i><i style="background:#e5e7eb"></i><i style="background:#e0f2fe"></i>
</div>
<div class="legend">Less <i style="background:#e5e7eb"></i><i style="background:#e0f2fe"></i><i style="background:#7dd3fc"></i><i style="background:#38bdf8"></i><i style="background:#0284c7"></i> More</div>
<p class="out">Click a cell to read its intensity level.</p>

CSS

.heat {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 4px;
  max-width: 380px;
}
.heat i { aspect-ratio: 1; border-radius: 3px; display: block; cursor: pointer; }
.heat i.sel { outline: 2px solid #0b1a33; outline-offset: 1px; }   /* clicked cell */
.legend { display: flex; align-items: center; gap: 6px; margin-top: 10px; font-size: .75rem; color: #6b7280; }
.legend i { width: 14px; height: 14px; border-radius: 3px; display: inline-block; }
.out { margin-top: 12px; font-size: .82rem; font-weight: 700; color: #0369a1; }

JavaScript — click a cell to read its level

const heat = document.querySelector('.heat');
const out  = document.querySelector('.out');

// map each shade to an intensity level 0–4
const shades = ['#e5e7eb','#e0f2fe','#7dd3fc','#38bdf8','#0284c7'];
heat.querySelectorAll('i').forEach(cell => {
  cell.addEventListener('click', () => {
    heat.querySelector('i.sel')?.classList.remove('sel');
    cell.classList.add('sel');
    const hex = cell.getAttribute('style').match(/#[0-9a-f]{6}/i)[0].toLowerCase();
    out.textContent = `Intensity level ${shades.indexOf(hex)} of 4`;
  });
});
In a real app you'd set each cell's shade from data — map a value to one of ~5 color steps and write it as the background.
03 · Calendar

A month grid

Seven columns (one per weekday) with day numbers. Highlight today and grey out days from the previous/next month.

SMTWTFS
30123456 78910111213 14151617181920 21222324252627

Click a day to select it.

HTML

<div class="calendar">
  <div class="head"><span>S</span><span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span></div>
  <div class="grid">
    <span class="muted">30</span><span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><span>6</span>
    <span>7</span><span>8</span><span>9</span><span>10</span><span>11</span><span class="today">12</span><span>13</span>
    <span>14</span><span>15</span><span>16</span><span>17</span><span>18</span><span>19</span><span>20</span>
    <span>21</span><span>22</span><span>23</span><span>24</span><span>25</span><span>26</span><span>27</span>
  </div>
</div>
<p class="out">Click a day to select it.</p>

CSS

.calendar { max-width: 320px; }
.head, .grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);   /* 7 weekdays */
  gap: 4px;
}
.head span { text-align: center; font-size: .68rem; color: #94a3b8; font-weight: 700; padding-bottom: 4px; }
.grid span { aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
             font-size: .8rem; color: #374151; border-radius: 6px;
             background: #fff; border: 1px solid #eef1f5; }
.grid span.muted { color: #cbd5e1; }
.grid span.pick  { cursor: pointer; }
.grid .today { background: #38bdf8; color: #fff; border-color: #38bdf8; font-weight: 700; }
.grid .sel   { background: #0284c7; color: #fff; border-color: #0284c7; }   /* clicked day */
.out { margin-top: 12px; font-size: .82rem; font-weight: 700; color: #0369a1; }

JavaScript — click a day to select it

const cal = document.querySelector('.grid');   // the day cells
const out = document.querySelector('.out');

cal.querySelectorAll('span:not(.muted)').forEach(day => {
  day.classList.add('pick');                   // show a pointer on real days
  day.addEventListener('click', () => {
    cal.querySelector('span.sel')?.classList.remove('sel');
    day.classList.add('sel');
    out.textContent = 'Selected: June ' + day.textContent;
  });
});
04 · Scheduler

A day's time slots

A two-column grid — times on the left, slots on the right — with events filling their slot. Great for agendas and booking views.

9:00
Team stand-up
10:00
11:00
Design review
12:00
1:00
Lunch & learn

Click an event to see its time.

HTML

<div class="schedule">
  <div class="time">9:00</div><div class="event">Team stand-up</div>
  <div class="time">10:00</div><div class="slot"></div>
  <div class="time">11:00</div><div class="event">Design review</div>
  <div class="time">12:00</div><div class="slot"></div>
  <div class="time">1:00</div><div class="event">Lunch &amp; learn</div>
</div>
<p class="out">Click an event to see its time.</p>

CSS

.schedule {
  display: grid;
  grid-template-columns: 56px 1fr;   /* time | slot */
  gap: 6px; max-width: 420px; font-size: .82rem;
}
.time { color: #94a3b8; text-align: right; padding-top: 6px; }
.slot { background: #fff; border: 1px solid #eef1f5; border-radius: 6px; min-height: 34px; }
.event {
  background: #e0f2fe;
  border-left: 3px solid #38bdf8;
  color: #0369a1; font-weight: 600;
  border-radius: 6px; padding: 7px 10px;
}
.event.sel { outline: 2px solid #0369a1; outline-offset: 1px; }   /* clicked event */
.out { margin-top: 12px; font-size: .82rem; font-weight: 700; color: #0369a1; }

JavaScript — click an event to read its time

const sched = document.querySelector('.schedule');
const out   = document.querySelector('.out');

sched.querySelectorAll('.event').forEach(ev => {
  ev.style.cursor = 'pointer';
  ev.addEventListener('click', () => {
    sched.querySelector('.event.sel')?.classList.remove('sel');
    ev.classList.add('sel');
    const time = ev.previousElementSibling.textContent;   // the time cell to its left
    out.textContent = time + ' — ' + ev.textContent;
  });
});
05 · Pie Chart

Share of a whole

A pie chart shows parts of a total. A single conic-gradient draws every slice — no SVG, no library. Pair it with a legend so the slices are labeled.

Direct — 45% Search — 25% Social — 18% Referral — 12%

HTML

<div class="pie" role="img" aria-label="Direct 45%, Search 25%, Social 18%, Referral 12%"></div>
<div class="legend">
  <span><i style="background:#38bdf8"></i> Direct — 45%</span>
  <span><i style="background:#9b7bff"></i> Search — 25%</span>
  <span><i style="background:#22c55e"></i> Social — 18%</span>
  <span><i style="background:#f59e0b"></i> Referral — 12%</span>
</div>

CSS

.pie {
  width: 140px; height: 140px; border-radius: 50%;
  background: conic-gradient(
    #38bdf8 0 45%,      /* Direct   */
    #9b7bff 45% 70%,    /* Search   */
    #22c55e 70% 88%,    /* Social   */
    #f59e0b 88% 100%    /* Referral */
  );
}
Each slice is a color start% end% stop. Want a donut? Lay a smaller white circle on top of the center.
06 · Bar Graph

Compare values with bars

Each bar is a div whose height is its value — no chart library needed. A flex row aligns them along a shared baseline.

40
Mon
75
Tue
55
Wed
90
Thu
65
Fri

Click a bar to read its value.

HTML

<div class="bars">
  <div class="col"><span class="v">40</span><div class="bar" style="height:40%"></div><span class="x">Mon</span></div>
  <div class="col"><span class="v">75</span><div class="bar" style="height:75%"></div><span class="x">Tue</span></div>
  <div class="col"><span class="v">55</span><div class="bar" style="height:55%"></div><span class="x">Wed</span></div>
  <div class="col"><span class="v">90</span><div class="bar" style="height:90%"></div><span class="x">Thu</span></div>
  <div class="col"><span class="v">65</span><div class="bar" style="height:65%"></div><span class="x">Fri</span></div>
</div>
<p class="out">Click a bar to read its value.</p>

CSS

.bars { display: flex; align-items: flex-end; gap: 14px; height: 150px; max-width: 420px; }
.col  { flex: 1; display: flex; flex-direction: column; align-items: center;
        justify-content: flex-end; height: 100%; cursor: pointer; }
.bar  { width: 100%; max-width: 42px; border-radius: 6px 6px 0 0;
        background: linear-gradient(180deg,#38bdf8,#0284c7); }   /* height = value % */
.v    { font-size: .7rem; font-weight: 700; color: #0369a1; margin-bottom: 4px; }
.x    { font-size: .72rem; color: #6b7280; margin-top: 6px; }
.col.sel .bar { background: linear-gradient(180deg,#f59e0b,#d97706); }   /* highlight the clicked bar */
.out  { margin-top: 12px; font-size: .82rem; font-weight: 700; color: #0369a1; }

JavaScript — click a bar to read its value

const bars = document.querySelector('.bars');
const out  = document.querySelector('.out');

bars.querySelectorAll('.col').forEach(col => {
  col.addEventListener('click', () => {
    bars.querySelector('.col.sel')?.classList.remove('sel');
    col.classList.add('sel');
    const value = col.querySelector('.v').textContent;
    const label = col.querySelector('.x').textContent;
    out.textContent = label + ': ' + value;
  });
});
07 · Line Graph

Show a trend over time

An inline <svg> <polyline> connects data points into a trend line — lightweight and crisp at any size.

HTML

<svg viewBox="0 0 300 120" role="img" aria-label="Upward trend">
  <polyline points="0,95 60,70 120,80 180,40 240,55 300,15"
            fill="none" stroke="#38bdf8" stroke-width="3"/>
</svg>

CSS

svg { width: 100%; height: 150px; }
polyline { stroke-linejoin: round; stroke-linecap: round; }   /* smooth corners */
08 · Data Grid

A dense, structured table

A data grid is a table built for rows of records — clear column headers, zebra striping for scan-ability, and consistent alignment.

IDNameStatusValue
001AlphaActive$1,240
002BravoPending$880
003CharlieActive$2,010
004DeltaClosed$540

Click a row to select it.

HTML

<table class="grid">
  <thead><tr><th>ID</th><th>Name</th><th>Status</th><th>Value</th></tr></thead>
  <tbody>
    <tr><td>001</td><td>Alpha</td><td>Active</td><td>$1,240</td></tr>
    <tr><td>002</td><td>Bravo</td><td>Pending</td><td>$880</td></tr>
    <tr><td>003</td><td>Charlie</td><td>Active</td><td>$2,010</td></tr>
    <tr><td>004</td><td>Delta</td><td>Closed</td><td>$540</td></tr>
  </tbody>
</table>
<p class="out">Click a row to select it.</p>

CSS

.grid { width: 100%; border-collapse: collapse; }
.grid th { background: #0b1a33; color: #fff; text-align: left; padding: 9px 12px; }
.grid td { padding: 9px 12px; border-top: 1px solid #eef1f5; }
.grid tbody tr:nth-child(even) { background: #f8fafc; }   /* zebra rows */
.grid tbody tr { cursor: pointer; }
.grid tbody tr.sel { background: #e0f2fe; }               /* selected row */
.out { margin-top: 12px; font-size: .82rem; font-weight: 700; color: #0369a1; }

JavaScript — click a row to select it

const grid = document.querySelector('.grid');   // the table
const out  = document.querySelector('.out');

grid.querySelectorAll('tbody tr').forEach(row => {
  row.style.cursor = 'pointer';
  row.addEventListener('click', () => {
    grid.querySelector('tr.sel')?.classList.remove('sel');
    row.classList.add('sel');
    out.textContent = 'Selected: ' + row.cells[0].textContent + ' · ' + row.cells[1].textContent;
  });
});
09 · Sortable Table

Click a header to sort

Click any column heading to sort the rows by it; click again to reverse the order. The arrow shows the current sort. Try sorting by Score — it sorts as numbers, not text.

NameRoleScore
Ada LovelaceEngineer92
Grace HopperEngineer88
Mae JemisonScientist105
Katherine JohnsonMathematician97

HTML

<table id="sortTable">
  <thead><tr>
    <th class="sortable">Name</th>
    <th class="sortable">Role</th>
    <th class="sortable" data-type="num">Score</th>  <!-- sort as numbers -->
  </tr></thead>
  <tbody>
    <tr><td>Ada Lovelace</td><td>Engineer</td><td>92</td></tr>
    <tr><td>Grace Hopper</td><td>Engineer</td><td>88</td></tr>
    <tr><td>Mae Jemison</td><td>Scientist</td><td>105</td></tr>
    <tr><td>Katherine Johnson</td><td>Mathematician</td><td>97</td></tr>
  </tbody>
</table>

CSS — the table plus the sort arrow

table { width: 100%; border-collapse: collapse; background: #fff;
        border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; font-size: .88rem; color: #374151; }
th { background: #f1f5f9; text-align: left; padding: 10px 12px; font-size: .7rem;
     text-transform: uppercase; color: #64748b; letter-spacing: .04em; }
td { padding: 10px 12px; border-top: 1px solid #eef1f5; }

th.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
th.sortable::after      { content: " \2195"; opacity: .35; font-size: .85em; }  /* idle */
th.sortable.asc::after  { content: " \2191"; opacity: 1; color: #0284c7; }      /* ascending */
th.sortable.desc::after { content: " \2193"; opacity: 1; color: #0284c7; }      /* descending */

JavaScript

const table   = document.getElementById('sortTable');
const tbody   = table.tBodies[0];
const headers = [...table.querySelectorAll('th.sortable')];

headers.forEach(th => th.addEventListener('click', () => {
  const asc = !th.classList.contains('asc');
  headers.forEach(h => h.classList.remove('asc', 'desc'));
  th.classList.add(asc ? 'asc' : 'desc');
  const i = th.cellIndex, num = th.dataset.type === 'num';
  [...tbody.rows].sort((a, b) => {
    let x = a.cells[i].textContent, y = b.cells[i].textContent;
    if (num) { x = parseFloat(x); y = parseFloat(y); }
    return (x > y ? 1 : x < y ? -1 : 0) * (asc ? 1 : -1);
  }).forEach(r => tbody.appendChild(r));
}));
10 · KPI Cards

Headline metrics at a glance

KPI (Key Performance Indicator) cards each show one big number, a label, and how it changed — the top row of most dashboards. A responsive grid lets them wrap on small screens.

Visitors
12,480
8.2% vs last week
Sign-ups
342
3.1%
Bounce rate
41%
2.4%
Avg. time
3:18
0:22

HTML

<div class="kpis">
  <div class="kpi">
    <p class="label">Visitors</p>
    <p class="value">12,480</p>
    <p class="trend up">▲ 8.2% vs last week</p>
  </div>
  <div class="kpi">
    <p class="label">Sign-ups</p>
    <p class="value">342</p>
    <p class="trend up">▲ 3.1%</p>
  </div>
  <div class="kpi">
    <p class="label">Bounce rate</p>
    <p class="value">41%</p>
    <p class="trend down">▼ 2.4%</p>
  </div>
  <div class="kpi">
    <p class="label">Avg. time</p>
    <p class="value">3:18</p>
    <p class="trend up">▲ 0:22</p>
  </div>
</div>

CSS

.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; }
.kpi  { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; }
.value { font-size: 1.9rem; font-weight: 800; }
.trend.up { color: #16a34a; } .trend.down { color: #dc2626; }   /* colour + arrow */

JavaScript — count each number up from zero

document.querySelectorAll('.kpi .value').forEach(el => {
  const target = el.textContent;
  if (!/^[\d,]+%?$/.test(target)) return;          // skip non-numeric like "3:18"
  const end = parseInt(target.replace(/,/g, ''), 10);
  const suffix = target.endsWith('%') ? '%' : '';
  let n = 0, step = Math.max(1, Math.round(end / 40));
  const tick = setInterval(() => {
    n = Math.min(end, n + step);
    el.textContent = n.toLocaleString() + suffix;
    if (n >= end) clearInterval(tick);
  }, 25);
});
Pair the colour with an up/down arrow or word so the change is clear without relying on green/red alone.
11 · Analytics Panel

KPIs and a chart in one card

An analytics panel groups a few KPIs with a small chart into a single dashboard widget. Click a bar to see its value. Combine this with KPI cards and a sortable table to build a full dashboard.

Weekly traffic

Last 7 days
12.4k
Visitors
+8.2%
vs last week
Selected day

HTML

<div class="analytics">
  <div class="head"><h4>Weekly traffic</h4><span>Last 7 days</span></div>
  <div class="an-kpis">
    <div class="an-kpi"><div class="v">12.4k</div><div class="k">Visitors</div></div>
    <div class="an-kpi"><div class="v">+8.2%</div><div class="k">vs last week</div></div>
    <div class="an-kpi"><div class="v" id="pick">—</div><div class="k">Selected day</div></div>
  </div>
  <div class="spark">
    <i style="height:46%" data-v="1,210" title="Mon"></i>
    <i style="height:62%" data-v="1,640" title="Tue"></i>
    <i style="height:54%" data-v="1,420" title="Wed"></i>
    <i style="height:78%" data-v="2,050" title="Thu"></i>
    <i style="height:90%" data-v="2,380" title="Fri"></i>
    <i style="height:40%" data-v="1,060" title="Sat"></i>
    <i style="height:34%" data-v="900" title="Sun"></i>
  </div>
</div>

CSS — the panel and bar “sparkline”

.analytics { background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; padding: 18px; max-width: 540px; }
.head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
.head h4 { color: #111; font-size: 1rem; margin: 0; }
.head span { font-size: .76rem; color: #6b7280; }
.an-kpis { display: flex; gap: 18px; margin-bottom: 16px; flex-wrap: wrap; }
.an-kpi .v { font-size: 1.4rem; font-weight: 800; color: #111; }
.an-kpi .k { font-size: .72rem; color: #6b7280; font-weight: 600; }

.spark { display: flex; align-items: flex-end; gap: 6px; height: 80px; }
.spark i { flex: 1; border-radius: 4px 4px 0 0; cursor: pointer;
           background: linear-gradient(180deg, #7dd3fc, #0284c7); transition: height .3s; }
/* each bar's height is set inline: style="height:78%" */

JavaScript — click a bar

const bars   = document.querySelectorAll('.spark i');
const picked = document.getElementById('pick');

bars.forEach(bar => bar.addEventListener('click', () => {
  bars.forEach(b => b.style.opacity = '.5');   // dim the rest
  bar.style.opacity = '1';                     // highlight this one
  picked.textContent = bar.dataset.v;          // show that day's value
}));
12 · Related

Keep building

More data work in Tables, Data Viz & Forms, and Dashboard Lab — or browse the full Component Library.