Matrix
A matrix sets one list against another: products against the parts they are made of, people against the projects they work on. Each pair meets in a square, and a mark in the square says how. Read across a row for what it has, and down a column for who has it.
Columns stand in groups. A group’s colour fills its bar and tints the squares below it, and each column’s name rises at 45° from the middle of its square, so many narrow columns can still be named. Under the pointer or the keyboard, the row and the column light up as a crosshair.
Design examples
Section titled “Design examples”A small matrix compares materials across activities, using marks rather than repeated words.
<section class="specimen">
<ds-matrix label="Workshop materials" literal data-showcase-props='{"groups":[{"id":"materials","label":"Materials"}],"columns":[{"id":"paper","label":"Paper","group":"materials"},{"id":"ink","label":"Ink","group":"materials"},{"id":"cloth","label":"Cloth","group":"materials"}],"rows":[{"id":"print","label":"Printmaking"},{"id":"book","label":"Bookbinding"},{"id":"draw","label":"Drawing"}],"cells":[{"row":"print","column":"paper","mark":"fill"},{"row":"print","column":"ink","mark":"fill"},{"row":"book","column":"paper","mark":"fill"},{"row":"book","column":"cloth","mark":"fill"},{"row":"draw","column":"paper","mark":"fill"}],"marks":[{"mark":"fill","label":"Included"}]}'></ds-matrix>
</section>
<style>
.specimen { width: 100%; min-width: 0; --cell: calc(20px * var(--sf, 1)); }
.specimen .line { width: 100%; }
.specimen .grow { flex: 1; min-width: 0; }
.specimen .muted { color: var(--ds-text-color-muted); }
.specimen .mono { --ds-typeface-regular: var(--ds-typeface-mono); font-variant-numeric: tabular-nums; }
.specimen .value { width: calc(4 * var(--cell)); text-align: right; }
.specimen .mark { display: inline-flex; align-items: flex-end; width: var(--cell); height: var(--ds-control-height); flex: 0 0 var(--cell); }
.specimen .columns { display: flex; flex-wrap: wrap; gap: var(--cell); }
.specimen .columns > * { min-width: 0; }
.specimen fieldset { border: 0; padding: 0; margin: 0; }
.specimen legend { padding: 0; }
.specimen label { display: flex; align-items: flex-end; gap: calc(var(--cell) / 2); min-height: var(--cell); }
.specimen ds-panel { height: calc(10 * var(--cell)); }
</style> (function mountShowcase(root) {
root.querySelectorAll("[data-showcase-props]").forEach((element) => {
const props = JSON.parse(element.dataset.showcaseProps);
Object.assign(element, props);
if (element.localName === "ds-sidebar") {
let items = props.items;
const render = () => {
let collapsedDepth;
const visible = items.filter((item) => {
const depth = item.depth ?? 0;
if (collapsedDepth !== void 0 && depth > collapsedDepth) return false;
collapsedDepth = item.expanded === false ? depth : void 0;
return true;
});
Object.assign(element, { items: visible });
};
element.addEventListener("sidebar-activate", (event) => {
const id = event.detail.id;
items = items.map((item) => ({ ...item, current: item.id === id }));
render();
});
element.addEventListener("sidebar-toggle", (event) => {
const { id, expanded } = event.detail;
items = items.map((item) => item.id === id ? { ...item, expanded } : item);
render();
});
}
});
root.querySelectorAll("ds-token-field").forEach((field) => {
field.addEventListener("ds-token-commit", (event) => {
const { text } = event.detail;
const input = field.querySelector("input");
if (!input || !text.trim()) return;
const token = document.createElement("ds-token");
token.setAttribute("value", text.trim());
token.setAttribute("label", text.trim());
token.setAttribute("remove-label", `Remove ${text.trim()}`);
field.insertBefore(token, input);
input.value = "";
input.dispatchEvent(new Event("input", { bubbles: true }));
});
field.addEventListener("ds-token-remove", (event) => {
const token = event.target;
if (token instanceof Element && token.localName === "ds-token") token.remove();
});
});
window.addEventListener("message", (event) => {
if (event.origin !== location.origin || event.source !== parent || event.data?.type !== "ds1-example-theme") return;
requestAnimationFrame(() => root.querySelectorAll("ds-chart, ds-spark, ds-bars").forEach((chart) => {
chart.refreshTheme?.();
}));
});
})(document); Try it
Section titled “Try it”Hover or move with the arrow keys: the row and the column light up. Pressing a square fires cell-press.
<ds-matrix label="Products and parts" literal totals totals-label="Products"></ds-matrix> const matrix = document.querySelector('ds-matrix');
matrix.groups = [
{ id: 'stack', label: 'Stack', color: 'var(--ds-orange)' },
{ id: 'parts', label: 'Parts', color: 'var(--ds-pink)' },
];
matrix.columns = [
{ id: 'typescript', label: 'TypeScript', group: 'stack' },
{ id: 'sign-in', label: 'Sign-in', group: 'parts' },
];
matrix.rows = [{ id: 'atlas', label: 'Atlas' }, { id: 'harbor', label: 'Harbor' }];
matrix.cells = [
{ row: 'atlas', column: 'typescript', mark: 'fill' },
{ row: 'atlas', column: 'sign-in', mark: 'triangle' },
{ row: 'harbor', column: 'sign-in', mark: 'outline' },
];
matrix.marks = [
{ mark: 'fill', label: 'Uses' },
{ mark: 'triangle', label: 'Shares one copy' },
{ mark: 'outline', label: 'Copied in' },
]; Use it
Section titled “Use it”<ds-matrix label="Products and parts" literal totals></ds-matrix>const matrix = document.querySelector("ds-matrix");matrix.groups = [ { id: "stack", label: "Stack", color: "var(--ds-orange)" }, { id: "parts", label: "Parts", color: "var(--ds-pink)" },];matrix.columns = [ { id: "typescript", label: "TypeScript", group: "stack" }, { id: "sign-in", label: "Sign-in", group: "parts" },];matrix.rows = [{ id: "atlas", label: "Atlas" }, { id: "harbor", label: "Harbor" }];matrix.cells = [ { row: "atlas", column: "typescript", mark: "fill" }, { row: "atlas", column: "sign-in", mark: "triangle", note: "From the shared source" },];matrix.marks = [ { mark: "fill", label: "Uses" }, { mark: "triangle", label: "Shares one copy" },];matrix.addEventListener("cell-press", event => open(event.detail.row, event.detail.column));A column without a group stands after the groups, untinted. marks names what each mark means: it is drawn as the legend below, and gives each square its name (“Atlas, Sign-in: Shares one copy”).
| Mark | Drawn as |
|---|---|
fill | The square filled with its column’s colour. |
outline | A 1px outline in a darker shade of the colour, over a lighter fill. |
stripes | Diagonal stripes in the darker shade, on a 4px tile that runs on from square to square. |
triangle | A small triangle in the darker shade. |
dot | A small dot in the darker shade. |
dash | A short dash in the title grey: the pair does not apply. It is not counted. |
The darker shade is the column’s colour mixed with the ink, so a column of marks stays calm instead of turning black.
Measures
Section titled “Measures”| Thing | Size |
|---|---|
| A square | One square: 19px inside and a 1px line at its end and foot, so a mark fills the inside exactly. |
| The row names | Eight squares (names), ending 4px clear of the squares, cut short with an ellipsis. |
| A group’s bar | One square high over its columns, its name in the ink on the sixteen, fading over its last 12px. |
| The column names | Six squares high, rising at 45° from the middle of their square. |
| The tint | The group’s colour at 22% under its column names and squares. |
| The outline | One 1px line round the squares, with the system’s 4px corners. |
| The totals | With totals, a last row counting each column’s marks, in small type. |
| The legend | Small type on one-square lines, starting where the squares start, each mark in a 16px box. |
Accessibility
Section titled “Accessibility”- The matrix is a grid named by
label, with the column names as column headers and the row names as row headers. - One square holds the Tab stop. The arrow keys move between squares, Home and End to a row’s first and last square, and with ⌘ (Ctrl) to the matrix’s first and last.
- Each square is named by its row, its column, its mark’s meaning and its
note; the same words show as its tooltip when the page loads the tooltips. - Enter or Space presses the square. Pressing a row’s or a column’s name fires
row-pressorcolumn-press.
| Attribute | Type | Default | What it does |
|---|---|---|---|
label | string | — | The grid’s name; literal shows it as written. |
totals | boolean | false | A last row counting each column’s marks. |
totals-label | string | The shared word “Total” | That row’s name. |
names | number | 8 | Squares for the row names. |
| Property | Type | What it is |
|---|---|---|
rows | { id, label }[] | The rows. |
columns | { id, label, group? }[] | The columns. |
groups | { id, label, color }[] | The column groups, in order. Take the colours from the palette. |
cells | { row, column, mark, note? }[] | The marks. |
marks | { mark, label }[] | The legend. |
| Event | Detail | What it means |
|---|---|---|
cell-press | { row, column, cell } | A square was pressed; cell is undefined when it is empty. |
row-press | { row } | A row’s name was pressed. |
column-press | { column } | A column’s name was pressed. |
| Part | What it is |
|---|---|
grid, group, column, name, cell, count, legend | The pieces above. |
| Custom property | Default | What it sets |
|---|---|---|
--ds-matrix-line | var(--ds-border-color) | The squares’ lines and the outline. |
--ds-matrix-colour | var(--ds-accent-color) | A column’s colour when it has no group. |