Web UI Foundation
Developer reference for GlycemicGPT web themes, semantic tokens, typography, icons, and UI primitives.
Web UI Foundation
This page documents the shared frontend foundation for apps/web. It is meant for contributors and maintainers who need to build UI that survives theme changes without rewriting component internals.
This is not a full product design system. It is the current foundation: semantic colors, class driven themes, font roles, sprite icons, base primitives, product components, and the checks that keep those pieces reliable.
Ownership Map
| Area | Canonical file |
|---|---|
| Raw shared color values | apps/web/src/styles/config/colors.css |
| Semantic theme variables and mode mappings | apps/web/src/styles/config/theme.css |
| Legacy compatibility variables | apps/web/src/styles/config/legacy-theme.css |
| Class driven variants | apps/web/src/styles/config/custom-variants.css |
| Font families and role utilities | apps/web/src/styles/config/fonts.css |
| Shared radius tokens | apps/web/src/styles/config/radius.css |
| Shared animations | apps/web/src/styles/config/animations.css |
| Global element defaults | apps/web/src/styles/base.css |
| Base primitives | apps/web/src/base |
| Product components | apps/web/src/components |
| Icon sprite | apps/web/public/static_assets/iconSprite.svg |
| Icon names, titles, and sizes | apps/web/src/base/Icon/iconConfig.ts |
| Class composition | apps/web/src/lib/ui/twMerge.ts |
| Runtime theme choices | apps/web/src/providers/theme-config.ts |
Global styling enters through apps/web/src/app/globals.css. Keep the import order stable:
- Tailwind CSS theme, preflight, and utilities.
- Raw color tokens from
colors.css. - Class driven variants from
custom-variants.css. - Font utilities from
fonts.css. - Shared radius tokens from
radius.css. - Shared animations from
animations.css. - Legacy compatibility variables from
legacy-theme.css. - Semantic theme variables from
theme.css. - Global base rules from
base.css.
Theme Model
Raw colors and semantic theme variables are separate on purpose.
A semantic token is a named design role. It says what a color is for, not what color it is. For example, --color-accent means "the current action color". It does not mean blue, yellow, green, or any other fixed hue.
That indirection lets components stay stable while themes change underneath them. A button can keep using bg-accent text-accent-foreground, and each theme decides which raw colors those roles map to.
Example flow:
/* colors.css */
:root {
--color-base-accent-blue: #5eb1ff;
--color-base-ink: #191919;
--color-base-cool-silver: #f7f9fb;
}
/* theme.css */
:root {
--color-accent: var(--color-base-accent-blue);
--color-accent-foreground: var(--color-base-ink);
}
.theme-dark {
--color-accent: var(--color-base-accent-blue);
--color-accent-foreground: var(--color-base-ink);
}
.theme-seasonal {
--color-accent: var(--color-base-cool-silver);
--color-accent-foreground: var(--color-base-ink);
}<button className="bg-accent text-accent-foreground">
Save changes
</button>The component only knows that it needs the accent background and the approved foreground for text placed on that background. It does not know or care which palette value the current theme uses.
- Add raw palette values as
--color-base-*tokens incolors.css. - Map raw tokens to semantic variables in
theme.css. - Reassign the same semantic variables inside theme classes such as
.theme-dark. - Use Tailwind utilities that reference semantic variables inside components.
Components should describe intent, not raw color. Use classes such as bg-surface-page, bg-surface-primary, text-foreground-primary, text-foreground-secondary, bg-accent, text-accent-foreground, border-border-default, text-signal-error-text, and bg-signal-check-fill.
Do not use raw palette classes such as text-slate-500, bg-blue-600, or hover:bg-red-700 in new shared UI.
Light and dark mode are class driven. New semantic theme mode is enabled by adding a theme-* class to a wrapper. During the migration, the app root also keeps legacy light and dark classes so existing dark:* utilities continue to work. Do not use prefers-color-scheme in CSS for theme decisions.
Semantic Color Families
Semantic variables describe usage, not literal hue.
| Family | Purpose | Example utilities |
|---|---|---|
surface-* | Page backgrounds, panels, raised areas, inverse regions | bg-surface-page, bg-surface-primary, bg-surface-secondary |
foreground-* | Text and icons placed on approved surfaces | text-foreground-primary, text-foreground-secondary, text-foreground-muted |
accent-* | Primary action color and text placed on accent backgrounds | bg-accent, hover:bg-accent-hover, text-accent-foreground |
border-* | Outlines, dividers, hover borders, focus borders | border-border-default, ring-border-active |
signal-*-fill | Status fills, indicators, charts, diagrams, and badges | bg-signal-check-fill, bg-signal-warning-fill |
signal-*-text | Signal colored text and icons on neutral surfaces | text-signal-error-text, text-signal-info-text |
overlay-* | Scrims above app surfaces | bg-overlay-primary |
Avoid vague names such as ui-primary, ui-secondary, and ui-background. If a new shared meaning is needed, add one semantic token and give it matching assignments for every supported theme.
Color Accessibility
Color pairings must follow the Color Accessibility Guide. The current guide defines approved combinations for light, dark, and seasonal theme modes.
Normal text needs AA contrast, non text UI needs enough contrast to identify controls and state, and color must not be the only way to communicate medical or product status.
Useful WCAG references:
- WCAG 2.2 Conformance
- WCAG 1.4.3 Contrast Minimum
- WCAG 1.4.11 Non Text Contrast
- WCAG 1.4.1 Use of Color
- WCAG 2.4.7 Focus Visible
- WCAG 2.4.13 Focus Appearance
Baseline rules:
- Normal text needs at least
4.5:1contrast. - Large text needs at least
3:1contrast. - Important icons, borders, selected states, focus indicators, and controls need at least
3:1contrast against adjacent colors. - High contrast mode is an enhancement. It is not permission to ship failures in the default theme.
- Do not rely on color alone for state, status, or medical meaning.
- If a pairing is not documented, calculate contrast before using it.
Accent rules:
- Use
bg-accent text-accent-foregroundfor selected tabs, filled primary controls, checked controls, and highlight surfaces with text inside. - Do not put white text on
bg-accent. - Use
text-foreground-secondaryfor secondary paragraphs and labels onbg-surface-pageorbg-surface-primary. - Use
text-accentonly for short emphasis on approved neutral surfaces.
Typography
Typography utilities live in apps/web/src/styles/config/fonts.css.
Use complete role utilities instead of repeated font family, size, weight, line height, and spacing stacks:
font_header_1font_header_2font_header_3font_header_4font_body_1font_body_2font_body_3font_body_4font_metric_labelfont_metric_caption
Font families:
| Role | Family | Utility |
|---|---|---|
| Primary UI text | Poppins | font_poppins |
| Labels and compact metric text | JetBrains Mono | font_jetbrains_mono |
Font delivery rules:
- Register app fonts with
next/font/local, notnext/font/google. - Keep font files in
apps/web/src/app/fontsso builds do not depend on Google Fonts or CDN reachability. - Keep Inter as the app default through
--font-interand--font-sansunless the product intentionally changes the global app font. - Scope Poppins and JetBrains Mono through CSS variables and role utilities until the broader app is intentionally migrated.
- Register variable fonts with an explicit weight range so Next.js generates real variable font CSS instead of synthetic weights.
- Keep the matching SIL OFL 1.1 license files beside the redistributed font files.
Role utilities should define font family, size, weight, line height, and letter spacing. Do not usually combine role utilities with font_regular, font_bold, font_normal, or font_medium. If a repeated visual style needs that, add or adjust a role utility instead.
Font utilities should use rem units so text follows browser text size settings.
Useful typography accessibility references:
Radius
Shared radius tokens live in apps/web/src/styles/config/radius.css.
Use radius tokens as opt in utilities in components, such as rounded-button. Do not apply radius tokens globally to raw elements. A token should make a radius reusable, not silently restyle every matching element in the app.
Class Composition
Use twMerge from apps/web/src/lib/ui/twMerge.ts for dynamic class composition.
Do not call clsx, classnames, or tailwind-merge directly inside components. The local wrapper understands project font classes, so it can normalize conflicts such as font_header_1 followed by font_body_1.
When adding a local utility class group that can conflict with itself, update twMerge.ts and add focused tests.
Components
Base primitives live in apps/web/src/base. They provide accessible structure, stable typing, and default behavior. They may own minimal structural styling when it supports the primitive contract, such as layout reset classes, intrinsic sizing, hidden input mechanics, or sprite rendering defaults. They may also accept className props so composed components can apply semantic classes.
Base primitives should not own product appearance. Keep semantic colors, rich visual states, typography roles, branded treatments, and heavy layout styling in apps/web/src/components.
Current primitives:
Buttonowns the accessible button shell and defaulttype="button"behavior.Inputowns the shared input shell.Iconowns sprite based icon rendering.
Product components live in apps/web/src/components and compose base primitives with semantic classes. Current examples include PrimaryButton, SecondaryButton, HighlightButton, TextInput, and Checkbox.
Reusable component folders should follow the colocated pattern:
Component.tsxowns rendering.Component.types.tsorcomponent.types.tsowns props and exported types.Component.test.tsxorComponent.spec.tsxowns tests.index.tsowns public exports.- Component CSS stays beside the component only when the component owns local theme variables.
Keep base components thin and visually neutral. Minimal structural classes are acceptable in apps/web/src/base. Semantic color classes, product level visual states, branded styling, and product behavior belong in apps/web/src/components.
Component Accessibility
Components must support keyboard use, visible focus, correct names, roles, values, and labels.
Rules:
- Icon only controls need an accessible name through visible text,
aria-label, oraria-labelledby. - Interactive controls need visible focus through semantic border or ring tokens.
- Disabled states must remain identifiable without relying only on color.
- Form inputs need clear labels and useful error text.
- Status and medical meaning must use text, icon shape, or labels in addition to color.
Useful component accessibility references:
- WCAG 2.1.1 Keyboard
- WCAG 2.4.7 Focus Visible
- WCAG 4.1.2 Name, Role, Value
- WCAG 3.3.2 Labels or Instructions
Icons
Shared icons use the SVG sprite at apps/web/public/static_assets/iconSprite.svg.
Use Icon from apps/web/src/base/Icon when a symbol exists in the sprite. Register every symbol in apps/web/src/base/Icon/iconConfig.ts with a default title and size. IconName is derived from that config.
Icon rules:
- Add only icons required by current work.
- Keep sprite symbols minimal and reusable.
- Use
currentColorin sprite paths so icons follow text color utilities. - Let the configured
titleandsizebe the default for shared icons. - Use the
titleprop only when visible context needs a more specific accessible name. - Use
decorativewhen an icon is purely visual and should be hidden from assistive technology. - Do not duplicate inline SVG markup in components when a shared sprite symbol exists.
Current icon sources:
Useful icon accessibility references:
Adding A Theme
Follow this path for a new semantic theme:
- Add raw palette values to
colors.css. - Reassign existing semantic tokens in a new
.theme-*block intheme.css. - Add the selectable theme to
theme-config.ts. - Set
semanticClassto the new class without the dot. - Set
legacyClassto onlylightordark. - Update color accessibility documentation with approved pairings.
- Run
npm run validate:theme-colorsfromapps/web. - Run focused provider tests,
npm run typecheck, and the checks for the changed surface.
Do not create legacy classes for new semantic themes. Do not create per component color variable families such as --background-color-primary-button or --color-primary-button.
Tests
Every base component added to apps/web/src/base needs colocated unit tests.
Test for:
- Meaningful rendering contracts.
- Accessible attributes and labels.
- State behavior.
- Emitted events.
- Class composition behavior when it matters.
Avoid low value snapshot tests. Prefer direct assertions for roles, attributes, callbacks, and sprite references.
Before considering UI foundation work done, run these commands from apps/web:
npm test
npm run typecheck
npm run build