Theming Cascade & overrides
Cascade & overrides
How Pretable's CSS layers work and how to override any part with plain CSS.
All of Pretable's grid styling lives in a single cascade layer named pretable, and every default selector is wrapped in :where() so it carries zero specificity. Together these mean your styles win — by layer order, by specificity, or both — without !important or specificity tricks.
Plain selectors recolor the selected cell and the header and the resize handle with no !important, and a computed-style readout proves an attempted resize-handle width override does nothing because the surface writes width inline.
Every override above is a plain selector with no !important. The last line of the caption is the exception worth reading closely: it's proof, not a claim, that a width on the resize handle does nothing — see What layering does not get you: inline layout below.
Three ways to override, in increasing power
-
Tokens — set any
--pretable-*variable at:rootor a scoped selector. This is the blessed path for recoloring and resizing. Override after importing the theme file. See Override tokens. -
Deep CSS — write a selector targeting any grid part. Because the defaults are specificity
(0,0,0), even a single class wins:css.my-grid [data-pretable-cell][data-pretable-selected="true"] { background: hotpink; } -
Layer order — because all Pretable CSS is in
@layer pretable, anything unlayered or in a later layer wins regardless of specificity.
Declare the layer order (Tailwind / reset users)
If you use cascade layers — including Tailwind v4, which layers Preflight in base — declare the order once so the cascade is predictable:
@layer theme, base, pretable, components, utilities;pretableis afterbase, so a reset (Tailwind Preflight,normalize.cssinbase) cannot clobber the grid's borders and padding.pretableis beforeutilities, so a utility class likeclass="bg-red-500"on a cell still wins.
If you don't use layers at all, you need none of this — plain unlayered CSS beats every layer automatically.
Put resets in
@layer base. Anormalize.cssimported unlayered would beat the grid (unlayered beats all layers) and could strip its styling.
Worked examples
The three overrides in the demo above are the worked examples: recolor the selected cell, restyle the header, recolor the resize handle. All three are plain selectors with no !important — they win because the grid's defaults are layered and specificity-(0,0,0). deep-override.css in the demo's Code tab is exactly what you'd write; the only thing scoped for this page is the .cascade-demo prefix (see the file's own comment for why).
What layering does not get you: inline layout
Layering settles skin-versus-skin. It does not help against the grid's own inline styles, which beat every stylesheet regardless of layer.
The surface writes geometry inline — position, top, left, width,
height, z-index — because those values are computed per render from the
column plan and the scroll offset. A virtualized cell's left is not a design
decision you can restyle; it is where that column currently is.
So width on the resize strip is not overridable. The demo above proves it rather than asserting it: deep-override.css also asks the resize handle for width: 8px, and the caption beneath the grid reads the handle's actual computed width straight out of the DOM — 4px, the value the surface wrote inline, not the 8px the stylesheet asked for.
!important does win, but the strip is anchored to its column's trailing edge
and grows outward from it, so a thicker one overhangs the next column by the
extra width. Prefer restyling what is not inline — background, cursor on
hover, the --pretable-resize-handle* tokens.
The rule of thumb: anything that positions or sizes a box is inline and
off-limits; type and color are yours. Fonts, background, padding,
opacity and transition are all left to CSS.
Two things sit outside that rule and are inline today, so plan around them:
| Inline, despite not being layout | On | Why |
|---|---|---|
outline-* | body cells | The focus ring is driven by focus state the surface already tracks per cell |
overflow-wrap, white-space | body cells | Follows column.wrap, which is a data-model setting rather than a theme |
Header cells also carry inline text-align and gap, but those are the header's
flex layout rather than skin. Their color and divider are not inline — both
follow --pretable-text-header and --pretable-rule.
Targeting attributes
Pretable emits a stable set of data-pretable-* attributes you can target with CSS:
| Element | Attributes |
|---|---|
Root [data-pretable-scroll-viewport] | data-pretable-hydrated ("false" in server-rendered HTML, "true" once React has hydrated — see Knowing when a server-rendered grid is interactive) |
Row [data-pretable-row] | data-pretable-row-id, data-pretable-row-index, data-pretable-selected, data-pretable-focused |
Body cell [data-pretable-cell] | data-pretable-column-id, data-pretable-selected, data-pretable-focused, data-pretable-pinned ("left" | "right"), data-pretable-wrap, data-pretable-column-type (the column's type; absent when the column declares none), data-pretable-column-align ("start" | "center" | "end"; absent for the default start alignment — see below) |
Header cell [data-pretable-header-cell] | data-pretable-column-id (user-defined columns; the row-select column's header omits this), data-pretable-pinned ("left" | "right"), data-pretable-column-type and data-pretable-column-align (same values as the body cell, so a column's header and its cells align together) |
Group-row cell [data-pretable-cell] inside [data-pretable-group-row] | data-pretable-column-id, data-pretable-focused, data-pretable-pinned, plus data-pretable-column-type and data-pretable-column-align on the aggregate cells, so aggregates line up with the data beneath them. The cell holding the twisty and label also carries data-pretable-group-cell and is excluded from the alignment rules — it owns its own indent |
Row-select column [data-pretable-row-select-cell] / [data-pretable-row-select-header] | The synthetic checkbox column. It matches the generic cell and header-cell selectors too, and is the first match for both — exclude it with :not([data-pretable-row-select-cell]), not with [data-pretable-column-id] (its cells carry one; only its header does not) |
Header overlays [data-pretable-header-overlays] | data-pretable-column-id. A zero-width box anchored on the column's trailing edge; the resize strip and filter funnel hang off it, so it is the element to target to move both together |
Resize strip [data-pretable-resize-handle] | data-pretable-column-id, data-pretable-dragging (always present; "true" while a resize is in progress, else "false") |
Filter funnel [data-pretable-filter-funnel] | data-pretable-column-id, data-pretable-filter-active (always present; "true" when that column has a filter, else "false"). Its wrapper is [data-pretable-filter-funnel-slot] |
data-pretable-column-align is written only when a column resolves to something
other than the default: "end" for type: "number" columns, or whatever the
column sets on align explicitly. Start-aligned columns — the overwhelming
majority — carry no attribute at all, so target the default by absence.
A [data-pretable-column-align="start"] rule matches only the columns that
opted into align: "start" by hand, and silently misses every column that is
start-aligned because nothing said otherwise.
For example, style one column (header and body), the selected cells in another, and the start-aligned cells by absence:
[data-pretable-column-id="revenue"] {
font-variant-numeric: tabular-nums;
}
[data-pretable-cell][data-pretable-selected="true"] {
background: #1d4ed8;
color: white;
}
/* Every start-aligned column — not [data-pretable-column-align="start"] */
[data-pretable-cell]:not([data-pretable-column-align]) {
padding-inline-start: 12px;
}These names are stable; aria-* and role are also targetable but follow ARIA semantics, not Pretable's namespace.