diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index 77e7533fb1..253b70d8ce 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/services/web/app/src/Features/Project/ProjectController.mjs @@ -1079,7 +1079,7 @@ const _ProjectController = { user ) - const initialLoadingScreenTheme = getInitialLoadingScreenTheme( + const initialLoadingScreenTheme = UserSettingsHelper.getInitialTheme( userSettings?.overallTheme ) @@ -1469,19 +1469,6 @@ const _ProjectController = { }, } -function getInitialLoadingScreenTheme(overallThemeSetting) { - switch (overallThemeSetting) { - case 'light-': - return 'light' - case '': - return 'dark' - case 'system': - return 'system' - default: - return 'dark' - } -} - const defaultSettingsForAnonymousUser = userId => ({ id: userId, ace: { diff --git a/services/web/app/src/Features/Project/ProjectListController.mjs b/services/web/app/src/Features/Project/ProjectListController.mjs index 5acb9e724d..5b3a909f49 100644 --- a/services/web/app/src/Features/Project/ProjectListController.mjs +++ b/services/web/app/src/Features/Project/ProjectListController.mjs @@ -664,6 +664,7 @@ async function projectListPage(req, res, next) { userAffiliations, userEmails, userSettings, + initialTheme: UserSettingsHelper.getInitialTheme(userSettings.overallTheme), reconfirmedViaSAML, allInReconfirmNotificationPeriods, survey, diff --git a/services/web/app/src/Features/Project/UserSettingsHelper.mjs b/services/web/app/src/Features/Project/UserSettingsHelper.mjs index 93d751e0db..297e29007a 100644 --- a/services/web/app/src/Features/Project/UserSettingsHelper.mjs +++ b/services/web/app/src/Features/Project/UserSettingsHelper.mjs @@ -33,6 +33,19 @@ function buildRefProviderSettings(settings) { } } +function getInitialTheme(overallThemeSetting) { + switch (overallThemeSetting) { + case 'light-': + return 'light' + case '': + return 'dark' + case 'system': + return 'system' + default: + return 'dark' + } +} + async function buildUserSettings(_req, _res, user) { return { mode: user.ace.mode, @@ -63,4 +76,5 @@ async function buildUserSettings(_req, _res, user) { export default { buildUserSettings, + getInitialTheme, } diff --git a/services/web/app/views/layout-base.pug b/services/web/app/views/layout-base.pug index 781df76a62..d9b4c3b4ac 100644 --- a/services/web/app/views/layout-base.pug +++ b/services/web/app/views/layout-base.pug @@ -106,6 +106,7 @@ html( 'red-nav-bar-for-admins': !settings.isDevEnv && hasFeature('saas') && hasAdminAccess(), } data-theme='light' + data-initial-theme=initialTheme ) if settings.recaptcha && settings.recaptcha.siteKeyV3 script( diff --git a/services/web/frontend/js/shared/hooks/use-themed-page.tsx b/services/web/frontend/js/shared/hooks/use-themed-page.tsx index 1809ffb871..eeffc1d092 100644 --- a/services/web/frontend/js/shared/hooks/use-themed-page.tsx +++ b/services/web/frontend/js/shared/hooks/use-themed-page.tsx @@ -8,5 +8,7 @@ export default function useThemedPage(featureFlag?: string) { // Sets the body's data-theme attribute for theming document.body.dataset.theme = activeOverallTheme === 'dark' ? 'default' : 'light' + // drop the server-rendered first-paint hint + delete document.body.dataset.initialTheme }, [activeOverallTheme]) } diff --git a/services/web/frontend/stylesheets/base/initial-theme.scss b/services/web/frontend/stylesheets/base/initial-theme.scss new file mode 100644 index 0000000000..ddd65b53ed --- /dev/null +++ b/services/web/frontend/stylesheets/base/initial-theme.scss @@ -0,0 +1,13 @@ +// Match body's background so the first paint isn't white in dark mode. +// Background only: color-scheme is set per container (e.g. .project-ds-nav-page) +[data-initial-theme='dark'] { + background-color: var(--bg-dark-primary); +} + +[data-initial-theme='system'] { + // The 'system' theme is resolved client-side (useThemedPage), which also + // removes this attribute; until then, follow the OS preference. + @media (prefers-color-scheme: dark) { + background-color: var(--bg-dark-primary); + } +} diff --git a/services/web/frontend/stylesheets/main-style.scss b/services/web/frontend/stylesheets/main-style.scss index 82cc87e1b5..1f6b9881fc 100644 --- a/services/web/frontend/stylesheets/main-style.scss +++ b/services/web/frontend/stylesheets/main-style.scss @@ -44,6 +44,9 @@ @import 'base/links'; @import 'base/base'; +// Body theming for the first paint, before the client resolves the theme +@import 'base/initial-theme'; + // Page layout that isn't related to a particular component or page @import 'base/layout';