Skip to main content

Theme and Styling

This document covers the MUI theme construction pipeline, the six appearance presets, the style builder system, and where styling decisions should live.

For the concrete catalog of existing UI surfaces and primitives, see the Design system reference.

Semantic typography is owned by src/styles/textStyleBuilders.ts and consumed through src/components/text/Text.tsx. componentStyleBuilders.ts does not define canonical text roles; it only supplies surrounding layout, surfaces, and decorative motion helpers.

Theme construction pipeline

Appearance presets

Six named presets define the complete visual language for each appearance option:

PresetPersonalityPrimarySecondary
atlasBalanced technicalTealWarm amber
evergreenCalm editorial (default)GreenWarm terracotta
emberRicher copperCopperCool blue
solsticeCelestial warmthGolden amberTwilight violet
driftCoastal inspirationOcean blueCoral
graphiteRefined edgeCool slateRose-gold

Each preset carries:

AppAppearancePreset {
key, label, shortDescription
palette → Record<PaletteMode, AppAppearancePalette> // full light + dark palettes
typography → AppAppearanceTypography // body + heading font families
surface → Record<PaletteMode, AppSurfaceTreatment> // opacity, blur, glow tokens
motion → AppMotionTreatment // CSS animation flags + durations
}

The default preset is evergreen. Stored in localStorage under 'danhenderson-appearance'.

Surface treatment tokens

Each preset defines per-mode surface appearance control:

TokenWhat it controls
backgroundOverlayOpacityOpacity of the page background image overlay
cardGradientStartAlpha / cardGradientEndAlphaCard gradient transparency
cardBorderAlphaCard border transparency
cardShadowAlphaCard shadow intensity
cardBlurPxCard backdrop blur radius
panelSurfaceAlpha / panelBorderAlphaNested panel transparency
glowStrength / textGlowStrength / secondaryGlowStrengthPrimary and secondary glow intensities
sectionBottomGlowOpacityCV section bottom glow
sectionBorderSweepOpacityCV section animated border sweep
accentTintAlpha / secondaryTintAlphaTint wash overlays

Motion treatment tokens

CSS-only decorative animation control (distinct from Framer Motion):

TokenWhat it controls
tabHoverShimmerMsShimmer sweep duration on tab hover
pillPulseEnabled / pillPulseMsGlow pulse on pill-shaped chips
chipWaveEnabled / chipWaveMsBackground wave animation on chips
borderGlowEnabled / borderGlowMsBorder glow pulse
sectionBorderSweepEnabled / sectionBorderSweepMsCV section border gradient sweep
statusBreatheEnabled / statusBreatheMsStatus text breathing
headingBreatheEnabled / headingBreatheMsHeading text breathing

All of these are disabled when motionIntensity is off or subtle (via cssAnimations: false).

MUI theme augmentation

The standard MUI Theme interface is augmented in src/theme/mui.d.ts:

declare module '@mui/material/styles' {
interface Theme {
appearanceTreatment: AppResolvedTreatment;
}
}

This makes theme.appearanceTreatment available everywhere useTheme() is called, carrying:

  • .surface — resolved AppSurfaceTreatment for current mode + preset
  • .motion — resolved AppMotionTreatment for current preset + intensity
  • .motionScaleMotionScaleFactors for current intensity level

MUI component overrides

createAppTheme() sets these global MUI overrides:

ComponentKey overrides
MuiCssBaselineSmooth scrolling, font rendering, link colors
MuiAppBarBackdrop blur, alpha-blended background, subtle bottom border
MuiPaperBorder with divider color at 45% alpha
MuiButtonNo elevation, pill-shaped (borderRadius 999), outlined uses primary at 50% alpha
MuiChipPill-shaped (borderRadius 999)
MuiSpeedDial / MuiSpeedDialActionSurface tweaks with backdrop blur
MuiAccordionRounded corners, no ::before pseudo-element
MuiLinkHover underline

Typography scale

Typography uses fluid clamp() sizing from preset-defined font families:

  • h1h6: Responsive heading scale with preset heading fonts
  • body1/body2: Preset body fonts with defined sizes and line heights
  • overline: Themed heading font with increased letter-spacing
  • button: Weight 600, letter-spacing 0.02em

Style builder system

Two style builder modules compute theme-aware style objects, then expose them through memoized hooks:

Consuming style builders

Components consume via hooks that memoize the builder output:

// src/styles/componentStyles.ts
export const useComponentStyles = () => {
const theme = useTheme();
return useMemo(() => createComponentStyleMap(theme), [theme]);
};

// src/styles/appStyles.ts
export const useAppStyles = () => {
const theme = useTheme();
return useMemo(() => createAppStyleMap(theme), [theme]);
};

Components use them like:

const styles = useComponentStyles();
<Box sx={styles.contentCardSx}>...</Box>;

Key computed surfaces in componentStyleBuilders

SurfaceWhat it produces
contentCardSxCard background gradient + glow shadow + border + blur
cvSectionCardSxExtended card + bottom glow + border sweep animation
subtleSurfacePanel surface with reduced alpha
hoverShimmerSxTab hover shimmer ::after pseudo-element
pillPulseOverlaySxGlow pulse ::after for pill chips
borderGlowOverlaySxBorder glow animation
interactiveSurfaceSxButton-like hover with shadow

Key computed surfaces in appStyleBuilders

SurfaceWhat it produces
getBackgroundImageSx()Full-cover background with ::before overlay
backgroundShellSxTranslucent shell panel on background
headerToolbarSxResponsive header padding and height
photographyGridSxResponsive 1/2/3 column grid
pageFrameContainerSxRoute container padding
backToTopFabSxFixed FAB with blur and shadow

Motion timing tokens in componentStyleBuilders

The style builder also exports stagger timing constants, scaled by the current intensity:

TokenBase valueScaled by
itemStaggerMs120msstaggerFactor
sectionStaggerMs120msstaggerFactor
loadingPulseDurationMs1600msdurationFactor
loadingBarStaggerMs200msstaggerFactor

CSS animation keyframes

src/styles/animations.ts defines Emotion keyframes used for decorative CSS animations:

KeyframeVisual effect
shimmerSweepTranslucent highlight sweep at 90°
ambientPulseOpacity + scale fade with glow shadow
backgroundSweepGradient position slide for chips
breatheGentle opacity + Y-translate oscillation
loadingPulseSmooth opacity ramp (35% → 100%)
pulseRingExpanding ring with fade-out
cursorBlinkStep-based on/off for terminal cursor

These are conditionally applied based on AppMotionTreatment flags, which are disabled when cssAnimations: false.

Spring easing

src/styles/springEasing.ts exports the shared spring physics curve:

SPRING_EASING_CSS = 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
SPRING_EASING_MOTION = [0.175, 0.885, 0.32, 1.275]

This produces ease-out-back (slight overshoot and settle). It is used across:

  • MUI Zoom component easing
  • Framer Motion transition presets
  • Route transition easing
  • AnimatedContentCard entrance

Where styling should live

CategoryWhereExample
Palette colorsAppearance preset in appAppearance.tsprimary.main, secondary.main, background.paper
Typography scalecreateAppTheme()h1h6 sizes, body font families
Semantic text rolestextStyleBuilders.ts + Text.tsxsectionTitle, meta, inlineLabel, proseParagraph
Component defaultsMUI component overrides in createAppTheme()Button shape, chip shape, paper border
Surface treatmentscomponentStyleBuilders.ts → computed from theme.appearanceTreatment.surfaceCard gradients, glow shadows, border opacity
Page-level layoutappStyleBuilders.ts → consumed via useAppStyles()Background overlays, header styling, page containers
Motion timingcomponentStyleBuilders.ts → exposed as timing tokensStagger delays, animation durations
Component-local stylingInline sx prop using theme tokensOne-off spacing, conditional visibility

Where styling should NOT live

Anti-patternWhy it's wrong
Hardcoded hex colors in componentsBreaks theme consistency and appearance preset switching
Custom Typography styling for standard text rolesBypasses the semantic text primitive system
Ad hoc card insets/spacing not from style buildersCreates inconsistent spacing that doesn't scale with theme
CSS-in-JS style objects defined outside the builder systemMisses theme reactivity and treatment token changes
Motion durations as inline magic numbersBreaks intensity scaling and consistency

Current inconsistencies

  1. Some components use direct sx spacing where a style builder profile would be more consistent. This is pragmatic for one-off adjustments but should not be extended to new shared surfaces.

  2. The IDE chrome (src/components/ide/) maintains its own token file (vscodeTokens.ts) with hardcoded colors outside the theme system. This is intentional — the VS Code simulation needs to look like VS Code, not like the portfolio theme.

  3. Some shared components still carry decorative text-adjacent helpers from componentStyleBuilders.ts such as heading-breathe and status-breathe motion. Those helpers are valid only when they do not redefine the canonical role, tone, or context that already lives in textStyleBuilders.ts.

Further reading