This document collects GeoView-specific conventions for styling, CSS, and theming. See best-practices.md for general coding conventions.
Keep non-trivial component styles in a companion *-style.ts module. This separates the component’s
rendering and interaction logic from its visual presentation and makes theme-dependent styles reusable.
Export getSxClasses(theme): SxStyles from the style module, with JSDoc describing the theme
parameter and returned styles. Import SxStyles from @/ui/style/types. In the component, use:
const memoSxClasses = useMemo((): SxStyles => getSxClasses(theme), [theme]);
This standardized style memo needs neither JSDoc nor trace logging. Combine conditional styles with
the MUI array syntax, for example [memoSxClasses.base, condition && memoSxClasses.extra] as SxProps.
Use geoViewColor and geoViewFontSize tokens rather than hard-coded colors and font sizes.
Spacing and border radius have their own conventions in the sections below.
Reusable @/ui and core/components/ui components may receive a standard external MUI theme,
which does not contain GeoView’s custom extensions. Access those extensions with optional chaining,
for example theme.palette.geoViewColor?.primary.main. Apply the same protection to other
non-standard extensions rather than assuming every caller supplies a GeoView theme.
Import UI components and icons from the @/ui barrel. Only MUI hooks, utilities, and types are
imported directly from MUI. GeoView wrappers provide additional behavior: for example, IconButton
requires aria-label, uses it as the fallback tooltip, and exposes additional tooltip, ref, and
visibility props. Inspect the wrapper rather than assuming it has the same interface as MUI.
All user-visible text and labels use t('domain.key'); add each key to both en/fr translation files.
Namespaces are case-sensitive (for example appbar, mapctrl, dataTable) and interpolation uses
i18next ``. Reuse existing keys before adding new ones, and verify meaningful French text.
For interaction semantics and focus behavior, see the accessibility guide.
A display theme is more than a palette: configuration validation, UI selectors, and documentation must all recognize the same theme name. When adding or renaming a theme:
TypeDisplayTheme, VALID_DISPLAY_THEME, the schema enum, and getThemeOptions.defaultGeoViewColors before palette overrides. If the theme has a custom font, pass it
to generateThemeOptions so base typography and heading fonts agree.rt-18-global-settings.html instead of replacing an existing
one. Renumber each following map’s ID, heading, and snippet target together, and update the
corresponding map numbers in the release-testing plan.theme.spacing() in style modules, no sx shorthandGeoView uses MUI’s default 8px spacing scale: theme.spacing(n) = n × 8px. There is no custom spacing scale override today; if this ever changes, update the conversion guidance in Section 6 accordingly, since its tie-break rules are derived from the 8px assumption.
In-scope properties: padding/margin and all directional variants (paddingTop, mx, etc.), gap, rowGap, columnGap.
Rule:
*-style.ts files (getSxClasses(theme), MUI styleOverrides, styled()): always use explicit theme.spacing() with the full CSS property name. Never use sx shorthand aliases (p, m, gap, pt, mx, etc.) in these files.// ✅ Good
padding: theme.spacing(2),
gap: theme.spacing(1),
margin: theme.spacing(0.5, 1, 0.25, 0.75),
// ❌ Bad — shorthand inside a *-style.ts file
p: 2,
gap: 1,
No bare-zero exception. Even a zero value must go through theme.spacing(0) rather than a bare 0/'0px'. The rendered result is identical either way, but this keeps theme.spacing( a complete, unambiguous grep target for every spacing declaration in the codebase — no exceptions to remember.
// ✅ Good
margin: theme.spacing(0),
// ❌ Bad — bare zero breaks the "always theme.spacing()" invariant
margin: 0,
margin: '0px',
sx props in JSX: shorthand is acceptable there, but per best-practices.md inline styles should generally be minimized — prefer moving styles into a *-style.ts module using explicit theme.spacing() instead of reaching for inline sx.Why:
theme.spacing( is a single, unambiguous grep target for every spacing value in the codebase. Shorthand aliases require matching against a fixed list of MUI spacing-aware keys (p, m, px, py, pt, pr, pb, pl, mx, my, mt, mr, mb, ml, gap, rowGap, columnGap), which is easy to get wrong and harder to mass find/replace later.sx shorthand has no way to express a single declaration with distinct top/right/bottom/left values (CSS’s 2/3/4-value shorthand). Only the full property name with theme.spacing(t, r, b, l) supports this — e.g. padding: theme.spacing(0.5, 1, 0.25, 0.75).Out of scope for spacing-scale conversion: border-radius (follow §7 for radius values), border-width, fixed control width/height, icon/graphic sizing, focus-indicator metrics, map-canvas/OpenLayers overlay position offsets (top/left/right/bottom used for absolute positioning).
rem/em-based spacing should be converted too. Treat an existing rem/em value on an in-scope property (padding, margin, gap, etc.) as a legacy inconsistency, not an intentional exception — convert it to theme.spacing() like any other value, using the px-equivalent at the default root font size (16px) to pick the nearest factor. Only keep a value in rem/em when it’s deliberately coupled to font-size (e.g. spacing meant to scale with adjacent text for WCAG text-resize behavior), and mark that case with a one-line comment explaining why it’s exempt.
Always prefer a standard scale factor. New and edited code should use values that land exactly on the 8px grid — 0.25(2px), 0.5(4px), 0.75(6px), 1(8px), 1.25(10px), 1.5(12px), 1.75(14px), 2(16px), 2.25(18px), 2.5(20px), 2.75(22px), 3(24px), 3.75(30px), 4(32px), etc. Any value expressible in exact 2px increments (a 0.25 factor step) converts exactly and requires no judgment call (e.g. 10px → theme.spacing(1.25), 20px → theme.spacing(2.5)).
Whole, half, and quarter factors: factors that are multiples of 1 or 0.5 (e.g. 1, 1.5, 2, 3) are called “whole” or “half” factors; factors that only land on a multiple of 0.25 (e.g. 1.25, 1.75, 3.25) are called “quarter” factors. This distinction matters for the tie-break rule below.
Off-scale values are the exception, not the default. They should only occur when converting a pre-existing legacy value that doesn’t divide evenly by 2px — never introduce a new off-scale value by choice. When snapping a legacy off-scale value (e.g. 5px, 15px, 25px) onto the scale:
15px is equidistant between theme.spacing(1.75) (14px) and theme.spacing(2) (16px); snap to theme.spacing(2). Similarly 25px → theme.spacing(3) (24px), not theme.spacing(3.25) (26px).// ✅ Good — snapped value documented
marginTop: theme.spacing(2), // snapped from 15px
Do not silently guess on a tie. If a snap decision affects a value used in more than one place (a recurring rhythm — for example, the same 15px legacy value appearing in both a card header and a panel header), confirm the direction once and apply it consistently across the affected files rather than deciding file-by-file.
theme.shapeUse one of the five border-radius tokens. Never use a literal px/% value or a bare number.
The examples below are guidance rather than fixed mappings; choose a token based on the visual context and consistency with surrounding components.
| Token | Value | Example uses |
|---|---|---|
borderRadiusNone |
0 |
Squared-off corners, e.g. where a surface attaches to another (tabs on panels) |
borderRadiusSm |
4px |
Smaller elements, e.g. map controls, chips, thumbnails, icons, scrollbar thumbs, inline code |
borderRadiusMd |
6px |
Common surfaces, e.g. cards, panels, popovers, tooltips, inputs (same as the MUI base) |
borderRadiusLg |
8px |
Larger grouped containers, e.g. settings sections |
borderRadiusFull |
9999px |
Circles (square elements) and pills (rectangular elements) |
Where each kind of file gets the token:
getSxClasses(theme)), plus inline sx with a theme: use theme.shape.borderRadiusMd.@/ui components, theme.components overrides and cssText strings: use GEOVIEW_SHAPE.borderRadiusMd from @/ui/style/default. @/ui components can run under an external theme without the GeoView tokens. Plugins import it from geoview-core/ui/style/default./* = borderRadiusSm */ comment.`${theme.shape.borderRadiusMd} ${theme.shape.borderRadiusMd} ${theme.shape.borderRadiusNone} ${theme.shape.borderRadiusNone}`.// ❌ Bad
borderRadius: '5px',
borderRadius: 1, // in sx this is multiplied by the base: 1 → 6px, 2 → 12px
borderRadius: theme.shape.borderRadius, // a number, so in sx it renders 6 × 6 = 36px
// ✅ Good
borderRadius: theme.shape.borderRadiusMd,
Prefer central defaults. MUI’s own components (Button, Paper, Dialog, Menu, inputs, Tooltip and others) already read the numeric base theme.shape.borderRadius (6), so don’t re-declare a radius on them. When a whole component family needs a different radius, change it once in theme.components (themeOptionsGenerator.ts) instead of at each call site.
Off-scale legacy values move to the nearest token: 5px goes to borderRadiusMd, and anything else is decided case by case. Keep theme.shape.borderRadius (the base) as a number — MUI components such as Skeleton do math on it.
Use the focus selector that matches the element receiving focus:
ButtonBase-derived controls (for example, Button, IconButton, and ListItemButton): use &.Mui-focusVisible.&:focus-visible.&:has(.Mui-focusVisible) for a MUI child or &:has(:focus-visible) for a native/custom child.Let the theme provide the standard indicator for MUI controls. For custom controls or composite wrappers that need to draw an indicator, use getFocusIndicatorStyles() from themeOptionsGenerator.ts. Keep local overrides limited to layout needs such as inset placement or suppressing a redundant halo; do not restate the standard ring’s color, width, or halo locally.
When an inset offset equals the outline width, use the existing FOCUS_OUTLINE_WIDTH constant (for example, outlineOffset: `-${FOCUS_OUTLINE_WIDTH}`) rather than repeating its pixel value. Keep distinct one-off offsets literal; do not add tokens for them. Use boxShadow: 'none' directly when the halo must be suppressed.
Form controls show their focus outline in keyboard navigation mode. Keep indicators visible when controls are clipped or inset, and preserve fallback indicators for external themes. See the accessibility guide for user-facing focus behavior.