ADR-006: Shadcn + Dark Theme First
Status: Accepted Date: February 2026 Decision makers: SALLY Engineering Team
Context
SALLY’s frontend needs a consistent, accessible UI component library. Dispatchers often work in low-light environments (early morning, late night shifts), making dark theme support a practical requirement rather than a cosmetic preference. The team evaluated three approaches:
- Material UI (MUI) — Comprehensive component library with theming support. Installed as a package dependency. Customization requires overriding the theme object and dealing with CSS-in-JS specificity issues.
- Radix UI + custom styling — Headless, accessible primitives with no built-in styling. Full control but requires building every component’s visual layer from scratch.
- Shadcn/ui — Pre-built, accessible components based on Radix UI primitives, styled with Tailwind CSS. Components are copied into the project (not installed as a package dependency), giving full ownership and customization control.
MUI’s package-based approach would create a dependency on their theming system and make it harder to enforce SALLY’s strict design constraints (grayscale-only palette, specific dark theme behavior). Radix alone would require too much upfront investment in building the visual layer. Shadcn/ui provides the middle ground: production-ready components that the team owns and can modify freely.
The team also decided that dark theme should not be an afterthought. Adding dark mode after building a light-mode UI leads to inconsistencies and missed styles. Instead, dark theme support should be mandatory from the first component.
Decision
Use Shadcn/ui components with a mandatory dark theme-first approach. The specific conventions are:
Component library:
- Shadcn/ui components are copied into
src/components/ui/and become part of the codebase. They are not a package dependency. - New components are added with
pnpm dlx shadcn@latest add [component-name]. - All UI elements must use Shadcn components instead of plain HTML. For example,
<Button>instead of<button>,<Card>instead of<div className="border rounded-lg">.
Color palette:
- The UI uses only black, white, and gray shades. No brand colors, no accent colors.
- Status indicators (red for critical, yellow for warning, green for success, blue for info) are the only exception, and they must include dark mode variants.
- This restriction keeps the interface clean and professional, and reduces the surface area for dark theme inconsistencies.
Semantic color tokens:
- Backgrounds use
bg-background,bg-card,bg-mutedinstead of hardcoded values likebg-white. - Text uses
text-foreground,text-muted-foregroundinstead oftext-gray-900. - Borders use
border-borderinstead ofborder-gray-200. - These tokens are defined as CSS variables in
globals.cssand automatically switch values between light and dark themes via thenext-themesprovider.
Mandatory dark mode classes:
- If a component uses a light-mode-only class (e.g.,
bg-gray-50), it must include the dark variant (e.g.,bg-gray-50 dark:bg-gray-900). - Inverted elements (black background in light mode) must invert in dark mode:
bg-black dark:bg-white text-white dark:text-black.
Theme management:
next-themeshandles theme switching and persistence.- The theme is stored in
localStorageand respects the system preference by default.
Consequences
What became easier:
- Dark theme works everywhere from day one. There is no “dark theme retrofit” project because every component is built with both themes from the start.
- The grayscale palette eliminates color decision fatigue. Developers do not need to choose between shades of blue or debate brand color placement. The UI is visually consistent without a dedicated design system team.
- Full ownership of components means the team can modify any component’s behavior, accessibility, or styling without waiting for an upstream package release or working around library limitations.
- Semantic tokens make theming declarative. A developer writes
bg-backgroundonce, and it works in both themes without conditional logic. - Adding a new Shadcn component is a one-command operation that generates a file the team owns.
What became harder:
- Developers must learn and follow strict conventions. Using
bg-whiteinstead ofbg-backgroundis a code review rejection. This requires training and vigilance. - Shadcn components are copied, not installed. When the upstream Shadcn project releases improvements (accessibility fixes, new variants), the team must manually review and apply those changes. There is no automatic update path.
- The grayscale restriction limits visual expressiveness. Some UI patterns (colored navigation, branded headers, data visualizations with color encoding) require explicit exceptions to the palette rule.
- Every new UI component must be tested in both light and dark themes at three breakpoints (mobile, tablet, desktop). This doubles the visual QA surface area compared to a light-theme-only approach.
- Developers unfamiliar with Tailwind’s dark mode variant (
dark:) syntax need time to learn the patterns.