Best sellers this week
| Product | Sold | Takings |
|---|---|---|
| Notebooks | 120 | €1,440 |
| Fountain pens | 34 | €1,190 |
| Desk lamps | 18 | €1,080 |
| Gift cards | 52 | €1,040 |
| Total | 224 | €4,750 |
Rows of data under headings that sort, filter and explain, with rows that open, pin or group, optional selection and a foot for the pagination.
| ID | |||||
|---|---|---|---|---|---|
| Website redesign | PRJ-1041 | €48,000 | 24 | Design | |
| Mobile app | PRJ-1042 | €120,000 | 36 | Build | |
| Brand refresh | PRJ-1043 | €32,500 | 12 | Review | |
| Help center | PRJ-1044 | €18,000 | 58 | Live | |
| Checkout flow | PRJ-1045 | €64,000 | 9 | Planning |
| Choice | Values | Default | What it does |
|---|---|---|---|
looksmbk-table--look-<value> | framedflatfilled | framed | Framed: a bordered, rounded frame, the heading row on its own surface, room at the sides. Flat, for a small table inside a card: no frame, no heading surface, a rule over every row, no room at the sides, so the table's words line up with the card's other lines, and rows one line of text high. A flat table takes none of sticky-header, page-sticky-header and pin-first. Filled, for a calmer table on the page: the framed frame and rows, the heading row and the foot on a neutral fill, group rows sunken and low, no lines between headings, the same room at a row's ends and between its columns, and the arrow of a column not sorted only on hover and keyboard focus. |
sticky-headersmbk-table--sticky-header | On or off | Off | A long table in a frame of its own: the heading row stays at the frame's top and the foot at its foot while the rows scroll inside it. The consumer limits the frame's height; the frame is then a named region the keyboard can reach. Framed and filled looks. |
page-sticky-headersmbk-table--page-sticky-header | On or off | Off | A long table in the page: the frame keeps no height of its own, and the heading row stays in view under the room the page keeps clear at its top while the page scrolls, with the foot at the foot of the window, until the table ends. The table still scrolls sideways, its heading with it. Framed and filled looks. |
pin-firstsmbk-table--pin-first | On or off | Off | A wide table: the first column stays in view while the table scrolls sideways, so every row keeps its subject. For a table on the page canvas, in the framed and filled looks. |
align-topsmbk-table--align-top | On or off | Off | Cells of very different heights: every cell starts at the top of its row, and its first line sits level with the first line of the others, so the eye reads straight across a tall row. All three looks. |
Partssmbk-table__tablesmbk-table__captionsmbk-table__columnsmbk-table__header-rowsmbk-table__headersmbk-table__header-cellsmbk-table__header-textsmbk-table__header-labelsmbk-table__header-blocksmbk-table__header-iconsmbk-table__sortsmbk-table__filtersmbk-table__filter-countsmbk-table__infosmbk-table__rowsmbk-table__row-presssmbk-table__cellsmbk-table__startsmbk-table__leadsmbk-table__expandersmbk-table__pinsmbk-table__contentsmbk-table__choose-spacesmbk-table__valuesmbk-table__value-iconsmbk-table__stacksmbk-table__descriptionsmbk-table__copysmbk-table__prosesmbk-table__figuresmbk-table__barsmbk-table__groupsmbk-table__group-labelsmbk-table__group-countsmbk-table__detail-rowsmbk-table__detailsmbk-table__detail-titlesmbk-table__footersmbk-table__scroll-hintsmbk-table__scroll-tracksmbk-table__scroll-thumb
Sortable headings, six kinds of cell, and the foot with a range on the left and pages on the right.
| ID | Approved budget for this quarter. | ||||
|---|---|---|---|---|---|
| Website redesign | PRJ-1041 | €48,000 | 24 | Design | |
| Mobile app | PRJ-1042 | €120,000 | 36 | Build | |
| Brand refresh | PRJ-1043 | €32,500 | 12 | Review | |
| Help center | PRJ-1044 | €18,000 | 58 | Live | |
| Checkout flow | PRJ-1045 | €64,000 | 9 | Planning |
The flat look, for a small table inside a card: no frame and no heading surface, a rule over every row, its words lined up with the card's title, and rows one line of text high.
| Product | Sold | Takings |
|---|---|---|
| Notebooks | 120 | €1,440 |
| Fountain pens | 34 | €1,190 |
| Desk lamps | 18 | €1,080 |
| Gift cards | 52 | €1,040 |
| Total | 224 | €4,750 |
The filled look, for a calmer table on the page: the heading row and the foot on a neutral fill, sunken group rows, no lines between headings. Only the sorted column shows its arrow; point at another heading, or Tab to it, and its arrow shows.
| Stage | |||
|---|---|---|---|
| Running2 | |||
| Mobile app | Tom Reyes | €120,000 | Build |
| Help center | Sam Kim | €18,000 | Live |
| Waiting on you3 | |||
| Website redesign | Maya Chen | €48,000 | Design |
| Brand refresh | Ada Okafor | €32,500 | Review |
| Checkout flow | Lena Brandt | €64,000 | Planning |
A foot holding a line of words is one small control high; the paginations of the example above make it taller.
| Stage | |||
|---|---|---|---|
| Running2 | |||
| Mobile app | Tom Reyes | €120,000 | Build |
| Help center | Sam Kim | €18,000 | Live |
Column widths, a prose column, checkboxes, a pinned row, a row that opens with its detail, and the current row, all under group rows.
| Latest change | ||
|---|---|---|
| Changed this week2 | ||
| Website redesign | Moved the opening hours onto the contact page, so the shop and the website always say the same thing. | €48,000 |
| Mobile app | Sign-in now remembers the last account used on this phone. | €120,000 |
Next step The sign-in change reaches the first hundred testers on Tuesday. | ||
| Changed earlier1 | ||
| Help center | Split the long billing article into three short ones, each answering one question, and linked them from the invoice email. | €18,000 |
Sixteen rows under group rows in a frame lower than the table: the heading row and the foot keep their look with the rows passing under them.
| Stage | |||
|---|---|---|---|
| Running8 | |||
| Website redesign | Maya Chen | €48,000 | Design |
| Mobile app | Tom Reyes | €120,000 | Build |
| Brand refresh | Ada Okafor | €32,500 | Review |
| Help center | Sam Kim | €18,000 | Live |
| Checkout flow | Lena Brandt | €64,000 | Planning |
| Design tokens | Maya Chen | €22,000 | Build |
| Onboarding emails | Sam Kim | €9,500 | Live |
| Pricing page | Tom Reyes | €12,000 | Review |
| Waiting on you8 | |||
| Partner portal | Lena Brandt | €88,000 | Planning |
| Annual report | Ada Okafor | €27,000 | Design |
| Search upgrade | Sam Kim | €54,000 | Build |
| Careers site | Maya Chen | €16,500 | Live |
| Status page | Tom Reyes | €7,000 | Review |
| Docs migration | Lena Brandt | €41,000 | Build |
| Store locator | Ada Okafor | €19,000 | Planning |
| Newsletter | Sam Kim | €4,500 | Live |
Rows of the same kind of thing, one per line, under headings the reader can sort by: clusters, users, orders. It is for comparing and finding, not for laying out a page. A card grid is for things that each need room; a table is for many things that each need a line.
detailTrigger="cell"), rather than from a caret at the row's start.page-sticky-header), so the page keeps one scroll and the headings stay in view.sticky-header) and limit the frame's height.href, or an onClick for something that opens in the page.openerId), so the row stays one Tab stop.tallHeader), so the head of each record sits above its figures.density="compact", so it sits tighter than the page around it.align-top, so each figure stays level with the name it belongs to.label when it has a caret, a detail or a checkbox, so their names say which row they act on.href when its press goes to an address, so it stays a real link that opens in a new tab.none when it fits its frame anyway.density, on the table, and leave the rest of the page to the page's.align-top when its cells differ a lot in height, so each first line stands level with the name beside it.align-top, put a control, a tag or a picture in TableContent, which centers it on the first line.detailTrigger="cell" for the button.data-density on a part of a page other than a table's frame. A table is the one part that takes a density of its own.Approved budget for this quarter. | ||
|---|---|---|
| Website redesign | €48,000 | |
| Mobile app | €120,000 | |
| Brand refresh | €32,500 |
Put the column that names the row first, so the reader can find a row by scanning down the left.
Approved budget for this quarter. | |||
|---|---|---|---|
| €48,000 | 24 | Website redesign | |
| €120,000 | 36 | Mobile app | |
| €32,500 | 12 | Brand refresh |
With the name buried in the middle, the reader has to read across every row to find one.
| Project | Note |
|---|---|
| Website redesign | Started on 12 March by the brand team and due at the end of April. |
Keep a value to one line, and give sentences a prose column, which wraps within a readable measure.
| Project | Owner |
|---|---|
| Website redesign, started on 12 March by the brand team and due at the end of April | Maya Chen |
A sentence in a value column stays on one line, stretches the table and pushes the other columns out of view.
| Project | Budget |
|---|---|
| Website redesign | €48,000 |
| Mobile app | €120,000 |
| Brand refresh | €32,500 |
Line figures up at the end of their column, so they compare digit by digit.
| Project | Budget |
|---|---|
| Website redesign | €48,000 |
| Mobile app | €120,000 |
| Brand refresh | €32,500 |
Figures at the start of their column put hundreds under thousands.
| Product | Sold | Takings |
|---|---|---|
| Notebooks | 120 | €1,440 |
| Fountain pens | 34 | €1,190 |
| Desk lamps | 18 | €1,080 |
Inside a card, give a small table the flat look, so its words line up with the card's own and it reads as part of the card.
| Product | Sold | Takings |
|---|---|---|
| Notebooks | 120 | €1,440 |
| Fountain pens | 34 | €1,190 |
| Desk lamps | 18 | €1,080 |
A framed table inside a small card draws a second card inside the first, its words set in from the card's own.
sticky-header), whose height the page limits, the heading row stays at the frame's top and the foot at its foot while the rows scroll inside it. In the page (page-sticky-header), the frame keeps no height of its own: while the page scrolls, the heading row stays at the top of what shows and the foot at the window's foot, until the table ends. The pinned form (pin-first) keeps the first column in view while a wide table scrolls sideways. Each heading that sticks or moves draws its own rules and surface, so they stay with it and nothing passing behind shows at its edges.A row is at least one medium control high, and grows with what its cells hold: a value over its description keeps a medium inset above and below it, so its two lines never run up to the rules, and a prose cell's sentences grow the row line by line.
When one cell of a row is much taller than the others, the others sit in the middle of that height, a line or two below the name they belong to, and the eye has to match them up row by row. With align-top every cell starts at the top of its row and its first line stands where a one-line value's does, so the eye reads straight across. That holds for plain words, a value, a value over its description, a link or value that wraps and a block of the page's own, and a row's caret, pin and choose box and a value's copy button stay with the first line.
A row of one line looks the same either way, so the flag changes only the rows that are taller. Every look takes it, a held first column on a phone included. A prose column already lines its own row up at the top, and the flag does the same for a table with no column of sentences, without the prose measure. In the flat look a row is one line of text, so a tag, an avatar or a button, taller than that line, starts at the top of the row with its middle a little below the line. Markup written by hand takes the block's align-top modifier and needs no script.
Pressing a heading's filter button opens a list of choices that tick; ticking one leaves the list open, and the consumer filters the rows and passes the new count. Pressing the info button opens its explanation in a popover beside it; Escape, a press outside or moving focus away closes it. Pressing a row's caret opens or closes its detail row; the table keeps which rows are open, or the consumer does when it passes them in. With detailTrigger="cell" the table draws no caret: a button the consumer puts in one of the row's cells opens the detail, and the consumer holds which rows are open in expanded. Give that table an id; tableDetailId(id, row.id) is the id of each row's detail row, for the button's aria-controls. Pressing a sortable heading asks the consumer to sort by it; the sorted heading reads in the default text color, shows its direction with its arrow and announces it. The sort button and the copy button each give the pointer at least 24 by 24 px to press. Choosing rows is the consumer's to handle; the table draws the checkboxes and the chosen rows' fill and reports the presses. In a selectable table a row can be left out of the choosing (selectable: false): it has no checkbox and keeps the checkbox's room empty, so its first value lines up with the others, and the heading's checkbox speaks for the rows that can be chosen, ticked when every one of them is; pressing it asks the consumer to choose, or let go of, those rows only. A row also passes on any data- attribute given to it, such as 'data-task': '42', to its row element, for the page's own scripts, checks and links. A row can also be the current one (current), in a table with checkboxes or without. It is the one row that a button of the page's own in it acted on, such as the part fitted or the plan in use. It takes the chosen row's fill and darker descriptions, and its row element has aria-current="true" for screen readers. Use selected for the rows a reader chooses with the checkboxes, to act on them together, and current for the one row a button in it made current. A row can be both, and looks the same as either. Rows cannot be reordered: the table draws no drag handle, and draggable draws nothing. In the filled look (look="filled"), the arrow of a column not sorted is unseen until a reader goes to it: it shows while a pointer is over its heading and while the keyboard is on its sort button, and it keeps its room, so the heading does not move as it comes and goes. On a touch screen it shows once the column is sorted. The sort button itself never hides. The flat look (look="flat") keeps every behavior here but the two forms for big tables: their sticking cells paint a surface of their own, which a card around the table would not match. Given sticky-header or pin-first with it, the table draws without them and warns in development. A table wider than its place scrolls sideways inside its frame, so no column is ever hidden; with the first column pinned, every row keeps its subject in view while it does. A long table given a lower frame scrolls inside it with its headings in view. Nothing animates.
A table wider than its frame scrolls sideways inside it, and a long table given a lower frame scrolls down inside it, with the system's scrollbar: a thin, see-through bar over its rows that takes no room and shows while the mouse is over it or it scrolls. While the bar is hidden it is not laid out at all, so a page that measures how far the frame runs past its width just after narrowing it gets the table's own width; while the bar shows, it follows the frame's new size a moment later. Where no scrollbar takes room (the system's, or the browser's on a phone or a trackpad), a thin bar along the frame's foot also shows that there is more to the side and how much of the table is in view, and follows the scroll; it holds the frame's visible edges, and in a long table its foot, just over the table's foot. It fades while the system's scrollbar shows, in the same strip, so there is never a second bar.
A column takes its width from what its rows hold, so when the rows change, the columns move. Give a column a width and the table keeps every column's width instead: filtering, paging, showing more rows, opening a row or swapping the rows for others moves no column, because the table no longer reads its rows to size them. A width is a share of the table (share- and the percent), a size token (its path, which starts size-) or rest, the room the other columns leave, which columns taking it share equally. A column given no width in such a table takes the rest too, and there is no way to give a number. Give one column rest and the others a width, and that column takes whatever the table has over.
What a cell holds then wraps inside its column and its row grows; a word too long for the column breaks; a heading too long for it takes a second line. A tag, a button and a picture keep their size, so a column holding one needs room for it. A prose column takes the width it is given, and its lines still stop at its widest measure.
A table with column widths narrows with its frame down to a least width, and past that its frame scrolls it sideways, as any table too wide for its frame does. So a column taking the rest keeps its room on a phone, and shares stay readable. minWidth on the table gives it another least width, as a size token, or none, for a small table that may narrow with its frame. The least width has to leave room for the column taking the rest beside the columns with a size token: when they fill it, that column has nothing left. A table that gives no column a width sizes itself from its rows, as it always has.
A column can be headed by a block rather than a line: give it tallHeader, and header holds a few lines and buttons, such as a value over its description and a row of buttons. Its heading grows to fit, the heading row is as high as the tallest heading in it, and the rule runs under that one. A column without tallHeader keeps its one line. The block's lines stack and wrap, and its column stays wide enough to read them while a table too wide for its frame narrows, so the frame scrolls sideways instead. A column with a width sets the block's width, so give it room for the block's buttons.
A block can hold buttons of its own, and a button cannot hold a button, so in a tall heading the block sits outside the sort button. The arrow alone sorts, after the block, or close before it in a column lined up at the end, with the filter and info buttons after it on the block's first line. The block's own buttons do not sort. In the filled look a tall heading's arrow stays in view, since it has no words to press.
In the long forms, what is scrolled into view lands clear of the heading, however high it stands, and the same goes for a long heading wrapping in a column with a width. A heading that would take more than half of what shows, the frame or in the page what scrolls it, would cover the rows it heads, so it stops holding: it stays at the table's top and scrolls away with the rows until there is room for it again. A pinned first column stays pinned, and the foot stays at the foot.
A row can be pressed as a whole, to open what it stands for, and is a plain row until a page says what the press does. Give it href and it holds a real link, so opening in a new tab, the link's menu and every other way of following a link work from anywhere on the row. Give it onClick and it holds a real button. Give it both, for a page that moves between its own pages, and the link calls onClick, which can stop the browser going there. A disabled row takes no press. A press on a control in the row, a checkbox, a caret, a link, a button or anything else that takes a press, does that control's job and never the row's, so the page's own handlers have nothing to stop. A disabled control keeps the press too and does nothing with it. A swipe over a row scrolls, as it does anywhere.
The row takes the hover fill as every row does, and the chosen row's fill while it is pressed. While what it opened is open, the page marks it current, which keeps that fill, with the pointer still resting on the row too, and tells screen readers, on the row and on its link or button. What opens over the page, such as a dialog, is named by the row's aria-haspopup.
A press on a row can open a popover with no opener of the page's own, since the row's own link or button can be that opener. Give the table an id, and give the popover openerId, the id of the row's link or button, which tableRowPressId(id, row.id) gives. Put the popover in one of the row's cells. The row's onClick turns the popover's open over, and the page marks the row current while it is open and gives the row aria-haspopup="dialog". The row stays one Tab stop. Opening moves focus into the panel, Escape and the close button put it back on the row's own link or button, and another press on the row closes the panel. The panel hangs on the whole row, so it opens under the row's start. To hang it on the row's name, wrap the name in a div with an id and pass that as the popover's hangOnId: a span around a cell's value has only the first line's box, so the panel would cover the description. With the first column held, a press on the held cell does all this too, and focus never stays on the held cell's copy, which Tab and screen readers leave out.
It works beside everything else a row does. In a table with checkboxes, a press on the row opens it and only its checkbox chooses it, so a row can be open and chosen apart. A row with a detail keeps its caret, or the page's button in a cell, for the detail, and a press elsewhere on the row does the row's own job; the detail row is no part of the press. It works in every look and form. With the first column held (pin-first), the held cell takes the press too, and a control passing under it stays out of reach, as in every held table.
The filled look takes every form and behavior here. Its fills are see-through, so a table takes them on any surface; a heading or a foot that sticks or moves lays them over the page canvas, so the rows pass under it unseen, which suits a table on the canvas. Its foot grows with what it holds and wraps on a phone, so nothing in it runs past the frame, and it stays in place while a wide table scrolls sideways, its fill under the whole of what shows.
A table takes the density of the page it sits on. density="compact" makes one table compact in a comfortable page: it writes data-density="compact" on the frame, and everything inside the frame then takes the compact steps of the design tokens, exactly as it would on a compact page. The rows, the heading row and the foot step down together, as does what they hold: a pagination's buttons and select, a small button, the room at a tag's sides. A tag keeps its height, as it does at either density. Nothing outside the frame changes, and on a page that is compact already the option changes nothing. A row with a choose box and a caret at its start, or an avatar in a cell, stands a little taller than a plain one, as it does on a compact page.
What the table opens over the page keeps the table's density, wherever it shows, because it stays inside the frame in the page's structure: a heading's filter list, its info popover, and the list of the select in the foot. A control of the page's own in a cell or a detail row is compact too, and so is a dialog the page draws there; draw it outside the table to give it the page's density. The density is the page's to set, never a control for the reader.
Every look and form takes it. The sort, filter, info, caret and copy buttons keep their size and their focus rings. On a touch screen their press areas still stop at their own row, so they are as tall as the lower row, and never less than the least a press needs.
In the long form in a frame of its own, the table's foot stays at the frame's foot while the rows scroll under it, as the heading row stays at its top, and what the frame scrolls into view, such as a focused button, lands clear of both.
In the long form in the page, the frame never scrolls downwards, so every turn of the wheel and every swipe over the table moves the page. As the page scrolls, the heading row's real cells move down to stay just under the room the page keeps clear at the top of what scrolls it, and stop where the table ends; the foot, the scroll hint and the scrollbar's sideways bar move up to the foot of what shows, never above the heading row's own place. That room is the scroll padding at the top (scroll-padding-top) of what scrolls the table: the window, or the nearest part around the table that scrolls or clips what runs past it. The system's app bar that hides on scroll keeps its room there while it shows, and the heading rises into the room while the bar has slid away. The table still scrolls sideways inside its frame, its heading cells with their columns, and a pinned first column stays pinned. Where the browser can tie the move to the scroll itself, it moves them as it scrolls; elsewhere the table's script moves them on each scroll. What the page scrolls into view in the rows, such as a focused button, lands clear of the heading and the foot. Without the table's script, nothing moves and the table reads as a plain long one.
A reader can press the heading anywhere down a long table, and a browser that holds a scroll to a row on the screen would follow the row a sort moved. So in both long forms nothing in the frame anchors a scroll (overflow-anchor: none): a sort leaves the page, or the frame, where it is, and a site needs no rule of its own for it. The same goes for a row that opens or grows above what shows: the rows under it move down, and the scroll does not make up for it.
On a touch screen the sort, copy and caret buttons, the heading's filter and info buttons and a choose box keep their size and take an invisible press area round them, out to the comfortable touch target but never past their own row; side by side in a cell, they reach only halfway to each other. In a table with a pinned first column, what scrolls into view stops beside the pinned column, never under it.
label in place of the whole block (abbr), for those that say a column's heading before each of its cells.aria-current="true" on its row element), beside its fill.aria-current="true"), so it is heard where the focus is.smbk-table__choose-space element where a row that cannot be chosen would have its checkbox.aria-current="true".detailTrigger="cell"), give it aria-expanded, aria-controls set to the detail row's id (tableDetailId(id, row.id), with the table's id), and a name that says what it opens and for which row, its visible words included, such as "Brake pads fits 6 models".hidden.columns-fixed modifier, and holds a colgroup straight after its caption with one col per column, each the column part with its width modifier, such as smbk-table__column--width-rest. A least width is the table part's min-width modifier.smbk-table--look-filled, and its sorted heading the smbk-table__header--active modifier beside aria-sort: the modifier is what keeps that column's arrow showing. Nothing else in the markup changes.tallHeader a label, the heading in words: it names the arrow, the filter and the info button, and without one they are named from the column's key. A page in another language gives the words of the sort button's name in sortLabel.h1 to h6) out of a block: a table's heading cell cannot hold one. Give the line the text style it needs.tall modifier, smbk-table__header--tall. Its header-cell part is a div holding the block, a div with the header-block part. A sort button in it holds the arrow alone and needs an aria-label, and the heading its abbr.scroll-padding-top to the heading's height, so what scrolls into view lands clear of it.label, which names its link or button, and pass rowPressLabel when the press does something other than open the row, such as "Edit invoice INV-2042". Without a label a screen reader hears the row's id.aria-haspopup="dialog" and mark it current while the panel is open, which is how its link or button says so.pressable modifier, and as the first thing in its first cell an empty link or button with the smbk-table__row-press class and an aria-label. It needs no script. In a table with its first column held, that link or button goes first in the second cell, and the first cell takes a copy of it with tabindex="-1" and aria-hidden="true", so the held cell takes the press too.data-density="compact". Nothing else in the markup changes.| Key | What it does |
|---|---|
| Tab | Moves focus through the heading's sort, filter, info and copy buttons, the row checkboxes and carets, a block's or a cell's own buttons, the link or button of a row that can be pressed, and the scrolling frame when it has something to scroll. |
| EnterorSpace | On a button (sort, filter, info, caret, copy, a block's or a cell's own, a row's), presses it. |
| Enter | On the link of a row that can be pressed, follows it. |
| Space | On a checkbox, ticks or clears it. |
| Down ArroworUp Arrow | In a filter list, moves to the next or previous item. |
| Space | On an item of a filter list, ticks it and leaves the list open. |
| Escape | Closes a filter list or the info popover and returns focus to its button. Closes a popover a row opened and returns focus to the row's own link or button. |
| Arrow keys | On the scrolling frame, scroll it. |
The files the table is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
/* smbk-table — Table. Figma page "Table" 4312:13783: "Table" 4318:16988 over "TableHeader" 4318:11164,
* "TableHeaderCell" 4313:6100, "TableRow" 4318:11168, "TableRowCell" 4313:9290 and "TableFooter" 4318:11163.
* Three looks. Framed, the default: a bordered, rounded table, a header row on the default surface with a
* control-colored rule under it, hairlines between header cells, rows with
* a subtle rule under each that take the hover fill under the pointer and their own fill when chosen or current,
* and a foot for the pagination under a strong rule. Flat, for a table inside a card: no frame, no heading surface,
* a subtle rule over every row, no room at the table's sides, and rows one line of text high. Filled, the framed table
* with a calmer heading: the heading row and the foot on a neutral fill, group rows sunken and low, no hairlines
* between headings, and an arrow only where a reader is sorting. Numbers take tabular digits. A heading may line up at the end
* and carry a filter and an info button; a row may be pinned, highlighted, ruled off, open a detail row under it, or
* be a group heading; a column may hold prose that wraps, or keep a width of its own; a cell may hold a figure with a bar;
* and a table whose cells differ in height may line them up at the top of their rows. */
/* Positioned so the visually hidden caption is placed inside the frame; left to the page, it would sit
at the far end of a long scrolling area and stretch the document. The frame scrolls a table too wide for it. */
.smbk-table {
box-sizing: border-box;
position: relative;
display: block;
width: 100%;
border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
border-radius: var(--smbk-radius-container-sm);
color: var(--smbk-color-text-default);
/* A table wider than its place scrolls sideways inside the frame; hiding the overflow would hide data. */
overflow-x: auto;
/* The system's scrollbar: thin, in the subtle border color, on a clear track. */
scrollbar-width: thin;
scrollbar-color: var(--smbk-color-border-subtle) transparent;
}
.smbk-table__table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
/* .smbk-table__caption: hidden from view, kept for screen readers and the keyboard, by the shared rule in visually-hidden/visually-hidden.css. */
/* ---- header ---- */
.smbk-table__header-row {
background: var(--smbk-color-surface-default);
}
.smbk-table__header {
box-sizing: border-box;
height: var(--smbk-size-control-md);
padding: var(--smbk-space-inset-md) var(--smbk-space-inset-lg);
border-bottom: var(--smbk-border-width-hairline) solid var(--smbk-color-border-control);
color: var(--smbk-color-text-subtle);
font-family: var(--smbk-typography-label-sm-font-family);
font-size: var(--smbk-typography-label-sm-font-size);
font-weight: var(--smbk-typography-label-sm-font-weight);
line-height: var(--smbk-typography-label-sm-line-height);
letter-spacing: var(--smbk-typography-label-sm-letter-spacing);
text-transform: var(--smbk-typography-label-sm-text-transform);
text-align: start;
vertical-align: middle;
white-space: nowrap;
}
.smbk-table__header + .smbk-table__header {
border-left: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
}
.smbk-table__header-cell {
display: flex;
align-items: center;
gap: var(--smbk-space-inline-md);
height: var(--smbk-size-mark-md);
}
.smbk-table__header-text {
display: inline-flex;
align-items: center;
gap: var(--smbk-space-inline-xs);
}
.smbk-table__header-icon {
flex-shrink: 0;
font-size: var(--smbk-size-icon-xs);
}
/* The sort button reaches 24 px tall, the least a press needs, past the heading's line: its margins give the
room back, so the heading row keeps its height. */
.smbk-table__sort {
display: inline-flex;
align-items: center;
gap: var(--smbk-space-inline-md);
min-height: var(--smbk-size-target-min);
margin: calc((var(--smbk-size-mark-md) - var(--smbk-size-target-min)) / 2) 0;
padding: 0;
border: 0;
background: none;
color: inherit;
font: inherit;
letter-spacing: inherit;
text-transform: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
-webkit-user-select: none;
user-select: none;
}
.smbk-table__header--active {
color: var(--smbk-color-text-default);
}
/* The sort arrow: up or down on the sorted column, both ways on the others, in the subtle text color. */
.smbk-table__sort > .smbk-table__header-icon {
color: var(--smbk-color-text-subtle);
}
/* A column lined up at the end, for figures: the heading, its arrow (before the label) and its buttons move to the end. */
.smbk-table__header--align-end {
text-align: end;
}
.smbk-table__header--align-end .smbk-table__header-cell {
justify-content: flex-end;
}
/* The filter and info buttons: the extra-small ghost icon button, 24 tall in the 20 px heading line; their margins
give the room back, so the heading row keeps its height. */
.smbk-table__header-cell .smbk-table__filter,
.smbk-table__header-cell .smbk-table__info {
margin-block: calc((var(--smbk-size-mark-md) - var(--smbk-size-control-xs)) / 2);
}
/* How many filters apply, close after the funnel. The button's name says the same in words. */
.smbk-table__filter-count {
margin-inline-start: calc(var(--smbk-space-inline-xs) - var(--smbk-space-inline-md));
}
@media (hover: hover) {
.smbk-table__sort:hover {
color: var(--smbk-color-text-default);
}
}
.smbk-table__sort:focus-visible {
outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
outline-offset: var(--smbk-focus-ring-offset);
border-radius: var(--smbk-radius-control-xs);
}
/* ---- rows ---- */
.smbk-table__row {
border-bottom: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
}
.smbk-table__row:last-child {
border-bottom: 0;
}
/* Group headings cut the rows into bodies of their own: the last row of each body but the last keeps its rule, and a
row whose closed detail row ends the body takes none, as the last row. */
.smbk-table__table > tbody:not(:last-child) > .smbk-table__row:last-child {
border-bottom: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
}
.smbk-table__table > tbody:last-child > .smbk-table__row:has(+ .smbk-table__detail-row[hidden]:last-child) {
border-bottom: 0;
}
/* An open row and its detail read as one: no rule between them. The detail row keeps the rule under it. */
.smbk-table__row:has(+ .smbk-table__detail-row:not([hidden])) {
border-bottom: 0;
}
@media (hover: hover) {
.smbk-table__row:hover {
background: var(--smbk-color-surface-hover);
}
/* A group heading and an open row's detail are not records: no hover fill. */
.smbk-table__row--type-group:hover,
.smbk-table__detail-row:hover {
background: none;
}
}
/* A chosen row: its own fill, beside the ticked checkbox that says so without color. The current row, the one the
page's own button in it acted on, in a table with or without checkboxes, takes the same fill; its row says so to a
screen reader (aria-current) and the page in words. Each keeps its fill under the pointer: a press with a mouse
leaves the pointer on the row it made current, and a hover fill there would say it had not. A description on either
takes the subtle text color, which keeps 4.5:1 on the fill where the subtlest does not. */
.smbk-table__row--selected,
.smbk-table__row--current,
.smbk-table__row--selected:hover,
.smbk-table__row--current:hover {
background: var(--smbk-color-action-default-background-pressed);
}
.smbk-table__row--selected .smbk-table__description,
.smbk-table__row--current .smbk-table__description {
color: var(--smbk-color-text-subtle);
}
/* A highlighted row, a linked or matching record: the info tint. Its descriptions take the subtle text color too,
which keeps 4.5:1 on the tint. The tint says nothing to a screen reader: the page says why in words. */
.smbk-table__row--highlighted {
background: var(--smbk-color-feedback-info-surface-subtle);
}
.smbk-table__row--highlighted .smbk-table__description {
color: var(--smbk-color-text-subtle);
}
/* A pinned row: a brand edge on its start side, beside the pin in its leading room that says so without color. */
.smbk-table__row--pinned > .smbk-table__cell:first-child {
box-shadow: inset var(--smbk-border-width-accent) 0 0 0 var(--smbk-color-border-brand);
}
.smbk-table__row--pinned > .smbk-table__cell:first-child:dir(rtl) {
box-shadow: inset calc(-1 * var(--smbk-border-width-accent)) 0 0 0 var(--smbk-color-border-brand);
}
/* A strong rule above a row separates groups. It is drawn by the row's cells, so it wins over the row above's rule. */
.smbk-table__row--rule-above > * {
border-top: var(--smbk-border-width-hairline) solid var(--smbk-color-border-strong);
}
/* A group heading: one cell across the row, its label and a count in the subtle color, no fill of its own. */
.smbk-table__group {
box-sizing: border-box;
height: var(--smbk-size-control-md);
padding: var(--smbk-space-inset-sm) var(--smbk-space-inset-lg);
color: var(--smbk-color-text-default);
font-family: var(--smbk-typography-label-md-font-family);
font-size: var(--smbk-typography-label-md-font-size);
font-weight: var(--smbk-typography-label-md-font-weight);
line-height: var(--smbk-typography-label-md-line-height);
letter-spacing: var(--smbk-typography-label-md-letter-spacing);
text-transform: var(--smbk-typography-label-md-text-transform);
font-variant-numeric: tabular-nums;
text-align: start;
vertical-align: middle;
white-space: nowrap;
}
.smbk-table__group-label {
display: inline-flex;
align-items: center;
gap: var(--smbk-space-inline-sm);
}
.smbk-table__group-count {
color: var(--smbk-color-text-subtle);
}
/* The row a row unfolds: one cell across every column on the sunken surface, its text starting where the group
heading's does. Closed, it is hidden, so it is out of the reading order. A caret at the row's start opens it, or a
button of the page's own in one of its cells, which then draws no caret and no leading room. */
.smbk-table__detail {
box-sizing: border-box;
padding: var(--smbk-space-inset-lg);
background: var(--smbk-color-surface-sunken);
color: var(--smbk-color-text-subtle);
font-family: var(--smbk-typography-body-sm-font-family);
font-size: var(--smbk-typography-body-sm-font-size);
font-weight: var(--smbk-typography-body-sm-font-weight);
line-height: var(--smbk-typography-body-sm-line-height);
letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
text-align: start;
vertical-align: top;
white-space: normal;
}
.smbk-table__detail > p {
margin: 0;
}
/* Its title sits straight over the text under it: the line heights give the air. */
.smbk-table__detail-title {
margin: 0;
color: var(--smbk-color-text-default);
font-family: var(--smbk-typography-label-md-font-family);
font-size: var(--smbk-typography-label-md-font-size);
font-weight: var(--smbk-typography-label-md-font-weight);
line-height: var(--smbk-typography-label-md-line-height);
letter-spacing: var(--smbk-typography-label-md-letter-spacing);
text-transform: var(--smbk-typography-label-md-text-transform);
}
.smbk-table__row--disabled {
color: var(--smbk-color-text-disabled);
}
/* Pictures, tags and links inside a disabled row fade with the disabled-media opacity, as Figma dims them. */
.smbk-table__row--disabled .smbk-table__cell > * {
opacity: var(--smbk-opacity-disabled);
}
/* A row is at least one medium control high (40, or 32 in compact); what a cell holds is one small control high and
sits centered in it, as Figma's TableRowCell does. The room above and below is padding, half of what the row has
over the small control, so a row of one line keeps its height and taller content grows the row with that room
kept (a cell's height is only its least). */
.smbk-table__cell {
box-sizing: border-box;
height: var(--smbk-size-control-md);
padding: calc((var(--smbk-size-control-md) - var(--smbk-size-control-sm)) / 2) var(--smbk-space-inset-lg);
font-family: var(--smbk-typography-body-sm-font-family);
font-size: var(--smbk-typography-body-sm-font-size);
font-weight: var(--smbk-typography-body-sm-font-weight);
line-height: var(--smbk-typography-body-sm-line-height);
letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
font-variant-numeric: tabular-nums;
vertical-align: middle;
white-space: nowrap;
}
/* A column lined up at the end, for figures: they line up on their last digit. */
.smbk-table__cell--align-end {
text-align: end;
}
/* A column of prose: sentences wrap between the prose measures, and the first line sits where a one-line value's
does. The other cells of its row align to the top with it. */
.smbk-table__cell--type-prose {
height: auto;
padding-block: calc((var(--smbk-size-control-md) - 1lh) / 2);
vertical-align: top;
white-space: normal;
}
.smbk-table__prose {
min-width: var(--smbk-size-table-prose-min);
max-width: var(--smbk-size-table-prose-max);
}
.smbk-table__row:has(> .smbk-table__cell--type-prose) > .smbk-table__cell:not(.smbk-table__cell--type-prose) {
padding-block-start: calc((var(--smbk-size-control-md) - var(--smbk-size-control-sm)) / 2);
vertical-align: top;
}
/* Rows aligned to the top, for cells of very different heights: every cell starts at the top of its row, and its first
line sits where a one-line value's does, so the eye reads across the row. A row of one line looks as it always does.
The two lengths say where that first line is: its room, one small control high, and how far down the row that room
starts. A control keeps the room to itself, and a line of words sits in the middle of it. */
.smbk-table--align-top {
--smbk-table-line: var(--smbk-size-control-sm);
--smbk-table-line-start: calc((var(--smbk-size-control-md) - var(--smbk-size-control-sm)) / 2);
}
.smbk-table--align-top .smbk-table__cell:not(.smbk-table__cell--type-prose) {
padding-block-start: var(--smbk-table-line-start);
vertical-align: top;
}
/* Words or a block of the page's own in a cell, with none of the table's pieces to center them. */
.smbk-table--align-top .smbk-table__cell:not(.smbk-table__cell--type-prose, :has(.smbk-table__content, .smbk-table__start)) {
padding-block-start: calc(var(--smbk-table-line-start) + (var(--smbk-table-line) - 1lh) / 2);
}
/* Set at the line's top, a piece cannot be pushed down by its own baseline. */
.smbk-table--align-top :is(.smbk-table__content, .smbk-table__start) {
vertical-align: top;
}
/* A value or a link that wraps keeps its first line in that room, its other lines under it. */
.smbk-table--align-top :is(.smbk-table__value, .smbk-table__content > .smbk-link) {
margin-block: calc((var(--smbk-table-line) - 1lh) / 2);
}
.smbk-table--align-top .smbk-table__value {
align-items: flex-start;
}
/* What stands beside those lines stays with the first of them: the icon before a value, the copy button after it, and
a row's caret, pin and choose box. */
.smbk-table--align-top .smbk-table__value-icon {
margin-block-start: calc((1lh - var(--smbk-size-icon-sm)) / 2);
}
.smbk-table--align-top .smbk-table__start {
align-items: flex-start;
}
.smbk-table--align-top .smbk-table__lead {
min-height: var(--smbk-table-line);
}
.smbk-table--align-top .smbk-table__content > :is(.smbk-table__copy, .smbk-checkbox) {
align-self: flex-start;
margin-block-start: calc((var(--smbk-table-line) - var(--smbk-size-target-min)) / 2);
}
/* The first cell's leading room, in a table with a caret that opens a row or a pinned row: one small icon wide in every
row and in the heading, so the columns line up, holding the caret or the pin. A row opened by the page's own button
in a cell has no caret, and needs no room. */
.smbk-table__start {
display: inline-flex;
align-items: center;
gap: var(--smbk-space-inline-lg);
}
.smbk-table__lead {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
gap: var(--smbk-space-inline-xs);
min-width: var(--smbk-size-icon-sm);
}
.smbk-table__header-cell > .smbk-table__lead {
margin-inline-end: calc(var(--smbk-space-inline-lg) - var(--smbk-space-inline-md));
}
.smbk-table__pin {
color: var(--smbk-color-text-subtle);
font-size: var(--smbk-size-icon-sm);
}
/* The caret that opens a row: the glyph inside a 24 px box, the least a press needs; the box's margins give the
room back. It points to the end while closed and down while open. */
.smbk-table__expander {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: var(--smbk-size-target-min);
height: var(--smbk-size-target-min);
margin: calc((var(--smbk-size-icon-sm) - var(--smbk-size-target-min)) / 2);
padding: 0;
border: 0;
background: none;
color: var(--smbk-color-text-subtle);
font-size: var(--smbk-size-icon-sm);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.smbk-table__expander[aria-expanded='false']:dir(rtl) > * {
transform: scaleX(-1);
}
@media (hover: hover) {
.smbk-table__expander:hover {
color: var(--smbk-color-text-default);
}
}
.smbk-table__expander:focus-visible {
outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
outline-offset: var(--smbk-focus-ring-offset);
border-radius: var(--smbk-radius-control-xs);
}
/* The content of a cell: a row of its pieces, one small control high. */
.smbk-table__content {
display: inline-flex;
align-items: center;
gap: var(--smbk-space-inline-md);
min-height: var(--smbk-size-control-sm);
}
/* A row that cannot be chosen, in a table whose rows can: the room the small choose box takes, its box and the room
the choice row keeps after it, kept empty, so the row's first value lines up with the others'. */
.smbk-table__choose-space {
flex: none;
width: calc(var(--smbk-size-selection-sm) + var(--smbk-space-inline-sm));
}
/* An icon before the value sits the small inline gap from it, as a 16 icon beside 14 px text does across the system. */
.smbk-table__value {
display: inline-flex;
align-items: center;
gap: var(--smbk-space-inline-sm);
}
.smbk-table__value-icon {
flex-shrink: 0;
font-size: var(--smbk-size-icon-sm);
}
/* A cell holding a value over its description keeps a medium inset above and below, so the two lines never run up
to the rules: the row grows to hold them. */
.smbk-table__cell:has(.smbk-table__stack) {
padding-block: var(--smbk-space-inset-md);
}
/* A value over its description: no gap, the line heights give the air. */
.smbk-table__stack {
display: inline-flex;
flex-direction: column;
gap: var(--smbk-space-text-label);
font-family: var(--smbk-typography-body-xs-font-family);
font-size: var(--smbk-typography-body-xs-font-size);
font-weight: var(--smbk-typography-body-xs-font-weight);
line-height: var(--smbk-typography-body-xs-line-height);
letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
}
.smbk-table__description {
color: var(--smbk-color-text-subtlest);
}
/* In a row aligned to the top, the value over its description is smaller type than a one-line value. Half the difference
of their line heights and of their sizes further down, its letters stand on the same line as those beside it. Under
it, the cell's own inset. */
.smbk-table--align-top .smbk-table__value:has(> .smbk-table__stack) {
margin-block-end: 0;
}
.smbk-table--align-top .smbk-table__stack {
margin-block-start: calc((var(--smbk-typography-body-sm-font-size) * var(--smbk-typography-body-sm-line-height) - 1lh + var(--smbk-typography-body-sm-font-size) - 1em) / 2);
}
/* The copy glyph, inside a 24 px box, the least a press needs; the box reaches past the glyph and its
margins give the room back, so the value does not move. */
.smbk-table__copy {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: var(--smbk-size-target-min);
height: var(--smbk-size-target-min);
margin: calc((var(--smbk-size-icon-sm) - var(--smbk-size-target-min)) / 2);
padding: 0;
border: 0;
background: none;
color: var(--smbk-color-text-subtle);
font-size: var(--smbk-size-icon-sm);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
@media (hover: hover) {
.smbk-table__copy:hover {
color: var(--smbk-color-text-brand);
}
}
.smbk-table__copy:focus-visible {
outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
outline-offset: var(--smbk-focus-ring-offset);
border-radius: var(--smbk-radius-control-xs);
}
/* A figure with a bar after it: the progress track and fill, in the default tone, at a fixed length. The figure is
the value; the bar is decoration for the eye. */
.smbk-table__figure {
flex-shrink: 0;
}
.smbk-table__content > .smbk-table__bar {
flex-shrink: 0;
width: var(--smbk-size-80);
}
/* ---- foot ---- */
.smbk-table__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--smbk-space-inline-md);
box-sizing: border-box;
min-height: var(--smbk-size-control-lg);
padding: var(--smbk-space-inset-xs) var(--smbk-space-inset-lg);
border-top: var(--smbk-border-width-hairline) solid var(--smbk-color-border-strong);
}
/* ---- the flat look: a table inside a card ----
No frame and no corner, no fill on the heading row and no hairlines between its headings, a subtle rule over every
row, the heading row's included, and no room at the table's first and last cell, so its words line up with the
card's other lines. Each row is one line of the body text with a small inset above and below, the heading row as
high as the others; the headings are in the small body style. The frame is still there, unseen, and still scrolls a
table too wide for it, with its scrollbar and its scroll hint. A flat table takes neither the long nor the pinned
form (the wrapper drops them): their sticking cells paint a surface of their own, which a card would not match. */
.smbk-table--look-flat {
border: 0;
border-top: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
border-radius: 0;
}
.smbk-table--look-flat .smbk-table__header-row {
background: none;
}
.smbk-table--look-flat .smbk-table__header,
.smbk-table--look-flat .smbk-table__cell {
height: auto;
padding: var(--smbk-space-inset-sm) 0;
}
/* Between columns, the room a cell keeps before its words; none before the first or after the last. */
.smbk-table--look-flat .smbk-table__header + .smbk-table__header,
.smbk-table--look-flat .smbk-table__cell + .smbk-table__cell {
padding-inline-start: var(--smbk-space-inline-md);
}
.smbk-table--look-flat .smbk-table__header {
border: 0;
font-family: var(--smbk-typography-body-xs-font-family);
font-size: var(--smbk-typography-body-xs-font-size);
font-weight: var(--smbk-typography-body-xs-font-weight);
line-height: var(--smbk-typography-body-xs-line-height);
letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
text-transform: var(--smbk-typography-body-xs-text-transform);
}
/* The heading's line is as high as a row's, so the heading row is as high as the rows under it. */
.smbk-table--look-flat .smbk-table__header-cell {
height: calc(var(--smbk-typography-body-sm-font-size) * var(--smbk-typography-body-sm-line-height));
}
.smbk-table--look-flat .smbk-table__row {
border-top: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
border-bottom: 0;
}
/* An open row's detail joins it, with no rule over it; the next row's rule runs under it. */
.smbk-table--look-flat .smbk-table__detail-row {
border-top: 0;
}
/* What a cell holds sets its row's height: a line of text, or a control with the inset above and below it. Set at the
line's top, an icon before a value cannot push the line down by its own baseline. */
.smbk-table--look-flat .smbk-table__content,
.smbk-table--look-flat .smbk-table__start {
min-height: 0;
vertical-align: top;
}
/* A prose column's first line, and the other cells of its row, start the same inset down. */
.smbk-table--look-flat .smbk-table__row:has(> .smbk-table__cell--type-prose) > .smbk-table__cell {
padding-block-start: var(--smbk-space-inset-sm);
}
/* Aligned to the top, a flat row's first line is that one line of text, the same inset down. */
.smbk-table--look-flat.smbk-table--align-top {
--smbk-table-line: calc(var(--smbk-typography-body-sm-font-size) * var(--smbk-typography-body-sm-line-height));
--smbk-table-line-start: var(--smbk-space-inset-sm);
}
.smbk-table--look-flat .smbk-table__group {
height: auto;
padding-inline: 0;
}
.smbk-table--look-flat .smbk-table__footer {
padding-inline: 0;
}
/* ---- columns with a width of their own ----
A table whose columns name their widths, in the column elements at its top, keeps them whatever its rows hold: the
browser lays such a table out from those elements alone and never reads a row, so filtering, paging or opening a row
moves no column. A column takes a share of the table, a size token, or the room the others leave. What a cell holds
wraps inside its column, as the widths no longer follow it; a word too long for the column breaks, and the row grows.
The table stops narrowing at its least width, past which its frame scrolls it sideways, so a column taking the room
that is left keeps some on a phone. A table that names no width has none of this, and sizes itself as it always has. */
.smbk-table__table--columns-fixed {
table-layout: fixed;
min-width: var(--smbk-size-width-480);
overflow-wrap: anywhere;
}
.smbk-table__table--columns-fixed :is(.smbk-table__header, .smbk-table__cell, .smbk-table__group) {
white-space: normal;
}
/* A heading too long for its column takes a second line, its row growing with it. */
.smbk-table__table--columns-fixed .smbk-table__header-cell {
height: auto;
min-height: var(--smbk-size-mark-md);
}
.smbk-table--look-flat .smbk-table__table--columns-fixed .smbk-table__header-cell {
min-height: calc(var(--smbk-typography-body-sm-font-size) * var(--smbk-typography-body-sm-line-height));
}
.smbk-table__table--columns-fixed .smbk-table__sort {
text-align: inherit;
}
/* The column sets a prose cell's width, so its narrowest measure no longer holds; its widest still keeps lines readable. */
.smbk-table__table--columns-fixed .smbk-table__prose {
min-width: 0;
}
/* What a cell lines up stops at its column, and a figure's bar gives way before its figure. */
.smbk-table__table--columns-fixed :is(.smbk-table__content, .smbk-table__start) {
max-width: 100%;
}
.smbk-table__table--columns-fixed .smbk-table__content > .smbk-table__bar {
flex-shrink: 1;
min-width: 0;
}
/* The room the columns with a width leave. Columns that take it share it equally. */
.smbk-table__column--width-rest {
width: auto;
}
/* A share of the table's width, so the column grows and narrows with the table. */
.smbk-table__column--width-share-5 {
width: 5%;
}
.smbk-table__column--width-share-10 {
width: 10%;
}
.smbk-table__column--width-share-15 {
width: 15%;
}
.smbk-table__column--width-share-20 {
width: 20%;
}
.smbk-table__column--width-share-25 {
width: 25%;
}
.smbk-table__column--width-share-30 {
width: 30%;
}
.smbk-table__column--width-share-35 {
width: 35%;
}
.smbk-table__column--width-share-40 {
width: 40%;
}
.smbk-table__column--width-share-45 {
width: 45%;
}
.smbk-table__column--width-share-50 {
width: 50%;
}
.smbk-table__column--width-share-55 {
width: 55%;
}
.smbk-table__column--width-share-60 {
width: 60%;
}
.smbk-table__column--width-share-65 {
width: 65%;
}
.smbk-table__column--width-share-70 {
width: 70%;
}
.smbk-table__column--width-share-75 {
width: 75%;
}
.smbk-table__column--width-share-80 {
width: 80%;
}
.smbk-table__column--width-share-85 {
width: 85%;
}
.smbk-table__column--width-share-90 {
width: 90%;
}
.smbk-table__column--width-share-95 {
width: 95%;
}
/* A size token: the column keeps this width whatever the table's. */
.smbk-table__column--width-size-40 {
width: var(--smbk-size-40);
}
.smbk-table__column--width-size-48 {
width: var(--smbk-size-48);
}
.smbk-table__column--width-size-56 {
width: var(--smbk-size-56);
}
.smbk-table__column--width-size-64 {
width: var(--smbk-size-64);
}
.smbk-table__column--width-size-72 {
width: var(--smbk-size-72);
}
.smbk-table__column--width-size-80 {
width: var(--smbk-size-80);
}
.smbk-table__column--width-size-88 {
width: var(--smbk-size-88);
}
.smbk-table__column--width-size-96 {
width: var(--smbk-size-96);
}
.smbk-table__column--width-size-width-240 {
width: var(--smbk-size-width-240);
}
.smbk-table__column--width-size-width-320 {
width: var(--smbk-size-width-320);
}
.smbk-table__column--width-size-width-400 {
width: var(--smbk-size-width-400);
}
.smbk-table__column--width-size-width-480 {
width: var(--smbk-size-width-480);
}
/* How narrow the table gets before its frame scrolls it sideways, in place of the least width above; none lets it
narrow with its frame. */
.smbk-table__table--min-width-none {
min-width: 0;
}
.smbk-table__table--min-width-size-width-320 {
min-width: var(--smbk-size-width-320);
}
.smbk-table__table--min-width-size-width-400 {
min-width: var(--smbk-size-width-400);
}
.smbk-table__table--min-width-size-width-480 {
min-width: var(--smbk-size-width-480);
}
.smbk-table__table--min-width-size-width-640 {
min-width: var(--smbk-size-width-640);
}
.smbk-table__table--min-width-size-width-720 {
min-width: var(--smbk-size-width-720);
}
.smbk-table__table--min-width-size-width-960 {
min-width: var(--smbk-size-width-960);
}
/* ---- a long table: the heading row stays in view ----
Two forms. In a frame of its own (sticky-header), the consumer limits the frame's height; the frame then scrolls
both ways, the headings stick to its top and the foot to its foot. In the page (page-sticky-header), the frame keeps
no height and scrolls only sideways, and the page's own scroll moves the real heading cells down to stay in view, and
the foot up to the window's foot, until the table ends (below). A collapsed border stays where it was drawn, so in
both a heading that sticks or moves carries its rule as a line of its own. */
/* The heading can be pressed anywhere down the table, and a browser anchoring a scroll to a row would follow the row the
sort moved. Nothing in either form's frame anchors a scroll, so the reader stays where they are. */
.smbk-table--sticky-header {
overflow-y: auto;
isolation: isolate;
overflow-anchor: none;
/* What the frame scrolls into view (a focused cell, a button) lands clear of the heading and the foot over the rows. */
scroll-padding-block-start: var(--smbk-size-control-md);
}
.smbk-table--sticky-header:has(> .smbk-table__footer) {
scroll-padding-block-end: var(--smbk-table-foot-height, var(--smbk-size-control-lg));
}
/* A frame the keyboard reaches: one that scrolls up and down, or a table too wide for it (the wrapper makes it a region then). */
.smbk-table:focus-visible {
outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
outline-offset: var(--smbk-focus-ring-offset);
}
.smbk-table--sticky-header .smbk-table__header {
position: sticky;
top: 0;
z-index: var(--smbk-layer-raised);
border-bottom-color: transparent;
background: var(--smbk-color-surface-default);
}
/* The rule under a sticking heading, and the hairline between headings, drawn by the heading itself. The rule's part
also lays the heading's surface again under its words, a hairline past its top edge and past its sides. A cell paints
nothing in its share of a collapsed border, so without it the rows would show between two headings and the rule would
break there; and a heading that moves with the scroll can land a device pixel under its place when the table sits
between two pixels, so they would show along its top too. It stops at the table's two ends, since a part past them
would give the frame something to scroll. */
:is(.smbk-table--sticky-header, .smbk-table--page-sticky-header) .smbk-table__header::after {
content: '';
position: absolute;
z-index: -1;
inset-block: calc(-1 * var(--smbk-border-width-hairline)) 0;
inset-inline: 0;
border-bottom: var(--smbk-border-width-hairline) solid var(--smbk-color-border-control);
background: inherit;
}
:is(.smbk-table--sticky-header, .smbk-table--page-sticky-header) .smbk-table__header:not(:first-child)::after {
inset-inline-start: calc(-1 * var(--smbk-border-width-hairline));
}
:is(.smbk-table--sticky-header, .smbk-table--page-sticky-header) .smbk-table__header:not(:last-child)::after {
inset-inline-end: calc(-1 * var(--smbk-border-width-hairline));
}
:is(.smbk-table--sticky-header, .smbk-table--page-sticky-header) .smbk-table__header + .smbk-table__header {
border-left-color: transparent;
}
:is(.smbk-table--sticky-header, .smbk-table--page-sticky-header) .smbk-table__header + .smbk-table__header::before {
content: '';
position: absolute;
top: 0;
bottom: var(--smbk-border-width-hairline);
left: 0;
width: var(--smbk-border-width-hairline);
background: var(--smbk-color-border-subtle);
}
/* The foot stays at the frame's foot while the rows scroll under it, on the heading row's floor. Sideways it scrolls
with the table, as in every table, so a foot wider than a narrow frame can still be scrolled to its end. */
.smbk-table--sticky-header .smbk-table__footer {
position: sticky;
bottom: 0;
z-index: var(--smbk-layer-raised);
background: var(--smbk-color-surface-default);
}
/* ---- a long table in the page: the heading row and the foot follow the page's scroll ----
The frame scrolls sideways only; it never scrolls downwards, so the wheel and the finger always move the page. The
heading cells move down by the page's scroll to stay just under the room the page keeps clear at the top of what
scrolls it (its scroll padding, which the system's app bar sets while it sticks), and stop where the table ends;
the foot, the scroll hint and the scrollbar's sideways track move up to the window's foot, never above the heading
row's own place. The wrapper's script writes how far they may move (--smbk-table-travel); where the browser links
an animation to a scroll, it moves them as it scrolls, and elsewhere the script does. Sideways the heading cells
scroll with their columns, as they are the real cells; a held first column stays held. Without the script nothing
moves, and the table reads as a plain long one. */
.smbk-table--page-sticky-header {
overflow-y: hidden;
isolation: isolate;
overflow-anchor: none;
}
.smbk-table--page-sticky-header .smbk-table__header {
position: relative;
z-index: var(--smbk-layer-raised);
border-bottom-color: transparent;
background: var(--smbk-color-surface-default);
}
.smbk-table--page-sticky-header .smbk-table__footer {
position: relative;
z-index: var(--smbk-layer-raised);
background: var(--smbk-color-surface-default);
}
/* A table's height and the text above it often leave the frame's foot on part of a pixel, and a layer moved by part of
a pixel draws its edge across two rows of pixels, with the rows showing there. So the script writes the part of a
pixel that lands the foot on a whole one (--smbk-table-foot-snap), to the nearest, and what follows the table moves
by that much. The heading's move starts later or sooner by a part of its own (below). */
.smbk-table--page-sticky-header > .smbk-table__table {
margin-block-end: var(--smbk-table-foot-snap, 0px);
}
/* What the page scrolls into view inside the rows (a focused cell, a button) lands clear of the heading and the foot. */
.smbk-table--page-sticky-header .smbk-table__table > tbody * {
scroll-margin-block: var(--smbk-size-control-md) var(--smbk-size-control-lg);
}
/* While the system's app bar that hides on scroll has slid away, the room it kept at the window's top is free, and
the heading rises into it. */
:root:has(.smbk-app-bar--scrolled-away) .smbk-table--page-sticky-header {
--smbk-table-room-top: 0px;
}
@supports (animation-timeline: view()) {
/* The frame's place in what scrolls it, inside the room kept clear at its top and foot. */
.smbk-table--page-sticky-header {
view-timeline-name: --smbk-table-page;
view-timeline-axis: block;
view-timeline-inset: var(--smbk-table-room-top, auto) auto;
}
/* Down by as much as the frame has passed that top, from its edge, to the table's end. The text above a table often
leaves the frame on part of a pixel, and a heading moved by part of a pixel draws its rule across two rows of pixels,
with the rows showing there. So the move starts later or sooner by the part of a pixel that makes it a whole one
(--smbk-table-top-snap), which the script writes. */
.smbk-table--page-sticky-header .smbk-table__header {
animation-name: smbk-table-heading;
animation-duration: auto;
animation-timing-function: linear;
animation-fill-mode: both;
animation-timeline: --smbk-table-page;
animation-range:
exit-crossing calc(var(--smbk-border-width-hairline) + var(--smbk-table-top-snap, 0px))
exit-crossing calc(var(--smbk-border-width-hairline) + var(--smbk-table-top-snap, 0px) + var(--smbk-table-travel, 0px));
}
/* Up by as much as the frame runs past the foot, from its edge, as far as the heading row's place. */
.smbk-table--page-sticky-header > :is(.smbk-table__scroll-hint, .smbk-table__footer),
.smbk-table--page-sticky-header > .smbk-scrollbar .smbk-scrollbar__track--x {
animation-name: smbk-table-foot;
animation-duration: auto;
animation-timing-function: linear;
animation-fill-mode: both;
animation-timeline: --smbk-table-page;
animation-range:
entry-crossing calc(100% - var(--smbk-border-width-hairline) - var(--smbk-table-travel, 0px))
entry-crossing calc(100% - var(--smbk-border-width-hairline));
}
}
@keyframes smbk-table-heading {
from {
translate: 0 0;
}
to {
translate: 0 var(--smbk-table-travel, 0px);
}
}
@keyframes smbk-table-foot {
from {
translate: 0 calc(-1 * var(--smbk-table-travel, 0px));
}
to {
translate: 0 0;
}
}
/* ---- a wide table: the first column stays in view ----
The pinned cells are painted on the page canvas, with the row's hover laid over it. They move only sideways, along
the rules the table draws across each row, so those rules show through them, on the same pixels as in every other
cell; a copy of its own in a pinned cell draws a pixel off them in every browser. */
.smbk-table--pin-first {
isolation: isolate;
}
.smbk-table--pin-first .smbk-table__header:first-child,
.smbk-table--pin-first .smbk-table__cell:first-child {
position: sticky;
left: 0;
}
.smbk-table--pin-first .smbk-table__header:first-child {
z-index: var(--smbk-layer-sticky);
background: var(--smbk-color-surface-default);
}
/* Raised, so what a later cell positions (a bar's track, a meter) passes under the held names rather than over them;
the first heading stays above it, and the scroll hint, coming after it, over it. */
.smbk-table--pin-first .smbk-table__cell:first-child {
z-index: var(--smbk-layer-raised);
background: var(--smbk-color-surface-canvas);
}
@media (hover: hover) {
.smbk-table--pin-first .smbk-table__row:hover .smbk-table__cell:first-child {
background: linear-gradient(var(--smbk-color-surface-hover), var(--smbk-color-surface-hover)), var(--smbk-color-surface-canvas);
}
}
.smbk-table--pin-first .smbk-table__row--selected .smbk-table__cell:first-child,
.smbk-table--pin-first .smbk-table__row--current .smbk-table__cell:first-child,
.smbk-table--pin-first .smbk-table__row--selected:hover .smbk-table__cell:first-child,
.smbk-table--pin-first .smbk-table__row--current:hover .smbk-table__cell:first-child {
background: var(--smbk-color-action-default-background-pressed);
}
.smbk-table--pin-first .smbk-table__row--highlighted .smbk-table__cell:first-child {
background: linear-gradient(var(--smbk-color-feedback-info-surface-subtle), var(--smbk-color-feedback-info-surface-subtle)), var(--smbk-color-surface-canvas);
}
/* A pinned row's brand edge, in a held cell: a strip of its own along the cell's start side, inside its edges, so its
row's rules cross it as everywhere else. The inset shadow the edge is in other tables, Firefox draws along a held
cell's foot as well, over its row's rule. */
.smbk-table--pin-first .smbk-table__row--pinned > .smbk-table__cell:first-child {
box-shadow: none;
}
.smbk-table--pin-first .smbk-table__row--pinned > .smbk-table__cell:first-child::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
inset-inline-start: 0;
width: var(--smbk-border-width-accent);
background: var(--smbk-color-border-brand);
pointer-events: none;
}
/* A group heading spans the table: its label sticks at the start while the columns scroll sideways. */
.smbk-table--pin-first .smbk-table__group-label {
position: sticky;
left: var(--smbk-space-inset-lg);
}
/* The pinned column's right edge, so the columns sliding under it read as passing behind. */
.smbk-table--pin-first .smbk-table__header:first-child::before,
.smbk-table--pin-first .smbk-table__cell:first-child::before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: var(--smbk-border-width-hairline);
background: var(--smbk-color-border-subtle);
}
/* In the long forms the heading's rule runs on under its edge, as under the hairlines between headings. */
:is(.smbk-table--sticky-header, .smbk-table--page-sticky-header).smbk-table--pin-first .smbk-table__header:first-child::before {
bottom: var(--smbk-border-width-hairline);
}
/* ---- the filled look: the framed table with a calmer heading ----
The frame, the rows, their height and their states stay as framed draws them. The heading row and the foot sit on
the neutral fill, with no hairlines between headings; a group row is sunken and one small control high; the room at
a row's ends and between its columns is the same. The fills are see-through, so a table takes them on any surface.
These rules come after the long and pinned forms, which this look takes, because they replace what those paint. */
.smbk-table--look-filled .smbk-table__header-row {
background: none;
}
.smbk-table--look-filled .smbk-table__header {
background: var(--smbk-color-surface-neutral-subtle);
}
.smbk-table--look-filled .smbk-table__header + .smbk-table__header {
border-left: 0;
}
.smbk-table--look-filled .smbk-table__header + .smbk-table__header::before {
content: none;
}
/* The fill marks the heading row and the foot, so the rule along each is the rows' own, not a stronger one. A heading
that sticks or moves carries its rule as a line of its own, so its border stays clear. */
.smbk-table--look-filled:not(.smbk-table--sticky-header, .smbk-table--page-sticky-header) .smbk-table__header {
border-bottom-color: var(--smbk-color-border-subtle);
}
.smbk-table--look-filled .smbk-table__header::after {
border-bottom-color: var(--smbk-color-border-subtle);
}
/* A collapsed table adds its rule's half to what a cell's content and padding take, so the padding stays well inside
the row's height and the edge is drawn inside it, at compact too. */
.smbk-table--look-filled .smbk-table__header {
padding-block: var(--smbk-space-inset-xs);
}
/* Half the room between columns on each side of a cell, and the row's own room at its two ends. Each side keeps some,
so a held first column's words stay clear of its edge. */
.smbk-table--look-filled :is(.smbk-table__header, .smbk-table__cell) {
padding-inline: calc(var(--smbk-space-inline-xl) / 2);
}
.smbk-table--look-filled :is(.smbk-table__header, .smbk-table__cell):first-child {
padding-inline-start: var(--smbk-space-inset-xl);
}
.smbk-table--look-filled :is(.smbk-table__header, .smbk-table__cell):last-child {
padding-inline-end: var(--smbk-space-inset-xl);
}
/* What runs across every column starts where the first column does. */
.smbk-table--look-filled .smbk-table__detail {
padding-inline: var(--smbk-space-inset-xl);
}
/* The fill is on the heading cell, not the row, so a group row keeps it under the rule that takes a row's hover fill
away. Its line sits centered in the small control's height, the edge inside it, and a label that wraps keeps that
room over and under. */
.smbk-table--look-filled .smbk-table__group {
height: var(--smbk-size-control-sm);
padding: calc((var(--smbk-size-control-sm) - 1lh) / 2 - var(--smbk-border-width-hairline)) var(--smbk-space-inset-xl);
background: var(--smbk-color-surface-sunken);
}
/* The foot is at least one small control high, and grows with what it holds: two paginations are taller, and on a
phone they wrap. So nothing in it runs past the frame, it stays where it is while a wide table scrolls sideways, its
fill under the whole of what shows; a foot scrolling with the table would leave the table's far end without one. */
.smbk-table--look-filled .smbk-table__footer {
position: sticky;
inset-inline: 0;
flex-wrap: wrap;
row-gap: var(--smbk-space-stack-xs);
min-height: var(--smbk-size-control-sm);
padding: var(--smbk-space-inset-2xs) var(--smbk-space-inset-xl);
border-top-color: var(--smbk-color-border-subtle);
background: var(--smbk-color-surface-neutral-subtle);
font-family: var(--smbk-typography-body-sm-font-family);
font-size: var(--smbk-typography-body-sm-font-size);
font-weight: var(--smbk-typography-body-sm-font-weight);
line-height: var(--smbk-typography-body-sm-line-height);
letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
}
/* The neutral fill is see-through, so what the rows pass under takes a floor beneath it: the page canvas, as a held
column's cells do, so a stuck heading looks as it does at rest on the canvas. */
.smbk-table--look-filled:is(.smbk-table--sticky-header, .smbk-table--page-sticky-header) :is(.smbk-table__header, .smbk-table__footer),
.smbk-table--look-filled:is(.smbk-table--sticky-header, .smbk-table--page-sticky-header)[data-scroll-hint] .smbk-table__scroll-hint,
.smbk-table--look-filled.smbk-table--pin-first .smbk-table__header:first-child {
background: linear-gradient(var(--smbk-color-surface-neutral-subtle), var(--smbk-color-surface-neutral-subtle)), var(--smbk-color-surface-canvas);
}
.smbk-table--look-filled.smbk-table--pin-first .smbk-table__group-label {
left: var(--smbk-space-inset-xl);
}
/* The arrow of a column not sorted waits for the reader: on a device that hovers, over its heading, and at keyboard
focus. Unseen, it keeps its room, so the heading does not move, and the button stays where Tab reaches it, with its
name. On a touch screen it shows once the column is sorted. */
.smbk-table--look-filled .smbk-table__header:not(.smbk-table__header--active) .smbk-table__sort > .smbk-table__header-icon {
opacity: 0;
}
.smbk-table--look-filled .smbk-table__header:not(.smbk-table__header--active) .smbk-table__sort:focus-visible > .smbk-table__header-icon {
opacity: 1;
}
@media (hover: hover) {
.smbk-table--look-filled .smbk-table__header:not(.smbk-table__header--active):hover .smbk-table__sort > .smbk-table__header-icon {
opacity: 1;
}
}
/* ---- the scroll hint: where the table runs past its frame and the browser hides its own scrollbar until
the frame scrolls (a phone, a trackpad), the script marks the frame and a thin bar along its foot shows
that there is more sideways and how much of it is in view (the wrapper sizes and moves the thumb as the
frame scrolls). It holds the frame's visible edges while the table scrolls under it, and its foot in a
long table, on the foot where there is one; in the page, it comes up to the window's foot with the foot.
Decoration: hidden from assistive technology, and it takes no press. ---- */
.smbk-table__scroll-hint {
display: none;
}
.smbk-table[data-scroll-hint] .smbk-table__scroll-hint {
position: sticky;
left: 0;
bottom: 0;
z-index: var(--smbk-layer-raised);
display: block;
padding: var(--smbk-space-inset-xs) var(--smbk-space-inset-sm);
pointer-events: none;
}
/* While the system's scrollbar shows over the frame (scrollbar.ts), in the same strip, the hint's bar fades out of
its way, so there is never a second bar. The board's hint does the same. */
.smbk-table[data-scroll-hint]:has(> .smbk-scrollbar[data-shown]) .smbk-table__scroll-track {
opacity: 0;
}
/* In a long table the rows scroll under the hint, so it takes the heading row's floor. */
:is(.smbk-table--sticky-header, .smbk-table--page-sticky-header)[data-scroll-hint] .smbk-table__scroll-hint {
background: var(--smbk-color-surface-default);
}
/* In a frame of its own the hint sits on the foot, which sticks to the frame's foot under it (the wrapper writes the
foot's height). */
.smbk-table--sticky-header[data-scroll-hint] .smbk-table__scroll-hint {
bottom: var(--smbk-table-foot-height, 0px);
}
.smbk-table__scroll-track {
position: relative;
transition: opacity var(--smbk-motion-feedback-duration) var(--smbk-motion-feedback-easing);
height: var(--smbk-size-4);
overflow: hidden;
border-radius: var(--smbk-radius-full);
background: var(--smbk-color-border-subtle);
}
.smbk-table__scroll-thumb {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: var(--smbk-radius-full);
background: var(--smbk-color-border-strong);
}
/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops fills and shadows; this puts the meaning back with the
user's own colors. Nothing here applies outside that mode. */
@media (forced-colors: active) {
/* Everything in the row, links and the components in its cells (a tag, an icon) included:
forced colors gives a part that names its own color the palette's text color, not the row's. */
.smbk-table__row--disabled,
.smbk-table__row--disabled * {
color: GrayText;
}
/* The brand edge is a shadow, which forced colors drops: a border in the user's text color takes its place. */
.smbk-table__row--pinned > .smbk-table__cell:first-child {
border-inline-start: var(--smbk-border-width-accent) solid CanvasText;
}
/* A floor drawn as a picture stays in this mode, and the user's text color may not read on it. */
.smbk-table--look-filled:is(.smbk-table--sticky-header, .smbk-table--page-sticky-header) :is(.smbk-table__header, .smbk-table__footer),
.smbk-table--look-filled:is(.smbk-table--sticky-header, .smbk-table--page-sticky-header)[data-scroll-hint] .smbk-table__scroll-hint,
.smbk-table--look-filled.smbk-table--pin-first .smbk-table__header:first-child {
background-image: none;
}
}
/* ---- touch: the small buttons and choose boxes take invisible press areas, each within its own row ----
On a touch screen an invisible area, centred on the sort, copy and caret buttons (the filter and info buttons and a
choose box take their own), takes the press out to the comfortable target where it falls short of it, but never
past its own row, so the rows above and below keep theirs. Inside a cell, a button or a choose box beside another
reaches only halfway into the gap between them. The lines a sticking heading draws for itself take
no press, so they never cut the edge off an area, and a pinned column keeps what the frame scrolls to out from under
it. What is drawn and every place stay as they are. */
@media (pointer: coarse) {
.smbk-table__sort,
.smbk-table__copy,
.smbk-table__expander {
position: relative;
}
.smbk-table__sort::after,
.smbk-table__copy::after,
.smbk-table__expander::after {
content: '';
position: absolute;
inset: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
}
/* Within its row: never taller than the row, a medium control. */
:is(.smbk-table__header, .smbk-table__cell) :is(.smbk-table__sort, .smbk-table__copy, .smbk-table__expander, .smbk-btn, .smbk-checkbox)::after {
inset-block: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (100% - var(--smbk-size-control-md)) / 2);
}
/* Within a flat row: one line of the body text and the small inset above and below it, or a control and that inset
where the control is taller than the line. In the heading every control is held to the line, and the copy and
caret buttons give back the room they take past it, so the line alone sets their row. */
.smbk-table--look-flat :is(.smbk-table__header, .smbk-table__cell) :is(.smbk-table__sort, .smbk-table__copy, .smbk-table__expander, .smbk-btn, .smbk-checkbox)::after {
inset-block: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), min(-1 * var(--smbk-space-inset-sm), (100% - var(--smbk-typography-body-sm-font-size) * var(--smbk-typography-body-sm-line-height)) / 2 - var(--smbk-space-inset-sm)));
}
.smbk-table--look-flat .smbk-table__header :is(.smbk-table__sort, .smbk-btn, .smbk-checkbox)::after,
.smbk-table--look-flat .smbk-table__cell :is(.smbk-table__copy, .smbk-table__expander)::after {
inset-block: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (100% - var(--smbk-typography-body-sm-font-size) * var(--smbk-typography-body-sm-line-height)) / 2 - var(--smbk-space-inset-sm));
}
/* A row grown to hold a value over its description is taller than the comfortable target in either density, so its
controls take the whole target and still stay inside it. */
.smbk-table__row:has(.smbk-table__stack) :is(.smbk-table__sort, .smbk-table__copy, .smbk-table__expander, .smbk-btn, .smbk-checkbox)::after {
inset-block: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
}
/* In a filled table, a control at the top of a row a prose cell holds has the prose's lines under it, so it takes the
whole target downward. How many lines the prose has no stylesheet can tell: on a row of one line it reaches a few
pixels past the row's foot, where the next row's own controls stay on top of it. */
.smbk-table--look-filled .smbk-table__row:has(> .smbk-table__cell--type-prose) :is(.smbk-table__copy, .smbk-table__expander, .smbk-btn, .smbk-checkbox)::after {
inset-block-end: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
}
/* In a row aligned to the top, a control on the first line has the row under it, however tall the row: it takes the
whole target downward and stops at the row's top. On a row of one line it reaches a few pixels past the row's foot,
as in a filled row a prose cell holds. */
.smbk-table--align-top:not(.smbk-table--look-flat) .smbk-table__row :is(.smbk-table__copy, .smbk-table__expander, .smbk-checkbox, .smbk-table__cell > .smbk-table__content > .smbk-btn)::after {
inset-block-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (100% - var(--smbk-size-control-md)) / 2);
inset-block-end: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
}
/* Beside each other in a cell: halfway into the cell's gap. A choose box beside words alone (a heading that does not
sort, a value with no link or button) keeps its whole area. */
.smbk-table__header-cell > .smbk-table__sort:has(~ :is(.smbk-btn, :has(.smbk-btn)))::after,
:is(.smbk-table__header-cell, .smbk-table__content) > .smbk-checkbox:has(+ :is(a, button), + * :is(a, button))::after,
.smbk-table__content > .smbk-btn:has(+ .smbk-btn)::after {
inset-inline-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-md) / -2);
}
.smbk-table__header-cell > .smbk-table__sort ~ .smbk-btn::after,
.smbk-table__header-cell > .smbk-table__sort ~ * .smbk-btn::after,
.smbk-table__header-cell > .smbk-checkbox ~ .smbk-table__sort::after,
.smbk-table__content > .smbk-btn + .smbk-btn::after {
inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-md) / -2);
}
/* A caret beside a row's choose box, which the wrapper draws first in the content after the lead: each halfway. */
.smbk-table__start > .smbk-table__lead:has(+ .smbk-checkbox) > .smbk-table__expander::after,
.smbk-table__start > .smbk-table__lead:has(+ .smbk-table__content > .smbk-checkbox:first-child) > .smbk-table__expander::after {
inset-inline-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (var(--smbk-space-inline-lg) + (var(--smbk-size-icon-sm) - var(--smbk-size-target-min)) / 2) / -2);
}
.smbk-table__start > .smbk-table__lead:has(> .smbk-table__expander) + .smbk-table__content > .smbk-checkbox:first-child::after {
inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (var(--smbk-space-inline-lg) + (var(--smbk-size-icon-sm) - var(--smbk-size-target-min)) / 2) / -2);
}
/* A pinned first column keeps what the frame scrolls into view out from under it, by its width, which the wrapper
writes; without the wrapper the frame scrolls as it always does. */
.smbk-table--pin-first {
scroll-padding-inline-start: var(--smbk-table-pinned-width, auto);
}
.smbk-table__header::before,
.smbk-table__header::after,
.smbk-table__cell::before,
.smbk-table__cell::after {
pointer-events: none;
}
}
/* ---- a tall heading: a column headed by a block ----
A column that asks for it holds a block in its heading, a few lines and buttons, and the cell grows to hold it. The
table lays the heading row out as any row, so the row is as high as its tallest heading and the line under it runs
under that one. A row with a tall heading starts every heading at the top, each first line where a one-line heading's
sits; the columns that did not ask keep their one line. The sort button cannot hold a block, which may hold buttons of
its own, so here it is the arrow alone, after the block, or before it in a column lined up at the end. */
.smbk-table__header--tall {
white-space: normal;
}
.smbk-table__header--tall .smbk-table__header-cell {
align-items: flex-start;
height: auto;
min-height: var(--smbk-size-mark-md);
}
.smbk-table__header-row:has(> .smbk-table__header--tall) > .smbk-table__header {
padding-block: calc((var(--smbk-size-control-md) - var(--smbk-size-mark-md)) / 2);
vertical-align: top;
}
.smbk-table--look-flat .smbk-table__header-row:has(> .smbk-table__header--tall) > .smbk-table__header {
padding-block: var(--smbk-space-inset-sm);
}
.smbk-table--look-flat .smbk-table__header--tall .smbk-table__header-cell {
min-height: calc(var(--smbk-typography-body-sm-font-size) * var(--smbk-typography-body-sm-line-height));
}
/* A column keeps a width its block can be read at while a table too wide for its frame narrows; a column with a width
of its own sets it. The block's lines stack, and it fills the heading, so a row of buttons in it can reach the
column's end. */
.smbk-table__header--tall .smbk-table__header-cell {
min-width: var(--smbk-size-width-240);
}
.smbk-table__table--columns-fixed .smbk-table__header--tall .smbk-table__header-cell {
min-width: 0;
}
.smbk-table__header-block {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: var(--smbk-space-stack-xs);
min-width: 0;
color: var(--smbk-color-text-default);
}
/* In a column lined up at the end, its lines and buttons go there too, the arrow close before them. */
.smbk-table__header--align-end .smbk-table__header-block {
flex-grow: 0;
align-items: flex-end;
}
/* The arrow alone: the least a press needs both ways, on the block's first line. */
.smbk-table__header--tall .smbk-table__sort {
flex-shrink: 0;
justify-content: center;
min-width: var(--smbk-size-target-min);
}
@media (hover: hover) {
.smbk-table__header--tall .smbk-table__sort:hover > .smbk-table__header-icon {
color: var(--smbk-color-text-default);
}
}
/* It is all there is to press, so in the filled look it does not wait for a reader to go to it. */
.smbk-table--look-filled .smbk-table__header--tall:not(.smbk-table__header--active) .smbk-table__sort > .smbk-table__header-icon {
opacity: 1;
}
/* A row's press areas stop at a medium control's height, which a tall heading has passed: its lines and the room at its
foot lie under its controls, so on a touch screen each takes the whole target downward. */
@media (pointer: coarse) {
.smbk-table .smbk-table__header--tall :is(.smbk-table__sort, .smbk-btn)::after {
inset-block-end: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
}
}
/* In the long forms, what is scrolled into view lands clear of the heading as high as it stands: the wrapper writes its
height when a heading can be more than one line, a tall heading or a long one in a column with a width. */
.smbk-table--sticky-header {
scroll-padding-block-start: var(--smbk-table-head-height, var(--smbk-size-control-md));
}
.smbk-table--page-sticky-header .smbk-table__table > tbody * {
scroll-margin-block-start: var(--smbk-table-head-height, var(--smbk-size-control-md));
}
/* A heading that would take more than half of what shows would cover the rows it heads, so the wrapper marks the frame
and the heading stays in its place at the table's top, scrolling away with the rows, and leaves them all the room. A
held first column stays held sideways. In the page the script writes its move on the cells themselves, which only an
important rule stands above. */
.smbk-table--sticky-header[data-heading-scrolls] {
scroll-padding-block-start: 0;
}
.smbk-table--sticky-header[data-heading-scrolls] .smbk-table__header {
top: auto;
}
.smbk-table--page-sticky-header[data-heading-scrolls] .smbk-table__header {
animation-name: none;
translate: none !important;
}
.smbk-table--page-sticky-header[data-heading-scrolls] .smbk-table__table > tbody * {
scroll-margin-block-start: 0;
}
/* ---- a row that can be pressed as a whole ----
Off unless a row is given a link or an action. The row stays a plain table row: one real link or button of its own,
with no words to show and a name for screen readers, lies over the whole row, so a press anywhere on it is a press
on that link or button, and a link opens in a new tab from anywhere on the row as any link does. Whatever a person
can press in a cell lies over that link or button in turn, so a press there does its own job and never reaches the
row's. It lies over what a cell only shows, a bar or a picture included, so no part of a row takes a press and does
nothing. A held first column is a layer that nothing outside it can reach into: there the row's link or button is
the first thing in the second cell, and the held cell holds an unnamed copy of it that Tab and screen readers leave
out. A control passing under the held column stays out of reach, as in every held table. */
.smbk-table:has(.smbk-table__row--pressable) {
isolation: isolate;
}
.smbk-table__row--pressable {
position: relative;
}
.smbk-table__row-press {
position: absolute;
inset: 0;
z-index: 1;
margin: 0;
padding: 0;
border: 0;
background: none;
appearance: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
/* No weight, so a control's own place in its stylesheet stands; the row's link or button, a class, keeps its own. */
:where(.smbk-table__row--pressable > .smbk-table__cell) :where(a[href], button, input, select, textarea, label, summary, [tabindex], [contenteditable], [aria-disabled='true']) {
position: relative;
z-index: 2;
}
/* A control that cannot be pressed now keeps the press and does nothing with it. Left to take no pointer, as it does
elsewhere, it would pass the press down to the row. Its disabled look holds under the pointer. */
.smbk-table__row--pressable > .smbk-table__cell :is(:disabled, [aria-disabled='true']) {
pointer-events: auto;
cursor: not-allowed;
}
/* The ring runs round the row, over a held cell too, and lies inside it, since the frame would clip it. The link or
button sits under what a held cell paints, so the cell holding it draws the ring, over the whole row as the link or
button lies. The row itself cannot: anything it draws, placed absolutely or not, gets a table cell of its own from
the browser, which is one more column, and in a table with column widths takes room from the columns. A held column
alone holds the link or button itself, where the ring stands in for a pinned row's edge while it shows. */
.smbk-table__row-press:focus-visible {
outline: 0;
}
.smbk-table__row--pressable > .smbk-table__cell:has(> .smbk-table__row-press:focus-visible)::after {
content: '';
position: absolute;
inset: 0;
z-index: var(--smbk-layer-raised);
width: auto;
background: none;
outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
outline-offset: calc(-1 * var(--smbk-focus-ring-width));
pointer-events: none;
}
/* While it is pressed it takes the fill it takes while what it opens is open (the current row). On a touch screen, with
no hover, this is the sign that the press landed. */
.smbk-table__row--pressable:has(> .smbk-table__cell > .smbk-table__row-press:active),
.smbk-table--pin-first .smbk-table__row--pressable:has(> .smbk-table__cell > .smbk-table__row-press:active) .smbk-table__cell:first-child {
background: var(--smbk-color-action-default-background-pressed);
}| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| border-width.hairline--smbk-border-width-hairline | 1px | Border width | |
| color.border.subtle--smbk-color-border-subtle | light: #c4cdd8dark: #2d3237changes with theme | Color | |
| radius.container.sm--smbk-radius-container-sm | 8px | Radius | |
| color.text.default--smbk-color-text-default | light: #232527dark: #e7ebf0changes with theme | Color | |
| color.surface.default--smbk-color-surface-default | light: #f3f5f8dark: #181a1cchanges with theme | Color | |
| size.control.md--smbk-size-control-md | comfortable: 40pxcompact: 32pxchanges with density | Size | |
| space.inset.md--smbk-space-inset-md | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| space.inset.lg--smbk-space-inset-lg | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| color.border.control--smbk-color-border-control | #7c8692 | Color | |
| color.text.subtle--smbk-color-text-subtle | light: #4e555edark: #9ba5b1changes with theme | Color | |
| typography.label-sm--smbk-typography-label-sm-font-family--smbk-typography-label-sm-font-size--smbk-typography-label-sm-font-weight--smbk-typography-label-sm-line-height--smbk-typography-label-sm-letter-spacing--smbk-typography-label-sm-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 12pxfontWeight: 500lineHeight: 1.4letterSpacing: 0px | The quick brown fox | Typography |
| space.inline.md--smbk-space-inline-md | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| size.mark.md--smbk-size-mark-md | 20px | Size | |
| space.inline.xs--smbk-space-inline-xs | 4px | Spacing | |
| size.icon.xs--smbk-size-icon-xs | 12px | Size | |
| size.target.min--smbk-size-target-min | 24px | Size | |
| size.control.xs--smbk-size-control-xs | 24px | Size | |
| focus.ring.width--smbk-focus-ring-width | 2px | Focus | |
| color.border.focus--smbk-color-border-focus | light: #cc0d0ddark: #f34336changes with theme | Color | |
| focus.ring.offset--smbk-focus-ring-offset | 2px | Focus | |
| radius.control.xs--smbk-radius-control-xs | 2px | Radius | |
| color.surface.hover--smbk-color-surface-hover | light: #373d4326dark: #606a754dchanges with theme | Color | |
| color.action.default.background-pressed--smbk-color-action-default-background-pressed | light: #c4cdd8dark: #4e555echanges with theme | Color | |
| color.feedback.info.surface-subtle--smbk-color-feedback-info-surface-subtle | light: #e3ebfedark: #162352changes with theme | Color | |
| border-width.accent--smbk-border-width-accent | 3px | Border width | |
| color.border.brand--smbk-color-border-brand | #cc0d0d | Color | |
| color.border.strong--smbk-color-border-strong | light: #9ba5b1dark: #373d43changes with theme | Color | |
| space.inset.sm--smbk-space-inset-sm | comfortable: 6pxcompact: 4pxchanges with density | Spacing | |
| typography.label-md--smbk-typography-label-md-font-family--smbk-typography-label-md-font-size--smbk-typography-label-md-font-weight--smbk-typography-label-md-line-height--smbk-typography-label-md-letter-spacing--smbk-typography-label-md-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 14pxfontWeight: 500lineHeight: 1.4letterSpacing: 0px | The quick brown fox | Typography |
| space.inline.sm--smbk-space-inline-sm | comfortable: 6pxcompact: 4pxchanges with density | Spacing | |
| color.surface.sunken--smbk-color-surface-sunken | light: #373d430ddark: #00000066changes with theme | Color | |
| typography.body-sm--smbk-typography-body-sm-font-family--smbk-typography-body-sm-font-size--smbk-typography-body-sm-font-weight--smbk-typography-body-sm-line-height--smbk-typography-body-sm-letter-spacing--smbk-typography-body-sm-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 14pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| color.text.disabled--smbk-color-text-disabled | light: #7c8692dark: #606a75changes with theme | Color | |
| opacity.disabled--smbk-opacity-disabled | 0.5 | Opacity | |
| size.control.sm--smbk-size-control-sm | comfortable: 32pxcompact: 28pxchanges with density | Size | |
| size.table.prose-min--smbk-size-table-prose-min | 240px | Size | |
| size.table.prose-max--smbk-size-table-prose-max | 480px | Size | |
| size.icon.sm--smbk-size-icon-sm | 16px | Size | |
| space.inline.lg--smbk-space-inline-lg | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| size.selection.sm--smbk-size-selection-sm | 16px | Size | |
| space.text.label--smbk-space-text-label | 0px | Spacing | |
| typography.body-xs--smbk-typography-body-xs-font-family--smbk-typography-body-xs-font-size--smbk-typography-body-xs-font-weight--smbk-typography-body-xs-line-height--smbk-typography-body-xs-letter-spacing--smbk-typography-body-xs-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 12pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| color.text.subtlest--smbk-color-text-subtlest | light: #606a75dark: #7c8692changes with theme | Color | |
| color.text.brand--smbk-color-text-brand | light: #cc0d0ddark: #f34336changes with theme | Color | |
| size.80--smbk-size-80 | 80px | Size | |
| size.control.lg--smbk-size-control-lg | comfortable: 48pxcompact: 40pxchanges with density | Size | |
| space.inset.xs--smbk-space-inset-xs | 4px | Spacing | |
| size.width.480--smbk-size-width-480 | 480px | Size | |
| size.40--smbk-size-40 | 40px | Size | |
| size.48--smbk-size-48 | 48px | Size | |
| size.56--smbk-size-56 | 56px | Size | |
| size.64--smbk-size-64 | 64px | Size | |
| size.72--smbk-size-72 | 72px | Size | |
| size.88--smbk-size-88 | 88px | Size | |
| size.96--smbk-size-96 | 96px | Size | |
| size.width.240--smbk-size-width-240 | 240px | Size | |
| size.width.320--smbk-size-width-320 | 320px | Size | |
| size.width.400--smbk-size-width-400 | 400px | Size | |
| size.width.640--smbk-size-width-640 | 640px | Size | |
| size.width.720--smbk-size-width-720 | 720px | Size | |
| size.width.960--smbk-size-width-960 | 960px | Size | |
| layer.raised--smbk-layer-raised | 1000 | Stacking order | |
| layer.sticky--smbk-layer-sticky | 1100 | Stacking order | |
| color.surface.canvas--smbk-color-surface-canvas | light: #e7ebf0dark: #111213changes with theme | Color | |
| color.surface.neutral-subtle--smbk-color-surface-neutral-subtle | light: #373d431adark: #606a7533changes with theme | Color | |
| space.inline.xl--smbk-space-inline-xl | comfortable: 16pxcompact: 12pxchanges with density | Spacing | |
| space.inset.xl--smbk-space-inset-xl | comfortable: 16pxcompact: 12pxchanges with density | Spacing | |
| space.stack.xs--smbk-space-stack-xs | 4px | Spacing | |
| space.inset.2xs--smbk-space-inset-2xs | 2px | Spacing | |
| motion.feedback.duration--smbk-motion-feedback-duration | 150ms | Motion | |
| motion.feedback.easing--smbk-motion-feedback-easing | cubic-bezier(0.16, 1, 0.3, 1) | Motion | |
| size.4--smbk-size-4 | 4px | Size | |
| radius.full--smbk-radius-full | 9999px | Radius | |
| size.target.comfortable--smbk-size-target-comfortable | 44px | Size |
{
"name": "table",
"block": "smbk-table",
"summary": "Rows of data under headings that sort, filter and explain, with rows that open, pin or group, optional selection and a foot for the pagination.",
"adapt": "css",
"axes": {
"look": {
"values": [
"framed",
"flat",
"filled"
],
"default": "framed",
"description": "Framed: a bordered, rounded frame, the heading row on its own surface, room at the sides. Flat, for a small table inside a card: no frame, no heading surface, a rule over every row, no room at the sides, so the table's words line up with the card's other lines, and rows one line of text high. A flat table takes none of sticky-header, page-sticky-header and pin-first. Filled, for a calmer table on the page: the framed frame and rows, the heading row and the foot on a neutral fill, group rows sunken and low, no lines between headings, the same room at a row's ends and between its columns, and the arrow of a column not sorted only on hover and keyboard focus."
}
},
"flags": {
"sticky-header": {
"description": "A long table in a frame of its own: the heading row stays at the frame's top and the foot at its foot while the rows scroll inside it. The consumer limits the frame's height; the frame is then a named region the keyboard can reach. Framed and filled looks."
},
"page-sticky-header": {
"description": "A long table in the page: the frame keeps no height of its own, and the heading row stays in view under the room the page keeps clear at its top while the page scrolls, with the foot at the foot of the window, until the table ends. The table still scrolls sideways, its heading with it. Framed and filled looks."
},
"pin-first": {
"description": "A wide table: the first column stays in view while the table scrolls sideways, so every row keeps its subject. For a table on the page canvas, in the framed and filled looks."
},
"align-top": {
"description": "Cells of very different heights: every cell starts at the top of its row, and its first line sits level with the first line of the others, so the eye reads straight across a tall row. All three looks."
}
},
"parts": [
"table",
"caption",
"column",
"header-row",
"header",
"header-cell",
"header-text",
"header-label",
"header-block",
"header-icon",
"sort",
"filter",
"filter-count",
"info",
"row",
"row-press",
"cell",
"start",
"lead",
"expander",
"pin",
"content",
"choose-space",
"value",
"value-icon",
"stack",
"description",
"copy",
"prose",
"figure",
"bar",
"group",
"group-label",
"group-count",
"detail-row",
"detail",
"detail-title",
"footer",
"scroll-hint",
"scroll-track",
"scroll-thumb"
],
"figmaNodeId": "4318:16988"
}