Skip to content
SproutAceve · v3.33.1
Overview What is your role? Connect to Sprout MCP reference Adopting Sprout
Brand & logos Color Typography Spacing & layout Elevation Iconography Accessibility
Overview Alert Avatar Badge Breadcrumb Buttons Cards Checkbox Data grid Dialog Dropdown button Fab button Floating tabs Inputs Menu Navigation Numeric input Progress bar Progress ring Radio button Ribbon toolbar Switch Tabs Tag Tooltip
Interaction & motion Voice & content Product patterns
AI patterns AI transparency & disclosure AI disclosure label AI labels in marketing
Design principles Personas Trust and ethics Meet the makers
Build the systemplay Tower of tokensplay Site runnerplay
How it works Changelogv3.33 Roadmap Contribute
Components

Data grid.

The centrepiece of every estimate screen, and the one component in Sprout where focus and selection have to coexist without confusing each other. A grid is not a table: every cell in it can be the one the keyboard is standing on, and any number of rows can be marked for a bulk action at the same time.

Light
Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40
m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm
890
kg
€ 2.140,00
3
01.03
Wall formwork
45
m²
€ 1.050,00
4
02.01
Sand-lime brickwork
148
m²
€ 6.290,00
5
02.02
Facade pointing
148
m²
€ 1.480,00
Dark
Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40
m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm
890
kg
€ 2.140,00
3
01.03
Wall formwork
45
m²
€ 1.050,00
4
02.01
Sand-lime brickwork
148
m²
€ 6.290,00
5
02.02
Facade pointing
148
m²
€ 1.480,00

Row 4 is selected, and the Qty cell on row 2 is active. Two tokens, and one channel each: containers/primary draws the border that means focus, states/primary/selected fills the row that means selection. The active cell has no fill of its own, so it shows whatever its row shows, and the two states stay readable when they land on top of each other.

Anatomy

Six parts. The gutter and the header frame the data, the cell is where the work happens, and the last two are state rather than structure: they mark where you are and what you have picked.

PartSpecNotes
1Row gutter
38px wide
header fill
supportive text
Row numbers, right aligned and tabular. It is the target for row selection and the anchor for the selected row rule, so it never scrolls away horizontally.
2Header
Manrope Bold 13
surface variant fill
on surface
Sticky at the top of a scrolling grid. Never a sort control unless sorting is actually implemented; a header that looks clickable and is not is worse than a plain one.
3Number column
right aligned
tabular figures
13 / 18
Quantities, prices and totals. Right alignment is what lines up the decimal comma; tabular figures line up the interior. Both, not one.
4Cell
44px row
12px side padding
1px hairline
One value, one hairline on the bottom and right, in layers/table line. Not outline, which is the border of an input field and makes the grid read as a form.
5Active cell
2px primary border
no fill
Exactly one per grid, ever. It is the keyboard caret, so it survives scrolling and it is what every key in the map below acts on.
6Selected row
2px primary left rule
on the gutter
Many per grid. Selection is a different question from focus, which is why it uses a different edge and a different hue: a rule on the left, not a border round a cell.

Focus and selection have to stay visually separate, because they answer different questions: where am I typing and what will the toolbar act on. One cell is active, any number of rows can be selected, and both can be true of the same row at once. They share the hue and that costs nothing, because they no longer share a channel: focus is a border, selection is a fill, and each variable gets one channel to itself. That is the split Excel, Sheets and AG Grid all use.

Density

Two row heights, and the choice is about how many rows fit, not how the grid looks.

Default · 44px
Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40
m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm
890
kg
€ 2.140,00
3
01.03
Wall formwork
45
m²
€ 1.050,00
4
02.01
Sand-lime brickwork
148
m²
€ 6.290,00
5
02.02
Facade pointing
148
m²
€ 1.480,00
Dense · 24px
Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40
m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm
890
kg
€ 2.140,00
3
01.03
Wall formwork
45
m²
€ 1.050,00
4
02.01
Sand-lime brickwork
148
m²
€ 6.290,00
5
02.02
Facade pointing
148
m²
€ 1.480,00
DensityRowWhere it belongs
Default44pxEvery grid, unless someone asked for the other one. Matches sizing/button-height, so an inline action in a cell fills the row exactly.
Dense24pxLong estimates and comparison views, on a pointer only. It is half the 48px a touch target needs, so it must never reach a phone or a tablet.

Dense changes the row and the cell padding, and nothing else. The value stays 13px, the header stays 13px Bold, and the hairline stays 1px. Shrinking the type as well is the tempting next step and the wrong one: at 24px rows the grid is already asking a lot of the eye, and smaller figures are what turns a fast scan into a misread quantity.

Keyboard

Forty lines of an estimate without touching the mouse. Every key below is a contract.

← ↑ → ↓Move the active cell. Stops at the edges.
TabNext cell, then the next row. Shift reverses.
EnterCommit and move down, so a column fills without leaving the keyboard.
EscRevert to the last committed value. Focus stays put.
any figureReplaces the value outright. It does not append.
F2Edit in place instead, caret at the end.
SpaceSelect the active cell's row. The active cell does not move.
CtrlEnterFill the value down the selection.

Esc reverts is the one with a build consequence: a cell has to hold its last committed value separately from what is on screen. That is the same displayed-against-stored split the numeric input page sets out.

No shortcut deletes a row. Destructive actions belong on a button someone has to look at.

States

Everything the grid can be doing, in one demo. The detail is underneath, a block per state.

Try meswitch the shape, flip the mode, and hover any row
Mode
Selection
Options
Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40
m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm
890
kg
€ 2.140,00
3
01.03
Wall formwork
45
m²
€ 1.050,00
4
02.01
Sand-lime brickwork
148
m²
€ 6.290,00
5
02.02
Facade pointing
148
m²
€ 1.480,00

One cell — four sides. A selection of one. Same rule as the rest, which is why the active cell needs no special case.

Surfaces and lines The grid at rest

L / DWhat it coloursToken
Cell fill
layers/surface
neutral/0 · neutral/900
Header and gutter
layers/surface variant
neutral/100 · neutral/800
Cell hairline
layers/table line (new)
neutral/150 · neutral/750
Value and header text
layers/on surface
neutral/900 · neutral/50
Row numbers
layers/supportive text
neutral/650 · neutral/200
Row hover
states/surface/hovered
neutral/50 · neutral/850

Hover fills the row, not the cell, because it answers which row am I about to hit. The gutter keeps the header fill so the numbers stay a column.

The hairline is table line, not outline. outline is the border of an input field, and a grid drawn in it outlines every cell like an empty field waiting to be filled.

Active cell The keyboard caret

One per grid. Border on four sides and no fill, so it stays legible on a plain row, a hovered row and a selected row alike.

L / DWhat it coloursToken
Border, 2px inset
containers/primary
primary/700 · primary/300
Cell fill
none, inherits the row
—

No fill is deliberate. Excel, Sheets and AG Grid all leave the active cell unfilled: a fill would compete with selection for the same channel. Border means focus, fill means selection.

Selection One fill, four shapes

Same fill whatever the shape. What changes is how much of the border gets drawn.

L / DWhat it coloursToken
Fill, every selected cell
states/primary/selected
primary/100 · primary/900
Border, 2px
containers/primary
primary/700 · primary/300
ShapeBorderWhy
One cellfour sidesA selection of one. Same rule as the rest, which is why the active cell needs no special case.
Rangeclosed perimeterBounded, and you act on it now, so the shape closes.
Rowleft edge onlyRuns off the side of the viewport. There is no right edge on screen to close, and the left one is always in view.
Columntop edge onlyThe same, turned ninety degrees. The edge goes on the header.

Focus and selection share the hue and that is fine, because they no longer share a channel. A row can be selected and hold the active cell at once, which is the normal state rather than an edge case.

The fill is faint by design, so it tints a row without competing with the figures on it. The border is the part that survives a glance.

Error cell Per cell, on commit

No room for a message under a 44px row, so the border carries it and the sentence goes in a summary above the grid.

L / DWhat it coloursToken
Border, 2px inset
containers/danger
danger/600 · danger/400

The cell still needs an accessible description carrying the same sentence. A red border alone says a value is wrong without saying why.

Error and active can both be true. Error takes the border, the selection fill stays.

Read only Reachable, not editable

A calculated total, or a line locked after approval. It takes focus and shows the active border, then swallows the keystroke.

L / DWhat it coloursToken
Cell fill
containers/neutral container
neutral/50 · neutral/850
Value
states/disabled
neutral/350 · neutral/650

Skipping these in the Tab order looks tidy and breaks copying a column. The one place a low ratio is correct: 1.4.3 exempts an inactive control.

It is neutral container, not surface variant, because the header took that one and a read only cell matching the header reads as a second header.

Figures and locale

A grid is a column of figures read against each other, so everything the numeric input page says about separators applies here with more force. One misplaced decimal in a column of forty is the failure mode this section exists to prevent.

RuleValueWhy
FigurestabularRight alignment lines up the decimal comma; tabular figures line up everything to the left of it. A grid is the one place both are worth having.
TypefaceManropeThe same face as the rest of the product, with font-variant-numeric: tabular-nums. Sprout does not need a separate typeface for figures.
DecimalcommaEvery locale Sprout ships to uses a comma. Accept a full stop on input and normalise it, because a numeric keypad puts a full stop under the thumb.
Groupinglocale, non-breakingA full stop in nl and de, a non-breaking space in sv, da and no. Whichever it is, it must not be a plain space, or a total wraps mid-figure.
Currency2 decimals, alwaysIncluding on a round number. Ragged decimals in a price column are what make a column impossible to scan.
Emptyblank, not zeroA blank quantity means not entered. Rendering it as 0 destroys the difference between nothing ordered and nothing recorded, and it poisons the total.

One number formatter for the whole grid, driven by the user's locale, and never a value formatted in the markup. The demo above is nl-BE: € 4.218,00 and 12,40. The same rows in sv-SE would read 4 218,00 kr and 12,40. Both are correct; a grid that hard codes either is not.

Do & don't

Four decisions that come up every time someone builds one.

Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40
m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm
890
kg
€ 2.140,00
3
01.03
Wall formwork
45
m²
€ 1.050,00
Do — one border, no fill. Row 2 is selected and holds the active cell. The cell stays unfilled, so you can see both at once.
Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40
m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm
890
kg
€ 2.140,00
3
01.03
Wall formwork
45
m²
€ 1.050,00
Don't — fill it as well. Same row, same cell, one fill added. Now the whole row is one block and focus is lost in it.
Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40
m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm
890
kg
€ 2.140,00
3
01.03
Wall formwork
45
m²
€ 1.050,00
Do — right align figures, unit in its own column. The decimal comma lands in the same place down the column.
Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40 m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm
890 kg
€ 2.140,00
3
01.03
Wall formwork
45 m²
€ 1.050,00
Don't — put the unit inside the figure. The suffix sits between the digits and the edge, so nothing lines up.
Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40
m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm, galvanised, cut to length on site
890
kg
€ 2.140,00
3
01.03
Wall formwork
45
m²
€ 1.050,00
Do — truncate the description. Fix every other column and give Description the slack. Full value on focus.
Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40
m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm, galvanised, cut to length on site
890
kg
€ 2.140,00
3
01.03
Wall formwork
45
m²
€ 1.050,00
Don't — let a cell wrap. One long line changes the height of its row and breaks the rhythm.
Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40
m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm
0,
kg
€ 2.140,00
3
01.03
Wall formwork
45
m²
€ 1.050,00
Do — validate on commit. Typing 0,5 passes through 0, and 0 is not an error yet.
Code
Description
Qty
Unit
Total
1
01.01
Reinforced concrete C30/37
12,40
m³
€ 4.218,00
2
01.02
Reinforcement bar Ø12 mm
0,
kg
€ 2.140,00
3
01.03
Wall formwork
45
m²
€ 1.050,00
Don't — flag mid-entry. An error on an unfinished value makes the grid feel broken.
← PreviousCheckboxNext →Dialog
Sprout · Aceve Design System · v3.33.1
Figma library GitLabReleases