Back to Super Productivity

Styling Guide

docs/styling-guide.md

18.20.115.1 KB
Original Source

Styling Guide

Rules

  • All visual styling must use CSS variables from src/styles/_css-variables.scss — never hardcode colors, spacing, shadows, transitions, or z-index.
  • Positioning/layout is fine as plain CSS — flexbox, grid, display, position, dimensions.
  • Check src/app/ui/ first before creating new styled elements — 40+ reusable components exist.
  • Component SCSS should be minimal — shared styles belong in src/styles/components/ or as a mixin.
  • Material overlay components (menus, dialogs, tooltips) render outside component scope — style them in src/styles/components/ and add a comment in the component pointing there.
  • Only reference custom properties that can reach the rule — a var(--x) whose --x is undeclared, or declared only inside some component's :host, silently voids the whole declaration. Enforced across SCSS, theme CSS and inline styles:/[ngStyle] by npm run lint:css-vars (tools/check-css-vars.js), which explains the fix when it fails.

Anti-Patterns

AvoidDo Instead
Hardcoded colors (#fff, red)CSS variables (--text-color, --card-bg, --color-danger)
Hardcoded spacing (16px, 1rem)Spacing variables (--s2, --s, --s-half)
Hardcoded shadowsElevation variables (--whiteframe-shadow-*dp, --md-sys-level*)
Hardcoded transitions/durationsTransition variables (--transition-standard, --transition-duration-*)
Custom z-index valuesZ-index variables (--z-main-header, --z-backdrop, etc.)
New styled elements without checkingCheck src/app/ui/ for existing reusable components first

Dialog action buttons

One treatment per role so dialogs feel consistent. Classify by function, not label ("Close" that dismisses a read-only view is secondary; "Close" that commits is primary).

RoleTreatmentNotes
Primary / confirmingmat-flat-button color="primary"Save, OK, Submit, Create, Schedule — the main affirmative action
Cancel / dismiss / secondarymat-button (no color)De-emphasized text button
Destructivecolor="warn" (keep the variant)Delete, Remove, Unschedule — never folded into primary
Genuine alternative (not cancel)mat-stroked-buttone.g. "Skip instance", "Configure" next to a primary action

Rules:

  • Icons: drop generic check/close icons on OK/Cancel/Save/Submit — they add nothing. Keep icons that carry meaning (alarm/today/event_busy in scheduling, wb_sunny, save, cloud_upload, delete_forever).
  • No color on cancel/close — leftover color="primary" on a Cancel just tints it; remove it.
  • No dead classes — the legacy Bootstrap btn btn-primary classes are gone; don't reintroduce them. submit-button is only styled inside dialog-create-tag.
  • Symmetric choice dialogs (e.g. sync "use remote" vs "use local") may use two matched mat-stroked-buttons — there is no single primary.

Callouts (info / warning / danger / success)

Tinted message boxes inside dialogs and config panes use the global .callout class from src/styles/components/_callout.scss — never a local .warning-box clone with its own rgba(255, 152, 0, …), which is invisible to the 15 themes.

html
<div class="callout callout--warning">
  <mat-icon aria-hidden="true">warning</mat-icon>
  <p>If you lose this password your synced data cannot be recovered.</p>
</div>
ModifierTone tokenUse for
(none)/--info--c-primaryNeutral context, "what will happen" notes
--success--color-successConfirmation, savings, "nothing to do"
--warning--color-warningIrreversible-but-intended actions
--danger--color-dangerDestructive actions, failures

One tone token (--callout-c) drives icon color, border and fill, the latter two via color-mix() — so every tone is theme-aware and a new tone is one declaration. Outer spacing stays with the consumer (.callout { margin-bottom: var(--s2); } in the dialog's own SCSS), the primitive owns no margin.

The icon is optional, but the copy must be a single child element — one <p>, or a <div> wrapping several. .callout is a flex row, so putting the class on a <p> and leaving the text bare turns every inline child into its own flex item: <strong>Note:</strong> … renders as two columns with a gap and a hanging indent on the wrap.

Sibling: .info-panel (_info-panel.scss) is the same idea for formly-generated markup, where no icon element can be added and the glyph has to come from a ::before.

Typography Scale

VariableValueVariableValue
--font-size-xs11px--font-size-xl18px
--font-size-sm12px--font-size-2xl22px
--font-size-md14px--font-size-3xl28px
--font-size-lg16px

Text sizes go through the scale; snap an off-scale value to the nearest step. Companion tokens: --font-weight-medium/-semibold/-bold, --line-height-tight/-snug/-normal, --font-mono-stack.

Two deliberate exceptions — leave these as plain px/em:

  • Material icon glyph sizes written as a matched set — font-size, width and height all 20px. The glyph must equal its box or it decenters, and the scale has no 20/24px step. See main-header.component.scss and the .tab-icon rules in config-page.component.scss.
  • Deliberately proportional em sizes that track their parent (font-size: 1em on an inline icon).

Key Files

FilePurpose
src/styles/_css-variables.scssAll CSS custom properties (design tokens)
src/styles/themes.scssMaterial theme setup + utility classes
src/styles/page.scssGlobal page/body styles
src/styles/util.scssUtility classes
src/styles/components/Global component styles (Material overrides, shared patterns)
src/styles/mixins/Reusable SCSS mixins
src/app/ui/40+ reusable Angular UI components

Spacing Variables (8px Grid)

VariableValueVariableValue
--s-quarter2px--s432px
--s-half4px--s540px
--s8px--s648px
--s216px--s756px
--s324px--s864px
scss
// ✅ Good
padding: var(--s2) var(--s3);
gap: var(--s-half);

// ❌ Bad
padding: 16px 24px;
gap: 4px;

Color Variables

Use caseVariables
Text--text-color, --text-color-muted, --text-color-most-intense
Backgrounds--bg, --card-bg, --task-c-bg, --sub-task-c-bg
Semantic--color-success (#4caf50), --color-warning (#ff9800), --color-danger (#f44336)
Material palette--palette-primary-500, --palette-accent-500, --palette-warn-500 (100–900)
Overlays--c-dark-10 through --c-dark-90, --c-light-05 through --c-light-90
Alpha coefficients--border-alpha (0.12), --overlay-alpha (0.1), --muted-alpha (0.6), --separator-alpha (0.3)

Theme-Specific Values

Light theme sets: --bg: #f8f8f7, --card-bg: #ffffff, --text-color: rgb(44, 44, 44) Dark theme sets: --bg: #131314, --card-bg: var(--dark3), --text-color: rgb(230, 230, 230)

Dark elevation colors: --dark0 (rgb(0,0,0)) through --dark24 (rgb(56,56,56))

Theme-Specific Overrides in Components

scss
@include darkTheme() {
  /* dark-only styles */
}
@include lightTheme() {
  /* light-only styles */
}

Mixins are in src/styles/mixins/_theming.scss.

Shadows & Elevation

  • --whiteframe-shadow-1dp through --whiteframe-shadow-24dp — classic Material shadows
  • --md-sys-level1 through --md-sys-level5 — Material Design 3 style

Transitions & Animations

TypeVariables
Shorthands--transition-fast, --transition-standard, --transition-enter, --transition-leave
Durations--transition-duration-xs (90ms), -s (150ms), -m (225ms), -l (375ms)
Additional--transition-duration-enter (225ms), --transition-duration-leave (195ms), --page-transition-duration (225ms)
Timing--ani-standard-timing, --ani-enter-timing, --ani-leave-timing, --ani-sharp-timing

When migrating hardcoded durations, pick the nearest bucket — up to ~15% drift is acceptable for UI transitions.

Focus Ring

Keyboard-accessibility tokens for custom interactive elements. Material components keep their own focus treatment; use these for non-Material buttons, cards, and custom controls.

VariableValue
--focus-ring-width2px
--focus-ring-offset2px
--focus-ringvar(--brand)
--focus-ring-colorvar(--focus-ring)

Themes should override the public --focus-ring primitive on body / body.isDarkTheme. --focus-ring-color is the compatibility alias consumed by existing components.

Quickest adoption — add the .focus-ring utility class from util.scss, which applies an outline on :focus-visible only (so it doesn't fire on mouse clicks).

scss
// opt-in per-element
.my-button:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
}

Z-Index Layers

VariableValuePurpose
--z-check-done11Task done checkbox
--z-main-header12Main header
--z-task-title-focus32Focused task title
--z-mobile-bottom-nav50Mobile bottom navigation
--z-side-nav60Side navigation
--z-backdrop222Backdrop overlay
--z-add-task-bar999Add task bar
--z-search-bar999Search bar
--z-onboarding-presets999Onboarding preset screen
--z-tour1001Tour overlay

Layout Variables

VariableValueNotes
--component-max-width800px900–1000px on iPad
--side-nav-width200px
--side-nav-width-l400px
--bar-height-large56px
--bar-height48px
--bar-height-small40px

Responsive Breakpoints

Available as CSS vars (--layout-xxxs through --layout-xl) and as SCSS variables and mixins in src/styles/mixins/_media-queries.scss:

BreakpointValue
xxxs398px
xxs440px
xs600px
sm960px
md1280px
lg1920px
xl2000px

Utility Classes

Defined in src/styles/util.scss and src/styles/themes.scss:

  • Layout: .center-wrapper, .mw (max-width container)
  • Responsive: .hide-xs, .hide-xxs, .hide-gt-sm
  • Input: .show-only-on-touch-primary, .show-only-on-mouse-primary
  • Theme: .show-dark-only, .show-light-only
  • Color: .bg-primary, .bgc-accent, .color-primary, .bg-success, .bg-warning, .bg-danger
  • Effects: .milk-glass (backdrop blur)

Authoring Themes

Theme files live in src/assets/themes/*.css and are loaded at runtime.

Side nav: never apply CB-creating properties to the magic-side-nav host

The mobile drawer (.nav-sidenav) and its overlay (.nav-backdrop-mobile) are position: fixed children of the magic-side-nav host. On mobile the host shrinks to width: 0 (hostWidthSignal in magic-side-nav.component.ts).

Any property that establishes a new containing block for fixed-position descendantsbackdrop-filter, filter, transform, perspective, contain: paint|layout|strict, or a matching will-change — re-anchors the drawer and backdrop to the 0-wide host. The drawer never appears when the user taps the menu.

Apply glass/blur/tint to the inner .nav-sidenav instead:

css
/* ❌ Bad — collapses the mobile drawer */
body.isDarkTheme magic-side-nav {
  background: var(--my-pane);
  backdrop-filter: blur(32px);
}

/* ✅ Good — host stays visually inert */
body.isDarkTheme magic-side-nav .nav-sidenav {
  background: var(--my-pane);
  backdrop-filter: blur(32px);
}

Properties that do NOT create a containing block — background, border, box-shadow, margin — are safe on the host. See velvet.css and liquid-glass.css for full reference patterns.

Global Component Styles

Located in src/styles/components/, these are needed for elements that render outside component scope:

  • _overwrite-material.scss — Material component customizations
  • _customizer-menu.scss, backdrop.scss, bottom-panel.scss
  • markdown.scss, mentions.scss, table.scss
  • fab-wrapper.scss, wrap-buttons.scss, multi-btn-wrapper.scss
  • planner-shared.scss, formly-rows.scss, scrollbars.scss