Skip to content
Apertura
API reference

@apertura/xlsx-view

DOM renderer for Excel workbooks

24 exported symbols · 22 declared here · 2 re-exported

Classes

AxisMetrics
class AxisMetrics

One axis of the grid. Ranges must be sorted and must not overlap; the constructor enforces neither because both callers build them in order and the check would be the most expensive thing here.

count
number
total
number
defaultSize
number
size
(index: number) => number
offset
(index: number) => number
Pixel offset of the start of `index`.
indexAt
(position: number) => number
The index whose band contains `position`; clamped to the axis.
CellCssBuilder
class CellCssBuilder
stylesheet
StyleSheetBuilder
appearance
(format: CellFormat) => CellAppearance
The class and the measurement facts for a resolved cell format.
differential
(format: DifferentialFormat) => string | undefined
A class for a differential format, as conditional formatting applies.
tableStyle
(format: DifferentialFormat) => string | undefined
A class for what a table paints under its cells. The same declarations as any differential format, but emitted into a cascade layer, because a table style is the one kind of formatting that *loses*: a cell that sets its own fill keeps it, and the band shows only where the cell asked for nothing.
baseFontPoints
() => number
The size the sheet is set in, at the zoom it is drawn at, in points. The grid needs it because a cell that states no size of its own emits no `font-size` at all — the declarations are a *diff* against the base format, which is what keeps the stylesheet to a few dozen rules instead of one per cell. Those cells then inherit, and what they inherit has to be the base size multiplied by the zoom. It used to be a literal `11pt` in the stylesheet, which is how a plain sheet came to keep its type at one size while its columns and rows scaled around it: the reader zoomed out and the words stayed put while the cells shrank away from under them.
ConditionalFormatter
class ConditionalFormatter
empty
boolean
needsEvaluator
boolean
Whether any rule needs a formula engine to decide. An `expression` rule always does. So does a comparison whose operand is not a number, a quoted string or a cell of this sheet — `Sheet2!$A$1` is the common case, and it is common because the schema cannot hold it: a rule that compares across sheets is written in an extension block, and its operand is a formula like any other.
useEvaluator
(predicate: PredicateEvaluator | undefined) => void
Gives the formatter a way to evaluate `expression` rules.
evaluate
(row: number, column: number, cell: Cell | undefined) => ConditionalResult | undefined
SheetGrid
class SheetGrid
element
HTMLElement
metrics
SheetMetrics
Where the rows and columns are, as the grid currently has them. Not the object handed in: folding an outline group rebuilds it, and the rebuilt one is what everything is drawn against.
selection
GridSelection | undefined
refresh
() => void
Rebuilds the visible window from nothing. Called when something behind the cells has changed and cannot be seen from the window: the container was resized, the formula engine finished and the conditional formats can finally be evaluated, a sparkline arrived. A plain re-render would do nothing in those cases — the window is the same, and the panes are skipped precisely because it is — so what is already drawn is thrown away first.
destroy
() => void
scrollTo
(row: number, column: number) => void
Scrolls until a cell is inside the scrolling pane.
select
(row: number, column: number, extend?: boolean) => void
cellAt
(row: number, column: number) => Cell | undefined
The cell behind a selection, for the formula bar.
setSparklines
(sparklines: ReadonlyMap<number, SparklineCell>) => void
The sparklines of this sheet, by the cell each draws in. Set from outside rather than read here, because the numbers behind a sparkline come from a range that may be on another sheet, and resolving one needs the workbook and the formula engine — neither of which the grid has.
search
(query: string) => number
Every cell of the sheet whose text contains the query. Searched against what is *shown*, which is the only thing a reader can have read: a search for `15 January 2024` finds the cell holding 45306, and a search for 45306 does not. Rows the reader folded or filtered away are skipped for the same reason — they are not on the screen being searched. The browser cannot do this itself. A grid draws the few hundred cells in its window and no more, so a page can only find what has been drawn: it reports nothing for text plainly in the document, which is worse than having no search at all, because it answers.
stepMatch
(direction: 1 | -1) => { at: number; of: number; } | undefined
Moves to the next match, or the previous one, and shows it. Wraps at either end: a reader stepping through a sheet expects to come back round rather than to stop at the bottom with no word about why.
SheetMetrics
class SheetMetrics

The geometry of one sheet at one zoom level.

columns
AxisMetrics
rows
AxisMetrics
zoom
number
digitWidth
number
Width of the digit zero, kept so a cell can say how many characters fit.
withRowVisibility
(rowVisibility: ReadonlyMap<number, boolean>) => SheetMetrics
The same geometry with a different set of rows folded away. Rebuilt rather than adjusted: the offsets are a prefix sum, so one row changing height moves every row after it and there is nothing to patch. A rebuild walks the exceptions, which are a few dozen entries.
withSizes
(sizes: { columnWidths?: ReadonlyMap<number, number>; rowHeights?: ReadonlyMap<number, number>; }) => SheetMetrics
The same geometry with a column or a row dragged to a new size.
XlsxView
class XlsxView extends BaseDocumentView<XlsxDocument>
activeSheetIndex
number
pageOfLocator
(locator: string) => number | undefined
Which sheet an address is on. A workbook shows one sheet at a time, so an address on another sheet is in the same position as a page that has not been mounted: nothing the reader does to the grid will ever bring it into view. The flow of the address names the sheet, and the workbook says where that sheet sits.
showSheet
(index: number) => Promise<void>
goToCell
(row: number, column: number) => void
Selects a cell, scrolling it into view.
renderContent
() => Promise<void>
Renders the content into {@link root}. Called on every update.
openFind
() => void
Opens the find bar, or puts the cursor back in it. Part of the viewer rather than of the host: the reason a search is needed at all is the virtualised grid, which is this component, and a host that did not know to build one would leave the reader with a Ctrl+F that lies.
closeFind
() => void
Closes the find bar and takes its marks off the sheet.
onContainerResize
() => void
Called when the container is resized. The default is a full re-render. Renderers that can reflow incrementally should override this — a full rebuild on every resize frame is exactly the behaviour that makes viewers feel slow.
setZoom
(zoom: number) => void
Changes the zoom level and re-renders.
update
() => void
Re-render after the container was resized or settings changed.
destroy
() => void
Tear the view down and release resources. The container is left empty.

Functions

cellButton
function cellButton(row: number, column: number, filters: readonly AutoFilter[], validations: readonly DataValidation[]): "filter" | "filtering" | "list" | undefined

The button Excel puts in a cell that opens something. Two of them look alike and mean different things. A filter button sits in the header row of an `autoFilter` range and is always there; the arrow of a list validation belongs to any cell the list covers. Both are what a reader looks for to know a sheet can be filtered or chosen from. A column that is currently filtering is drawn apart from one that merely could, because in Excel it is: the funnel replaces the arrow, and it is the only thing on screen saying rows are missing.

columnWidthPixels
function columnWidthPixels(widthCharacters: number, digitWidth: number): number

A column width in characters converted to pixels. `px = trunc(width * digitWidth) + 5` (MS-OI29500). The truncation is not a rounding choice — it is what makes 8.43 characters come out as exactly 64 pixels, and dropping it puts every column a pixel out.

renderSparkline
function renderSparkline(ownerDocument: Document, group: SparklineGroup, values: readonly (number | undefined)[], box: SparklineBox, color: ColorLookup): SVGElement | undefined

Draws one sparkline into an SVG element. `values` are the numbers the range held, with `undefined` where a cell was empty — which is a hole in the line rather than a zero, unless the group says otherwise.

rotationCss
function rotationCss(rotation: number, lineHeightPx: number): { text: Record<string, string>; cell: Record<string, string>; } | undefined

How a cell turns its text, `xf/alignment/@textRotation`. Excel states one number for two directions and a special case, and the encoding is not an angle: 0 to 90 turn the text anticlockwise, 91 to 180 turn it *clockwise* by what is left after ninety — so 180 is a quarter turn down to the right, not half a turn — and 255 stacks the letters one under another without turning them at all. A turn is about the corner the line starts from: rising to the right it starts at the bottom left of the cell, falling to the right at the top left. Turning about the middle instead swings half the line out through the edge, and the cell clips it — a header reading `tated` where the file said `Rotated`. The turn also carries the line *left* by its own height, since the box pivots about a corner it no longer occupies, so the line is pushed back by that much first: without it a quarter turn leaves the text entirely outside its cell, and at forty-five degrees it eats the first letter.

shapeCssfrom @apertura/drawing
function shapeCss(shape: ShapeLook, box: { width: number; height: number; }, themeColor: ThemeColorLookup): ShapeCss

The declarations that draw a shape. The geometry is honoured as far as CSS can take it: a rectangle is a rectangle, rounded corners are a radius, an ellipse is a radius of half. Everything else is drawn as its bounding rectangle — which the corpus says is a fair trade, since of the 2 862 shapes in it 2 827 are plain rectangles and 34 are rounded ones.

Interfaces

CellAppearance
interface CellAppearance

Everything a cell's class has to encode, beyond its format.

className
string | undefined
The class name, or `undefined` when the format needs no rule at all.
inheritedClassName
string | undefined
The part of the look the cell inherited rather than asked for. Drawn in a layer below the table styles, so that a table paints over the default font and under everything the cell actually set.
wrap
boolean
Whether the format wraps, which changes how the text is laid out.
fontFamily
string
Font as the measurer wants it, for the fit and overflow decisions.
fontSizePx
number
bold
boolean
italic
boolean
horizontal
import("/repo/packages/xlsx/dist/index").HorizontalAlignment
indentPx
number
rotation
number
ConditionalResult
interface ConditionalResult

What the rules decided about one cell.

className
string | undefined
Class of the differential format, when a rule matched.
bar
{ start: number; width: number; color: string; border: string | undefined; gradient: boolean; axis: { at: number; color: string; } | undefined; showValue: boolean; } | undefined
A data bar, as fractions of the cell's width. `start` and `width` rather than a length alone, because a bar does not always begin at the edge: where the values cross zero the bar grows out of an axis somewhere inside the cell, and the negative ones grow the other way.
scaleColor
string | undefined
Background from a colour scale.
icon
string | undefined
Icon glyph from an icon set.
hideValue
boolean
Whether the rule hides the value and shows only its decoration.
GridOptions
interface GridOptions
data
SheetData
metrics
SheetMetrics
styles
Stylesheet
css
CellCssBuilder
conditional
ConditionalFormatter
measurer
TextMetricsCache
date1904
boolean
showGridLines
boolean
showHeaders
boolean
zoom
number
imageUrl
(partName: string) => string | undefined
Resolves an image part to a URL the browser can load.
onSelect
(selection: GridSelection) => void
onZoom?
((zoom: number) => void) | undefined
Asked for a new zoom, when the reader turns the wheel with Ctrl held. The grid does not own the zoom — it is a property of the view, which has to rebuild the metrics and the styles for it — so the wheel asks rather than sets.
locatorPrefix?
string | undefined
Address prefix for this sheet, when the view is stamping them. A worksheet's cells have a native address — `Sheet!C14` — so the prefix is everything before it and the cell appends its own reference. Cheaper than building a locator per cell, and a grid of a million cells is exactly where that matters. Absent by default: a viewer that never highlights anything should pay nothing for the machinery.
GridSelection
interface GridSelection
active
{ row: number; column: number; }
range
CellRange
MetricsOptions
interface MetricsOptions
zoom
number
digitWidth
number
Width of the digit zero in the workbook's default font, in pixels.
minimumColumns
number
Lowest number of columns and rows to lay out, so a small sheet still fills the view.
minimumRows
number
rowVisibility?
ReadonlyMap<number, boolean> | undefined
Rows whose visibility the reader has taken over: true hidden, false shown. What folding an outline group amounts to — the rows keep their values and lose their height — and it has to say *shown* as well as *hidden*, because a group saved folded arrives with its rows already hidden by the file, and unfolding it has to overrule that. Kept out of the parsed data because it is the reader's doing and not the document's.
columnWidths?
ReadonlyMap<number, number> | undefined
Sizes the reader has dragged, in pixels, already scaled by the zoom. A column dragged wider is not a change to the document — nothing is written back — and it is the one thing a reader can do about a column of hashes or a heading cut off at the fold. Kept beside the folded rows because it is the same kind of thing: what the reader has done to the view.
rowHeights?
ReadonlyMap<number, number> | undefined
SizeRange
interface SizeRange

A run of consecutive indices that share a size.

from
number
to
number
size
number
Size in pixels; zero means hidden.
SparklineCell
interface SparklineCell

One sparkline ready to draw: the look it belongs to, and its numbers.

group
SparklineGroup
values
readonly (number | undefined)[]
XlsxViewOptions
interface XlsxViewOptions extends ViewOptions
formulaBar?
boolean | undefined
Show the formula bar above the grid. Defaults to true.
sheetTabs?
boolean | undefined
Show the sheet tabs. Defaults to true.
headers?
boolean | undefined
Show the row and column headers. Defaults to the sheet's own setting.
onSelectionChange?
((selection: GridSelection, sheetIndex: number) => void) | undefined
Called when the reader selects a different cell or range.
onSheetChange?
((index: number) => void) | undefined
locators?
boolean | { hash: string; } | undefined
Stamp every cell with the address extraction gave it, as `data-loc`. Off by default: it is one attribute per rendered cell, and a viewer that never highlights anything should pay nothing for it. On, it is what lets a passage found by a retrieval system be scrolled to and highlighted in the workbook it came from. The hash must be the one extraction used, or an address minted by one half is refused by the other.

Type aliases

ShapeCssfrom @apertura/drawing
type ShapeCss = Partial<Record<string, string>>

Values

DEFAULT_COLUMN_WIDTH_CHARACTERS
DEFAULT_COLUMN_WIDTH_CHARACTERS: 8.43

Excel's default column width, in pixels. The file says "8.43 characters" and means the width of the digit zero in the workbook's Normal font, plus five pixels of padding. For Calibri 11 that digit is seven pixels wide, which is where the familiar 64 comes from.

DEFAULT_DIGIT_WIDTH
DEFAULT_DIGIT_WIDTH: 7
XLSX_VIEW_CSS
XLSX_VIEW_CSS: "\n.jo-xl {\n --jo-xl-line: #d0d7de;\n --jo-xl-header-bg: #f5f5f5;\n --jo-xl-header-fg: #575757;\n --jo-xl-surface: #ffffff;\n /*\n * Black, and not a softer near-black.\n *\n * A cell whose font names no colour is drawn in the window text colour, and\n * in Excel that is pure black — every reference reading of every workbook\n * says #000000. A gentler grey reads better on its own but it is not what the\n * document looks like, and next to a cell that *does* set black the\n * difference shows as two shades of text in one column.\n */\n --jo-xl-text: #000000;\n --jo-xl-accent: #217346;\n --jo-xl-selection: rgba(33, 115, 70, 0.12);\n\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n background: var(--jo-xl-surface);\n color: var(--jo-xl-text);\n font-family: Calibri, 'Segoe UI', system-ui, sans-serif;\n font-size: 11pt;\n --jo-xl-row-header-width: 46px;\n}\n\n/* ------------------------------------------------------------- formula bar */\n\n.jo-xl__formula-bar {\n display: flex;\n align-items: stretch;\n gap: 0;\n flex: 0 0 auto;\n border-bottom: 1px solid var(--jo-xl-line);\n background: var(--jo-xl-surface);\n font-size: 12px;\n}\n\n.jo-xl__name-box {\n width: 140px;\n padding: 4px 8px;\n border-right: 1px solid var(--jo-xl-line);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.jo-xl__fx {\n padding: 4px 10px;\n color: var(--jo-xl-header-fg);\n font-style: italic;\n border-right: 1px solid var(--jo-xl-line);\n}\n\n.jo-xl__formula {\n flex: 1;\n padding: 4px 8px;\n white-space: pre;\n overflow: hidden;\n text-overflow: ellipsis;\n font-family: Consolas, 'SF Mono', monospace;\n}\n\n/* -------------------------------------------------------------------- grid */\n\n.jo-xl__host {\n flex: 1 1 auto;\n min-height: 0;\n position: relative;\n}\n\n/*\n * The grid does not select text; it selects cells.\n *\n * Dragging over a range is the sheet's own gesture, and while the browser was\n * also allowed to drag a text selection through it the two ran at once — and\n * the browser's ran through the *document* order, which on an absolutely\n * positioned grid has nothing to do with what is on the screen. The reader drew\n * a tidy green rectangle and got a ragged blue one over cells nowhere near it.\n *\n * Text selection is not given up, only asked for: double-clicking a cell opens\n * it, and the --reading rule below hands that one cell back to the browser so\n * the words in it can be selected and copied on their own. Escape, or a click\n * anywhere else, closes it again. It is what Excel does, and it is the only\n * arrangement in which both gestures can mean something.\n */\n.jo-xl__grid {\n user-select: none;\n -webkit-user-select: none;\n display: grid;\n width: 100%;\n height: 100%;\n overflow: hidden;\n position: relative;\n background: var(--jo-xl-surface);\n}\n\n/*\n * The grid is a tab stop, and its focus is shown by the selection.\n *\n * A ring around the whole sheet says only that the grid has focus, which is the\n * least useful thing a reader could be told; the selection rectangle says\n * *where* the focus is, which is what they need and what every spreadsheet\n * shows. So the ring is suppressed and the selection stands in for it — which\n * means the selection must always be drawn, and it is.\n */\n.jo-xl__grid:focus,\n.jo-xl__grid:focus-visible {\n outline: none;\n}\n\n.jo-xl__grid--rtl { direction: rtl; }\n\n.jo-xl__corner,\n.jo-xl__colhead,\n.jo-xl__rowhead,\n.jo-xl__pane {\n position: relative;\n overflow: hidden;\n min-width: 0;\n min-height: 0;\n}\n\n.jo-xl__scroller {\n overflow: auto;\n /* Scrolling repaints the whole pane; telling the compositor in advance keeps\n a large sheet at one frame per scroll instead of one per repaint. */\n will-change: scroll-position;\n}\n\n.jo-xl__sizer {\n position: absolute;\n top: 0;\n left: 0;\n pointer-events: none;\n}\n\n.jo-xl__canvas,\n.jo-xl__lines {\n position: absolute;\n top: 0;\n left: 0;\n width: 0;\n height: 0;\n}\n\n/*\n * The headers are part of the grid, so they scale with it.\n *\n * Relative to the grid's own size, which the view sets from the workbook's base\n * format multiplied by the zoom. A literal pixel size here was one of the\n * lengths that did not scale: the header boxes shrank with the columns and the\n * letters in them did not.\n */\n.jo-xl__corner,\n.jo-xl__colhead,\n.jo-xl__rowhead {\n background: var(--jo-xl-header-bg);\n color: var(--jo-xl-header-fg);\n font-size: 0.75em;\n user-select: none;\n}\n\n.jo-xl__corner {\n border-right: 1px solid var(--jo-xl-line);\n border-bottom: 1px solid var(--jo-xl-line);\n cursor: pointer;\n}\n\n.jo-xl__colhead { border-bottom: 1px solid var(--jo-xl-line); }\n.jo-xl__rowhead { border-right: 1px solid var(--jo-xl-line); }\n\n/* The freeze line, drawn on the panes that border it. */\n.jo-xl__pane--tl,\n.jo-xl__pane--tr { border-bottom: 1px solid #a0a0a0; }\n.jo-xl__pane--tl,\n.jo-xl__pane--bl { border-right: 1px solid #a0a0a0; }\n\n.jo-xl__header {\n position: absolute;\n top: 0;\n left: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n border-right: 1px solid var(--jo-xl-line);\n border-bottom: 1px solid var(--jo-xl-line);\n overflow: hidden;\n cursor: default;\n}\n\n.jo-xl__header--selected {\n background: var(--jo-xl-selection);\n color: var(--jo-xl-accent);\n font-weight: 600;\n}\n\n/* ------------------------------------------------------------------- cells */\n\n.jo-xl__cell {\n position: absolute;\n top: 0;\n left: 0;\n display: flex;\n align-items: flex-end;\n box-sizing: border-box;\n /*\n * Excel's own gap between a cell's edge and its text, and it is a length like\n * any other on the grid: it scales. Written as a property the view sets from\n * the zoom rather than as a literal, because a literal is what it was — and a\n * three-pixel constant left unscaled shifts every column of a zoomed sheet by\n * three pixels more than it should, which is small, systematic, and exactly\n * the kind of thing nobody finds by looking.\n */\n padding: 0 var(--jo-xl-cell-pad, 3px);\n overflow: hidden;\n white-space: pre;\n line-height: 1.15;\n /* Layout containment only. Paint containment would clip the text of a cell\n that overflows into its empty neighbours, which is behaviour Excel has and\n a grid has to keep. */\n contain: layout;\n}\n\n/* Text that spills into empty neighbours, as Excel lets it. */\n.jo-xl__cell--overflow {\n overflow: visible;\n z-index: 2;\n max-width: var(--jo-xl-spill);\n}\n\n.jo-xl__cell--merged { z-index: 1; }\n\n/*\n * The one cell the reader opened for its text.\n *\n * Raised above its neighbours so that a selection dragged across it cannot\n * wander into the cells drawn over it, and given a caret so that the browser's\n * own word and line selection work inside it.\n */\n.jo-xl__cell--reading {\n user-select: text;\n -webkit-user-select: text;\n cursor: text;\n z-index: 3;\n overflow: visible;\n}\n\n/*\n * A link is blue only when the workbook says so.\n *\n * Excel does not colour a cell because it has a hyperlink on it; it colours it\n * because the cell wears the Hyperlink style, which is written into the file\n * like any other. Files that libraries produce often carry the link without the\n * style, and Excel draws those in plain black — so painting every link blue\n * shows a document that does not exist. The pointer stays, and the underline\n * comes back under the cursor, where it costs the picture nothing.\n */\n.jo-xl__cell--link {\n cursor: pointer;\n}\n\n.jo-xl__cell--link:hover {\n text-decoration: underline;\n}\n\n/* A comment is marked the way Excel marks it: a triangle in the corner. */\n.jo-xl__cell--commented::after {\n content: '';\n position: absolute;\n top: 0;\n right: 0;\n border: 3px solid transparent;\n border-top-color: #d1343b;\n border-right-color: #d1343b;\n}\n\n.jo-xl__gridline {\n position: absolute;\n top: 0;\n left: 0;\n background: var(--jo-xl-line);\n pointer-events: none;\n}\n\n.jo-xl__gridline--h { height: 1px; }\n.jo-xl__gridline--v { width: 1px; }\n\n.jo-xl__selection {\n position: absolute;\n top: 0;\n left: 0;\n border: 2px solid var(--jo-xl-accent);\n background: var(--jo-xl-selection);\n pointer-events: none;\n z-index: 3;\n}\n\n/* --------------------------------------------------- conditional formatting */\n\n.jo-xl__bar {\n position: absolute;\n left: 0;\n top: 2px;\n bottom: 2px;\n border-radius: 1px;\n opacity: 0.75;\n pointer-events: none;\n}\n\n/* The value sits above its data bar, as it does in Excel. An absolutely\n positioned bar paints over ordinary flow content, so the text is given a\n position of its own rather than the bar a negative index — which would put\n it behind the cell's own background. */\n.jo-xl__value {\n position: relative;\n z-index: 1;\n min-width: 0;\n overflow: inherit;\n text-overflow: inherit;\n white-space: inherit;\n}\n\n.jo-xl__icon {\n font-size: 0.85em;\n margin-right: 3px;\n flex: 0 0 auto;\n}\n\n/* The button of a filter header or of a list validation. Excel draws it as\n part of the cell, at the right edge, over whatever the cell holds. */\n\n.jo-xl__button {\n position: absolute;\n right: 1px;\n top: 50%;\n transform: translateY(-50%);\n /* Above the cell's own text, which is drawn after it and would otherwise\n take the click meant for the button. */\n z-index: 4;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15px;\n height: 15px;\n border: 1px solid var(--jo-xl-line);\n border-radius: 2px;\n background: var(--jo-xl-header-bg);\n color: var(--jo-xl-header-fg);\n font-size: 9px;\n line-height: 1;\n /* It used to be a picture of a button. It opens a menu now, so it takes the\n click that lands on it rather than passing it through to the cell. */\n cursor: pointer;\n}\n\n.jo-xl__button:hover {\n border-color: var(--jo-xl-accent);\n}\n\n.jo-xl__button--filtering {\n color: var(--jo-xl-accent);\n font-weight: 700;\n}\n\n/*\n * The note a commented cell shows.\n *\n * Excel's colours, because a reader recognises them: the pale yellow paper and\n * the thin grey rule are what a note has looked like for thirty years, and a\n * box in the viewer's own palette would read as part of the viewer rather than\n * as something written into the document.\n */\n.jo-xl__note {\n position: absolute;\n z-index: 22;\n max-width: 320px;\n max-height: 240px;\n overflow: auto;\n padding: 6px 8px;\n border: 1px solid #b0b0a0;\n background: #ffffe1;\n box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.25);\n color: #000000;\n font-size: 12px;\n line-height: 1.35;\n /* The pointer must be able to reach it: a note longer than its box is one\n the reader has to scroll. */\n pointer-events: auto;\n}\n\n.jo-xl__note-author {\n font-weight: 700;\n margin-bottom: 2px;\n}\n\n.jo-xl__note-text {\n white-space: pre-wrap;\n}\n\n/*\n * The find bar, and the marks it leaves on the sheet.\n *\n * Floated over the top-right of the grid rather than pushed into the layout:\n * opening a search should not move the rows the reader was looking at.\n */\n.jo-xl__find {\n position: absolute;\n z-index: 25;\n top: 6px;\n right: 18px;\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 6px;\n border: 1px solid var(--jo-xl-line);\n border-radius: 4px;\n background: var(--jo-xl-surface);\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15);\n font-size: 12px;\n}\n\n.jo-xl__find-input {\n width: 180px;\n padding: 3px 6px;\n border: 1px solid var(--jo-xl-line);\n border-radius: 3px;\n font: inherit;\n color: inherit;\n background: var(--jo-xl-surface);\n}\n\n.jo-xl__find-count {\n min-width: 72px;\n color: var(--jo-xl-header-fg);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n.jo-xl__find-button {\n width: 22px;\n height: 22px;\n border: 1px solid var(--jo-xl-line);\n border-radius: 3px;\n background: var(--jo-xl-header-bg);\n color: inherit;\n font: inherit;\n line-height: 1;\n cursor: pointer;\n}\n\n.jo-xl__find-button:hover {\n border-color: var(--jo-xl-accent);\n}\n\n/*\n * A cell a search matched, and the one being stood on.\n *\n * The mark has to survive whatever the cell already wears — a table band, a\n * conditional format, its own fill — so it is an outline and a wash rather than\n * a background, which any of those would win against.\n */\n.jo-xl__cell--match {\n box-shadow: inset 0 0 0 1px var(--jo-xl-accent);\n background-image: linear-gradient(\n color-mix(in srgb, var(--jo-xl-accent) 18%, transparent),\n color-mix(in srgb, var(--jo-xl-accent) 18%, transparent)\n );\n}\n\n.jo-xl__cell--match-current {\n box-shadow: inset 0 0 0 2px var(--jo-xl-accent);\n background-image: linear-gradient(\n color-mix(in srgb, var(--jo-xl-accent) 38%, transparent),\n color-mix(in srgb, var(--jo-xl-accent) 38%, transparent)\n );\n}\n\n/*\n * The strip on a header's trailing edge that resizes it.\n *\n * Wider than the line it sits on: a two-pixel target is one a reader misses,\n * and every spreadsheet gives this a few pixels of slack. It stays inside the\n * header, which clips what overflows it, and sits above the header's own label\n * so that a press on it is a resize rather than a selection.\n */\n.jo-xl__grip {\n position: absolute;\n z-index: 5;\n}\n\n.jo-xl__grip--column {\n top: 0;\n bottom: 0;\n right: 0;\n width: 6px;\n cursor: col-resize;\n}\n\n.jo-xl__grip--row {\n left: 0;\n right: 0;\n bottom: 0;\n height: 6px;\n cursor: row-resize;\n}\n\n/* Where the drag will land, drawn while it is held. */\n.jo-xl__guide {\n position: absolute;\n z-index: 30;\n background: var(--jo-xl-accent);\n pointer-events: none;\n}\n\n.jo-xl__guide--column {\n top: 0;\n bottom: 0;\n width: 1px;\n}\n\n.jo-xl__guide--row {\n left: 0;\n right: 0;\n height: 1px;\n}\n\n/*\n * The menu a filter button opens.\n *\n * Above the panes rather than inside one: a pane clips what overflows it, and a\n * menu that lists twenty values would be cut off at the first row boundary.\n */\n.jo-xl__filter-menu {\n position: absolute;\n z-index: 20;\n min-width: 160px;\n max-width: 280px;\n border: 1px solid var(--jo-xl-line);\n border-radius: 4px;\n background: var(--jo-xl-surface);\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);\n font-size: 12px;\n overflow: hidden;\n}\n\n.jo-xl__filter-list {\n max-height: 240px;\n overflow: auto;\n padding: 4px 0;\n}\n\n.jo-xl__filter-item {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 2px 8px;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.jo-xl__filter-item:hover {\n background: var(--jo-xl-header-bg);\n}\n\n.jo-xl__filter-item span {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.jo-xl__filter-actions {\n display: flex;\n gap: 4px;\n padding: 6px;\n border-top: 1px solid var(--jo-xl-line);\n}\n\n.jo-xl__filter-button {\n flex: 1;\n padding: 3px 6px;\n border: 1px solid var(--jo-xl-line);\n border-radius: 3px;\n background: var(--jo-xl-header-bg);\n color: inherit;\n font: inherit;\n cursor: pointer;\n}\n\n.jo-xl__filter-button:hover {\n border-color: var(--jo-xl-accent);\n}\n\n/* A shape's own outline, drawn under whatever text it holds. */\n\n.jo-xl__outline {\n position: absolute;\n inset: 0;\n overflow: visible;\n pointer-events: none;\n}\n\n/* The outline gutter: a bracket per group, a button on its summary row. */\n\n.jo-xl__outline-bracket {\n position: absolute;\n top: 0;\n left: 0;\n width: 1px;\n background: var(--jo-xl-header-fg);\n opacity: 0.55;\n}\n\n.jo-xl__outline-button {\n position: absolute;\n top: 0;\n left: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 11px;\n height: 11px;\n border: 1px solid var(--jo-xl-header-fg);\n background: var(--jo-xl-bg);\n color: var(--jo-xl-header-fg);\n font-size: 9px;\n line-height: 1;\n}\n\n/* The axis of a data bar: where zero falls, when it falls inside the cell. */\n\n.jo-xl__bar-axis {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 1px;\n pointer-events: none;\n}\n\n/* A sparkline fills its cell and sits behind whatever the cell holds. */\n\n.jo-xl__sparkline {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n\n/* --------------------------------------------------------------- drawings */\n\n.jo-xl__drawing {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 4;\n pointer-events: none;\n overflow: hidden;\n}\n\n/*\n * A picture fills its box, and is distorted if the box says so.\n *\n * Excel stretches a picture into the rectangle it was given — a:stretch is what\n * the file says and stretching is what it means — so a picture whose box was\n * dragged out of proportion is drawn out of proportion. Fitting it inside\n * instead keeps the shape but leaves bands of empty sheet on two sides and puts\n * the picture somewhere the author did not, which is the more visible error of\n * the two and the harder one to explain.\n */\n.jo-xl__drawing img {\n width: 100%;\n height: 100%;\n object-fit: fill;\n}\n\n.jo-xl__drawing--placeholder {\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px dashed var(--jo-xl-line);\n background: color-mix(in srgb, var(--jo-xl-header-bg) 70%, transparent);\n color: var(--jo-xl-header-fg);\n font-size: 11px;\n text-align: center;\n padding: 4px;\n}\n\n.jo-xl__drawing--shape {\n white-space: pre-wrap;\n font-size: 11px;\n}\n\n/* ------------------------------------------------------------------- tabs */\n\n.jo-xl__tabs {\n display: flex;\n gap: 1px;\n padding: 3px 6px 0;\n border-top: 1px solid var(--jo-xl-line);\n background: var(--jo-xl-header-bg);\n overflow-x: auto;\n flex: 0 0 auto;\n}\n\n.jo-xl__tab {\n appearance: none;\n border: 1px solid transparent;\n border-bottom: none;\n border-radius: 4px 4px 0 0;\n background: transparent;\n color: inherit;\n padding: 4px 12px;\n cursor: pointer;\n white-space: nowrap;\n font: inherit;\n font-size: 12px;\n border-bottom: 3px solid var(--jo-xl-tab-color, transparent);\n}\n\n.jo-xl__tab:hover { background: rgba(0, 0, 0, 0.04); }\n\n.jo-xl__tab[aria-selected='true'] {\n background: var(--jo-xl-surface);\n border-color: var(--jo-xl-line);\n border-bottom-color: var(--jo-xl-tab-color, var(--jo-xl-surface));\n font-weight: 600;\n color: var(--jo-xl-accent);\n}\n\n.jo-xl__tab--hidden { opacity: 0.55; font-style: italic; }\n\n.jo-xl__empty {\n padding: 32px;\n text-align: center;\n color: var(--jo-xl-header-fg);\n font-size: 13px;\n}\n\n@media (prefers-color-scheme: dark) {\n .jo-xl {\n --jo-xl-line: #3d444d;\n --jo-xl-header-bg: #21262d;\n --jo-xl-header-fg: #9198a1;\n --jo-xl-surface: #0d1117;\n --jo-xl-text: #e6edf3;\n --jo-xl-accent: #3fb950;\n --jo-xl-selection: rgba(63, 185, 80, 0.16);\n }\n\n .jo-xl__tab:hover { background: rgba(255, 255, 255, 0.06); }\n}\n\n@media print {\n .jo-xl__formula-bar,\n .jo-xl__tabs { display: none; }\n\n .jo-xl__scroller { overflow: visible; }\n}\n"

The renderer's own stylesheet. Only the chrome and the structure are here. Everything that comes from the workbook — fonts, fills, borders, alignment — is emitted as generated classes at render time, because it differs per file and there are a few dozen of them per sheet rather than a fixed set.

xlsxView
xlsxView: ViewPlugin<XlsxDocument>