Skip to main content

Styling and theming

The app is styled with Chakra UI v3 — a token-driven system, not the v2 extendTheme API. Everything themed flows from one file: frontend/src/ui/theme.ts.

The Chakra v3 system

theme.ts builds the system with createSystem(defaultConfig, defineConfig({...})) and main.tsx mounts it via <ChakraProvider value={system}>. What it defines:

  • Brand palette — a 10-step navy ramp with the MotorPH corporate blue #1A3470 at brand.800 (headers, primary buttons, active nav) and #122550 at brand.900; accent tokens accent.red/yellow/green. Full brand rationale and logo usage live in ../reference/logo.md.
  • FontsInter for body and headings, Outfit Variable as the display font (marketing headlines, the eyebrow text style). Both are self-hosted via @fontsource imports in main.tsx — no external font CDN.
  • Semantic tokens with _light/_dark conditionalsfg, fg.muted, border, and focusRing resolve per color mode, so components using them adapt to dark mode with no extra code.
  • Recipes — a button recipe restyling the four variants (solid/subtle/outline/ghost) onto the brand ramp, and an input recipe (radius md). This is why a bare <Button> looks on-brand without props.
  • Custom radii scale, card/cardHover shadows plus a deeper floating shadow for marketing surfaces (see the comment in the file), and the eyebrow text style.

A minimal global reset plus a WCAG touch-target rule (44×44 px minimum on coarse pointers) lives in index.css.

Dark mode

Color mode is handled by next-themes: ui/components/color-mode.tsx wraps ThemeProvider with attribute="class" and defaultTheme="light", and color-mode-toggle.tsx flips it with setTheme. Components adapt three ways, in order of preference:

  1. use semantic tokens (fg, fg.muted, border, bg) that already resolve per mode;
  2. _dark style conditionals, e.g. color={{ base: 'brand.800', _dark: 'brand.100' }} (see app-layout.tsx);
  3. imperative checks via useTheme() where whole style clusters diverge (const isDark = theme === 'dark', same file).

Chakra v3 gotchas as practiced here

Chakra v3 components are compositional (explicit Root/Trigger/ Positioner/Content parts). Two hard-won rules from this codebase:

  • Menus and tooltips must be composed with Portal + Positioner, or they render clipped. The canonical example is the grid row-actions kebab in ServerDataGrid.tsx (RowActionsCell): MenuRoot → MenuTrigger → Portal → MenuPositioner → MenuContent. The trigger sits inside AG Grid's overflow-clipped cell containers; without the Portal, the popup is cut off at the row edge. The same composition is used everywhere a popup lives near clipping/stacking contexts — the profile menu, pagination page-size menu, grid View/Table menus.
  • Keep the same Tooltip.Root tree mounted; don't swap it in and out. NavTooltip in app-layout.tsx always renders Tooltip.Root and disables it with the disabled prop when the sidebar is expanded. As its comment notes, conditionally rendering a bare fragment vs. the tooltip would remount every nav item on each sidebar toggle — visible flicker and lost state.

AG Grid theming

Grids don't use Chakra styling; they use AG Grid's theming API, parameterized to match the brand: ui/ag-grid/theme.ts exports motorphGridTheme = themeQuartz.withParams({...}) with accentColor: '#1A3470', header background #E9EEF8 (= brand.50), Inter at 14 px, and 44 px row/header heights (the density system overrides heights per page — ag-grid.md). The grid PDF exporter's header fill ([26, 52, 112] in pdf.ts) is the same brand navy, so exports visually match the grids.

Theming the ALTCHA widget

The login CAPTCHA is a web component that themes itself via CSS custom properties. ui/components/altcha-captcha.tsx maps those onto Chakra's CSS variables —

'--altcha-color-base': 'var(--chakra-colors-bg-panel)',
'--altcha-color-base-content': 'var(--chakra-colors-fg)',
'--altcha-border-color': 'var(--chakra-colors-border)',
'--altcha-color-primary': 'var(--chakra-colors-brand-600)',

— so, as the inline comment puts it, the widget stays correct in both color modes for free: the Chakra variables it points at are the semantic tokens that already flip with the theme.