Make Updog's CSV Importer Match Your Product
I built Updog knowing it would live inside someone else's app, a real product that already has its own colors, its own font, its own corners and habits. An embedded importer has to disappear into that design and leave no seam. The person bringing in their data should never notice where your app ends and the editor begins.
That made Updog white-label from the start. There is no branding to strip, and the styling is not locked behind a theme object you pass in or an importer sealed in an iframe your CSS never reaches. Those approaches share one ceiling: the day your design needs something the fixed list does not cover, you stop and wait for it. So the whole look rides on CSS variables and BEM classes, which makes Updog a customizable CSV importer in the plainest sense. The browser already understands both, every element carries them, and you override them the same way you style your own markup. There is no private API. It is plain CSS, and it is available on every plan.
Everything is a CSS variable
The variables come in layers. At the bottom is a small set of base colors. From those, a wide range of shades is generated. On top sit named tokens, surface, border, and content, that point at the shades. A fourth set, the grid variables, carries those tokens into the spreadsheet, which is drawn on a canvas. You can reach in at any layer, and where you reach in decides how much you control and how much you let the editor decide for you.
This guide covers the full surface in the order I would work through it: base colors, semantic tokens, typography, the spreadsheet grid, and dark mode. The base colors alone give a rough fit. The semantic tokens give an exact match. The reference tables for every token live in the styling docs; here I explain how the layers fit together.
Map your design system onto Updog
A precise match is a mapping job. Every color and shape in your design system has a home in Updog, and the work is connecting the two. Go in this order, because each step builds on the one before it. Start with your neutrals, since they cover the most pixels. Your page and panel backgrounds map to the surface tokens, from layer0 for the base canvas up to layer4 for the most raised panel. Your dividers and outlines map to the two border tokens. Your text colors map to the content tokens, with primary as the base color the editor inherits everywhere, so set it first. Set these and most of the foreign feeling is gone.
Then the brand. Your primary action color becomes --updog-brand, and a full range of shades regenerates from it for hovers, links, the selected cell, and the active header. Your four status colors, for errors, success, warnings, and information, map to red, green, yellow, and blue. After that comes your font family. Last, carry the same decisions into the grid variables so the spreadsheet matches the rest. The sections below give the exact variable for each.
The base colors
Six colors sit under everything. Set these and the editor builds every shade it needs from them. This is the smallest change that still touches the whole product.
:root {
--updog-brand: #2f6df6;
--updog-neutral: #6b7280;
--updog-red: #e5484d;
--updog-green: #30a46c;
--updog-yellow: #ffb224;
--updog-blue: #0091ff;
} --updog-brand drives the primary actions and the highlights. --updog-neutral drives the grays, so the surfaces, the borders, and the muted text all shift with it. The other four are the status colors that tell the person what is happening with their data.
If your surfaces carry a tint instead of a flat white, set one more variable: --updog-ramp-light. The base surface and every light shade lean toward it, so pointing it at a warm off-white warms the whole light theme at once, the surfaces, the subtle fills, and the selected cell together. It is the lever dark mode uses to turn the palette over, and in a light theme you nudge it the same way without inverting anything. For a theme that reads as one tint, these three, --updog-brand, --updog-neutral, and --updog-ramp-light, often do the whole job, and the semantic layer below is optional. Set the named tokens when your surfaces and borders need different hues than a single tint can give.
The semantic layer, for an exact match
A real design system is rarely a clean tint of one gray. Your surfaces might be cool while your borders are warm, or your text softer than a generated shade would land. When you want the editor to match exactly rather than approximately, set the named tokens yourself instead of leaving them to the neutral.
:root {
/* Surfaces */
--updog-surface-layer0: #ffffff;
--updog-surface-layer1: #f7f7f8;
--updog-surface-layer2: #ededf0;
--updog-surface-layer3: #e0e0e4;
--updog-surface-layer4: #cfcfd6;
/* Borders */
--updog-border-primary: #e3e3e7;
--updog-border-secondary: #efeff1;
/* Content */
--updog-content-primary: #18181b;
--updog-content-secondary: #5c5c66;
--updog-content-tertiary: #8e8e96;
--updog-content-placeholder: #a8a8b0;
--updog-content-inverse: #ffffff;
/* Disabled state */
--updog-disabled-background: #f1f1f3;
--updog-disabled-border: #e3e3e7;
--updog-disabled-content: #b4b4bc;
} This is the layer that decides whether the editor reads as part of your app or merely near it. The surfaces are your backgrounds, from the flat base to the most raised panel. There are two borders: primary is the line you see almost everywhere, and secondary is the lighter line on dropdowns and a few inner edges. The content tokens are your text, from headings to placeholder hints. The brand and status tokens follow your base colors from the block above, so you do not repeat them here unless you want a specific shade.
The generated shades have names too, like --updog-gray-200 or --updog-brand-600, and you can override a single one if a step lands off. Most of the time you set the named tokens instead, which says what you mean rather than patching a number in a ramp. Dark mode is the one case where you work at the shade level, covered below.
Typography
The font is one variable, and it reaches everywhere. Set --updog-font-family and it applies across the React parts and inside the spreadsheet, where the cell text is drawn on a canvas. The grid reads the same value, so a million rows render in your font with no second setting to keep in sync.
:root {
--updog-font-family: "Inter", system-ui, sans-serif;
}
/* Only if your type scale differs from the defaults */
.updog-text--sm { font-size: 0.875rem; line-height: 1.5; }
.updog-text--md { font-size: 1rem; line-height: 1.5; }
.updog-text--lg { font-size: 1.25rem; line-height: 1.5; }
.updog-button-text--md { font-weight: 500; } The sizes are defined in rem, so they scale with the root font size of the page the editor sits in. If your app runs at a larger base size, the editor grows with it, and often you do not touch the sizes at all. When your type scale truly differs, the size and weight classes are there to override, as shown above.
The spreadsheet editor
The spreadsheet is the part people expect to be hard, because it is drawn on a canvas rather than page elements, and a canvas cannot read CSS on its own. So Updog reads your variables for it. When the editor mounts, it picks up every grid color from the page and paints with it, and it repaints when your theme changes. You write CSS, and the canvas obeys it like everything else.
The grid variables default to the same brand, surface, and status tokens you already set, so once the layers above are in place the spreadsheet editor is mostly matched. The block below is the full surface, for when you want to control the cell states directly: the edited cell, the invalid cell, the selection, the headers, and the colored bar that marks new, edited, error, and empty rows down the side of the grid.
:root {
/* Cells */
--updog-grid-cell-bg-idle: #ffffff;
--updog-grid-cell-bg-hover: #f7f7f8;
--updog-grid-cell-bg-dirty: #fff7e6;
--updog-grid-cell-bg-invalid: #fff0f0;
--updog-grid-cell-border-idle: #e3e3e7;
--updog-grid-cell-content-idle: #18181b;
--updog-grid-cell-content-cut: #5c5c66;
--updog-grid-cell-placeholder-idle: #a8a8b0;
/* Selection and focus */
--updog-grid-selection-bg: #eef4ff;
--updog-grid-selection-border: #2f6df6;
--updog-grid-focus-ring-border: #2f6df6;
/* Column headers */
--updog-grid-header-bg-idle: #f7f7f8;
--updog-grid-header-bg-active: #eef4ff;
--updog-grid-header-bg-selected: #dbe7ff;
--updog-grid-header-content-idle: #18181b;
--updog-grid-header-icon-idle: #5c5c66;
/* Row markers and the row status bar */
--updog-grid-marker-bg-idle: #f7f7f8;
--updog-grid-marker-bg-active: #eef4ff;
--updog-grid-marker-content-idle: #5c5c66;
--updog-grid-status-bar-bg-new: #b7e0c6;
--updog-grid-status-bar-bg-edited: #ffe1ad;
--updog-grid-status-bar-bg-error: #ffc9c9;
--updog-grid-status-bar-bg-empty: #d4d4d8;
/* Clipboard, fill handle, find matches, loading skeleton */
--updog-grid-clipboard-border-copy: #2f6df6;
--updog-grid-fill-handle-bg: #2f6df6;
--updog-grid-fill-handle-border: #ffffff;
--updog-grid-match-bg-default: #eef4ff;
--updog-grid-match-bg-current: #dbe7ff;
--updog-grid-skeleton-bg: #ededf0;
--updog-grid-skeleton-highlight: #f7f7f8;
} Shadows and scrollbars
Two smaller surfaces finish the match. The elevation tokens are the shadows under popovers and modals, and the scrollbar tokens style the thin scrollbars inside the editor. Set these to your own and even the edges of the editor belong to your product.
:root {
--elevation-1: 0 1px 2px rgba(0, 0, 0, 0.06), 0 4px 12px rgba(0, 0, 0, 0.08);
--elevation-2: 0 2px 4px rgba(0, 0, 0, 0.08), 0 8px 24px rgba(0, 0, 0, 0.16);
--updog-scrollbar-width: 0.375rem;
--updog-scrollbar-thumb: #cfcfd6;
--updog-scrollbar-thumb-hover: #b4b4bc;
} When a variable is not enough, target the class
The variables cover color and type, which is most of what a match needs. When you want something they do not reach, like the corner radius of your buttons, the exact padding of one toolbar, or a single button that should be a pill, every element in Updog carries a stable BEM class you can style directly. The components from the kit use a plain block name, such as .updog-button and .updog-input, with __element parts and --modifier states. The editor's own pieces use a prefixed block, such as .updog__data-editor-footer and .updog__aside-pane. Inspect the element to read its exact class.
/* Reach for a class only when no variable covers what you need. */
.updog-button--filled.updog-button--primary {
border-radius: 9999px;
}
.updog__data-editor-footer {
padding-inline: 1.5rem;
} Treat this as the escape hatch, not the main road. The variables are the contract Updog keeps stable across versions. The internal classes can change as the editor grows, so a rule pinned to one is more likely to need a touch later. Override the token when a token exists, and target the class when one does not.
Put the variables on the root
Define your variables on :root, not on a wrapper around the editor. Scoping them to your own class works for the editor itself, then breaks in a way that is hard to trace, because of the parts that float. Modals, dropdowns, context menus, and tooltips do not render inside the editor's box. They open in a portal at the end of the page, as siblings of your app rather than children of the editor. A selector scoped to your wrapper never reaches them, so the grid would carry your theme while the sheet picker and the right-click menu fell back to the defaults. Put the variables on :root and they reach the editor, the floating parts, and the canvas all at once.
This is also why the look is identical whether you embed Updog as a React component or as the <updog-editor> web component. Both render into the normal page rather than hiding behind a shadow root, so the same variables on :root style both the same way. You learn one method and it carries across.
Dark mode is yours to build
Updog does not ship a dark theme today. What it gives you is the means to build one, by setting variables again under your own dark selector. Flip that class on the page and the editor switches with it, the canvas grid included, because it watches the page for the change and repaints.
Work with the ramps, not the semantic tokens. First, point --updog-ramp-light at your dark base, so the base surface and every low shade you do not tune lean toward it and nothing strays light. Second, set the gray ramp. The surfaces, borders, content, disabled tokens, and the neutral tag all derive from gray shades, so tuning the ramp moves them together. The high grays carry text and must be light; the low grays you tune by hand for surface depth, since a flat mix toward one dark value leaves the panels too close to tell apart. Third, invert the colored ramps, pointing each pale tint at its dark counterpart so the button hovers, tag and tooltip backgrounds, selection, edited and invalid cells, matches, and skeletons all flip together.
.dark {
/* Dark base: surface-layer0 and every untuned low shade lean toward this.
Leave ramp-dark black so the -950 shades stay dark. */
--updog-ramp-light: #0f1115;
/* Gray ramp. Surfaces, borders, content, disabled, and the neutral tag all
derive from these. The high grays carry text and must be light; the low
grays are tuned for surface depth. */
--updog-gray-50: #161a20;
--updog-gray-100: #1e232b;
--updog-gray-200: #262c36;
--updog-gray-300: #2f3641;
--updog-gray-400: #5c6370;
--updog-gray-450: #5c6370;
--updog-gray-500: #6b7280;
--updog-gray-600: #a8aeb8;
--updog-gray-950: #e6e8eb;
/* Invert the colored ramps: subtle fills go dark, accents stay bright */
--updog-brand-50: var(--updog-brand-950);
--updog-brand-100: var(--updog-brand-900);
--updog-red-50: var(--updog-red-950);
--updog-yellow-50: var(--updog-yellow-950);
--updog-green-50: var(--updog-green-950);
--updog-blue-50: var(--updog-blue-950);
/* Keep the row status bars saturated so the strip stays visible */
--updog-grid-status-bar-bg-error: var(--updog-red-700);
} You cannot get dark mode by swapping a base color. The generated shades always run light to dark in the same direction, so a base swap only tints. Inverting the ramps is what turns the polarity around. And you do not rewrite every surface and text color by hand: those derive from the gray ramp, so you tune the ramp once and they follow.
Before you ship
- Put your overrides on
:root. Modals, dropdowns, and tooltips render in a portal at the end of the page, so a selector scoped to a wrapper around the editor will miss them. - Load your CSS after the Updog stylesheet. Both sit at the same specificity, so the file that comes later wins, and import order is what decides it.
- Inspect before you guess. Read the variable on the element you want to change and override that token. The names map to what you see.
- For dark mode, invert the colored ramps instead of swapping a base color, tune the gray ramp rather than each surface, and keep
--updog-content-inverselight so the text on brand-colored buttons does not flip.
Make it feel like yours
There is no Updog logo, no badge, and no powered-by line anywhere in the editor. That is on purpose. A white-label importer should leave your users looking at your product, not ours. Updog was always meant to blend into the data import flow you already built, so customer data onboarding feels like part of your app instead of a separate tool. The upload, the column mapping, and the spreadsheet all look like yours, and this guide is how you make that fit exact.