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.

Deep CSS override

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.

.md

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

  1. Tokens — set any --pretable-* variable at :root or a scoped selector. This is the blessed path for recoloring and resizing. Override after importing the theme file. See Override tokens.

  2. 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;
    }
  3. 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:

css
@layer theme, base, pretable, components, utilities;
  • pretable is after base, so a reset (Tailwind Preflight, normalize.css in base) cannot clobber the grid's borders and padding.
  • pretable is before utilities, so a utility class like class="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. A normalize.css imported 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 layoutOnWhy
outline-*body cellsThe focus ring is driven by focus state the surface already tracks per cell
overflow-wrap, white-spacebody cellsFollows 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:

ElementAttributes
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:

css
[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.