Skip to content

Add getCellStyle escape hatch for custom per-cell styling - #8

Merged
andrewlu0 merged 2 commits into
extend-hq:mainfrom
tothienbao6a0:feat/get-cell-style
Jun 18, 2026
Merged

Add getCellStyle escape hatch for custom per-cell styling#8
andrewlu0 merged 2 commits into
extend-hq:mainfrom
tothienbao6a0:feat/get-cell-style

Conversation

@tothienbao6a0

Copy link
Copy Markdown
Contributor

Summary

Adds an optional getCellStyle prop to XlsxViewer (and XlsxViewerProps) — a generic escape hatch for styling individual cells without forking the workbook data. It follows the same philosophy as the existing render-prop / color-override props (renderImage, renderTableHeaderMenu, selectionColor, etc.).

The viewer calls getCellStyle for every rendered cell and merges the returned partial React.CSSProperties on top of the cell's resolved style. Returning undefined/null leaves a cell untouched.

const getCellStyle = React.useCallback<NonNullable<XlsxViewerProps["getCellStyle"]>>(
  ({ cell, isTableHeader }) =>
    !isTableHeader && highlighted.has(`${cell.row}:${cell.col}`)
      ? { backgroundColor: "rgba(37, 99, 235, 0.12)", outline: "1px solid #2563eb" }
      : undefined,
  [highlighted]
);

<XlsxViewer file={buffer} getCellStyle={getCellStyle} />;

Why

Integrations often need to decorate specific cells (highlights, outlines, status tints) on top of the workbook's own formatting. Today that requires mutating workbook data or forking the renderer. getCellStyle provides a small, unopinionated hook for it.

Implementation

  • Applied at the single getCellData chokepoint that feeds both the DOM and canvas renderers, so styling works in either mode and the canvas style cache is built from the final merged style.
  • Wired into the cell render cache invalidation and getCellData dependencies, so changing the callback identity re-resolves and repaints cells. The DOM row memo already re-renders on getCellData identity change.
  • The callback receives an XlsxCellStyleContext: cell, workbookSheetIndex, sheetName, resolvedStyle, value, and flags (hasValidation, hasHyperlink, hasConditionalFormat, hasChartHighlight, isMerged, isTableHeader).

Renderer parity

  • DOM renderer (experimentalCanvas={false}) applies every returned CSS property.
  • Canvas renderer (default) honors the subset it can paint: backgroundColor, backgroundImage gradients, color, the four border* sides, padding, textAlign, textDecoration, textOverflow, and font properties. CSS-only effects (boxShadow, outline, animation) apply in the DOM renderer. This is documented.
  • Not applied to worksheet thumbnails painted via useXlsxViewerThumbnails(...).

Changes

  • getCellStyle prop + XlsxCellStyleContext type, exported from the package
  • Wiring through XlsxViewerInnerXlsxGridgetCellData
  • Playground: a "Highlight" toggle demonstrating the prop live
  • README docs (root + package) with the context table and renderer notes

Test plan

  • pnpm typecheck (package + playground)
  • pnpm build (package) — getCellStyle and XlsxCellStyleContext present in dist/index.d.ts
  • Toggle "Highlight" in the playground and confirm odd rows tint in both canvas and DOM renderers

Made with Cursor

Adds an optional `getCellStyle` viewer prop that returns CSS overrides
merged on top of each cell's resolved style. This is a generic escape
hatch for per-cell styling (highlights, outlines, status tints) without
forking the workbook data.

The hook is applied at the single getCellData chokepoint so both the DOM
and canvas renderers honor it, and is wired into the cell render cache
invalidation so changing the callback re-resolves and repaints cells.
The callback receives an XlsxCellStyleContext with the cell address,
sheet, resolved style, value, and flags (validation, hyperlink,
conditional format, chart highlight, merge, table header).

Also adds a "Highlight" toggle to the playground demonstrating the prop
and documents it in both READMEs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

@tothienbao6a0 is attempting to deploy a commit to the Extend Team on Vercel.

A member of the Team first needs to authorize it.

@tothienbao6a0

tothienbao6a0 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Downstream, extend-hq/ui#15 (draft) exposes getCellStyle through the @extend/xlsx-viewer registry component, and is blocked on this shipping in a release.

@tothienbao6a0

Copy link
Copy Markdown
Contributor Author

Opening this up for discussion 👋

Motivation: when embedding the viewer in a host app, there's currently no first-class way to layer app-driven styling (row highlights, selection outlines, status tints, etc.) onto cells without forking the component or post-processing the DOM. getCellStyle(context) is meant to be a minimal, generic escape hatch: the viewer builds its normal Excel-derived style first, then lets the host return CSS overrides on top.

A few things I tried to get right:

  • Renderer parity — overrides are applied through the single getCellData path so they show up identically in both the DOM and canvas renderers. I verified this live in the playground (there's a "Highlight" toggle on this branch that tints odd rows; it renders the same with canvas on and off).
  • Zero-cost when unused — the callback only runs when a getCellStyle is provided, so there's no overhead for existing consumers.
  • Useful contextXlsxCellStyleContext exposes the cell address, sheet, resolved style, value, and flags (validation / hyperlink / conditional format / chart highlight / merged / table header) so hosts can make decisions without re-deriving them.

Open questions for maintainers:

  1. Naming — getCellStyle / XlsxCellStyleContext consistent with your conventions?
  2. Is the context flag set the right scope, or would you prefer to start narrower?
  3. Any concerns about returning React.CSSProperties vs a more constrained style shape for the canvas path?

Happy to adjust to fit how you'd want this to look long-term.

@andrewlu0

Copy link
Copy Markdown
Collaborator

hi! thank you for this - one case I found is conditional color scale backgrounds are not overwritten by getCellStyle, where from the API I would assume they should be. is this intended ? if not, could we also apply styles to conditional color scales? I attached a sample xlsx file where applying red background to every cell does not override the color scale backgrounds

conditional-color-scale.xlsx

Screenshot 2026-06-18 at 11 08 49 AM

Conditional color-scale fills are stored on a separate field that both
the DOM and canvas renderers prioritize over the resolved cell style, so
a background returned from getCellStyle was visually ignored on
color-scale cells. When getCellStyle returns an explicit background
(backgroundColor or background), clear the color-scale fill so the host
override wins — both target the full-cell background. Data bars and icon
sets are overlays and are left intact.

Co-authored-by: Cursor <cursoragent@cursor.com>
@tothienbao6a0

Copy link
Copy Markdown
Contributor Author

Great catch, thank you — that's not intended, it was a gap. getCellStyle should win here.

Root cause: conditional color-scale fills aren't part of the resolved cell style — they're carried on a separate conditionalColorScale field that both renderers prioritize over style.backgroundColor (the DOM path overwrites backgroundColor with the scale color, and the canvas path picks conditionalColorScale?.color ?? backgroundColor). So my override merged into style but got shadowed downstream on color-scale cells.

Fix (pushed in d7e25a1): when getCellStyle returns an explicit background (backgroundColor or background), I clear the color-scale fill for that cell so the host override wins. Both target the full-cell background, so this lines up with the "getCellStyle is the final layer" expectation — your red-on-every-cell sample now paints red over the scale in both the DOM and canvas paths.

One scope question for you: I deliberately left data bars and icon sets intact, since those read as overlays/glyphs drawn on top of the fill rather than background fills (a background override replacing them seemed surprising). If you'd rather getCellStyle be able to suppress those too, I'm happy to add that — possibly behind something more explicit so it stays predictable. Let me know which behavior you'd prefer and I'll match it.

@andrewlu0

Copy link
Copy Markdown
Collaborator

yeah, i think data bars and icons should not be covered by cell styles, at least thats what i would expect

@andrewlu0 andrewlu0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@andrewlu0
andrewlu0 merged commit 064e028 into extend-hq:main Jun 18, 2026
1 of 2 checks passed
@tothienbao6a0

Copy link
Copy Markdown
Contributor Author

Thanks for merging, and agreed — data bars and icon sets staying put matches what's implemented here, so we're aligned on the semantics. 👍

One quick follow-up: is there a release planned that'll include this? I'd like to pin the downstream registry-component PR (extend-hq/ui#15) to the exact version that ships getCellStyle and take it out of draft. Happy to update it the moment a version is cut — just let me know the number.

@andrewlu0

Copy link
Copy Markdown
Collaborator

Thanks for merging, and agreed — data bars and icon sets staying put matches what's implemented here, so we're aligned on the semantics. 👍

One quick follow-up: is there a release planned that'll include this? I'd like to pin the downstream registry-component PR (extend-hq/ui#15) to the exact version that ships getCellStyle and take it out of draft. Happy to update it the moment a version is cut — just let me know the number.

just released 0.11.0 on npm !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants