https://github.com/withastro/blog-tutorial-demo/blob/complete/src/components/ThemeIcon.astro
https://discord.com/channels/1217527207467946087/1217544238397526016/1225065920393908244
Starlight:
- https://github.com/withastro/starlight/blob/b799f409bc941fdbdb41c4808b5aa602717af71b/packages/starlight/components/ThemeSelect.astro#L28
- https://github.com/withastro/starlight/blob/b799f409bc941fdbdb41c4808b5aa602717af71b/packages/starlight/components/ThemeProvider.astro
## Handling the existing PR
@Fryuni and @Oliver talked about how to handle the [open community PR]. What we agreed would be the best approach to have less friction with a community member to rewrite the entire thing would be to either:
- Commit directly to their branch
- Open a new PR includig them as co-author credits
[open community PR]: https://github.com/astrolicious/astro-tips.dev/pull/40
## DX Goal
Ideal API:
```astro
---
import ThemeProvider from '../ThemeProvider.astro';
import ThemeProvider from '../ThemeSelector.astro';
---
<html>
<head>
<ThemeProvider/>
</head>
<body>
<ThemeSeletor/>
<script>
theme.getCurrentTheme();
theme.setTheme();
theme.onThemeChange();
</script>
</body>
</html>
```
```html
<div>
</div>
<script is:inline>
class StarlightThemeSelect extends HTMLElement {
constructor() {
super();
}
}
customElements.define('starlight-theme-select', StarlightThemeSelect);
</script>
```
1 component in the head
- handles logic
- expose a method
## Theme Provider
Guide through the process in incremental steps:
### Defining the API
Points to explain:
- What is the expected final API:
- `setTheme` that accepts a theme to be set, or `'auto'` (default) that will use the system theme.
- `getTheme` returning the _selected_ theme. So in case of `'auto'` it will return the string `'auto'`, not the system theme.
- Why `is:inline` is needed (FOUC)
- Good practice of the iife to not polute the global scope
- `??=` for VT compatibility
Code:
```html
<script is:inline>
window.theme ??= (() => {
const defaultTheme = 'dark';
let selectedTheme = defaultTheme;
function setTheme(theme = defaultTheme) {
selectedTheme = theme;
document.documentElement.dataset.theme = theme;
}
function getTheme() {
return selectedTheme;
}
return {
setTheme,
getTheme
};
})();
theme.setTheme(theme.getTheme())
```
### Allowing a custom default theme
Point to explain:
- How to pass information from a server-side script to a client-side script
Code
```html
---
type Props = {
"default-theme"?: "auto" | "dark" | "light";
};
const { "default-theme": defaultTheme = "auto" } = Astro.props;
---
<script is:inline data-default-theme={defaultTheme}>
window.theme ??= (() => {
const defaultTheme = document.currentScript.dataset.defaultTheme;
let selectedTheme = defaultTheme;
function setTheme(theme = defaultTheme) {
selectedTheme = theme;
document.documentElement.dataset.theme = theme;
}
function getTheme() {
return selectedTheme;
}
return {
setTheme,
getTheme
};
})();
theme.setTheme(theme.getTheme())
</script>
```
### Persistence
Points to explain:
- Why store the theme on `localStorage`
- Why do we need to check if `localStorage` is defined (restricted environments like Brave on maximum shields mode)
Code:
```html
---
type Props = {
"default-theme"?: "auto" | "dark" | "light";
};
const { "default-theme": defaultTheme = "auto" } = Astro.props;
---
<script is:inline data-default-theme={defaultTheme}>
window.theme ??= (() => {
const defaultTheme = document.currentScript.dataset.defaultTheme;
const storageKey = "theme";
const store =
typeof localStorage !== "undefined"
? localStorage
: {
getItem: () => null,
setItem: () => {},
};
function setTheme(theme = defaultTheme) {
store.setItem(storageKey, theme);
document.documentElement.dataset.theme = theme;
}
function getTheme() {
return store.getItem(storageKey) || defaultTheme;
}
return {
setTheme,
getTheme
};
})();
theme.setTheme(theme.getTheme())
</script>
```
### System theme
Points to explain:
- What is the system/auto theme
- How to retrieve the system theme
Code:
```html
---
type Props = {
"default-theme"?: "auto" | "dark" | "light";
};
const { "default-theme": defaultTheme = "auto" } = Astro.props;
---
<script is:inline data-default-theme={defaultTheme}>
window.theme ??= (() => {
const defaultTheme = document.currentScript.dataset.defaultTheme;
const storageKey = "theme";
const store =
typeof localStorage !== "undefined"
? localStorage
: {
getItem: () => null,
setItem: () => {},
};
const mediaMatcher = window.matchMedia("(prefers-color-scheme: light)");
const systemTheme = mediaMatcher.matches ? 'light' : 'dark';
function setTheme(theme = defaultTheme) {
store.setItem(storageKey, theme);
document.documentElement.dataset.theme =
theme === 'auto' ? systemTheme : theme;
}
function getTheme() {
return store.getItem(storageKey) || defaultTheme;
}
return {
setTheme,
getTheme
};
})();
theme.setTheme(theme.getTheme())
</script>
```
### Theme changed event
Points to explain:
- Why dispatch an event (avoiding use of expensive `MutationObserver`)
Code:
```html
---
type Props = {
"default-theme"?: "auto" | "dark" | "light";
};
const { "default-theme": defaultTheme = "auto" } = Astro.props;
---
<script is:inline data-default-theme={defaultTheme}>
window.theme ??= (() => {
const defaultTheme = document.currentScript.dataset.defaultTheme;
const storageKey = "theme";
const store =
typeof localStorage !== "undefined"
? localStorage
: {
getItem: () => null,
setItem: () => {},
};
const mediaMatcher = window.matchMedia("(prefers-color-scheme: light)");
const systemTheme = mediaMatcher.matches ? 'light' : 'dark';
function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
const event = new CustomEvent('theme-changed', {
details: {
appliedTheme: theme,
selectedTheme: getTheme(),
},
});
document.dispatch(event);
}
function setTheme(theme = defaultTheme) {
store.setItem(storageKey, theme);
applyTheme(theme === 'auto' ? systemTheme : theme);
}
function getTheme() {
return store.getItem(storageKey) || defaultTheme;
}
return {
setTheme,
getTheme
};
})();
theme.setTheme(theme.getTheme())
</script>
```
### Watch for system theme changes
Points to explain:
- Why should we handle system changes
- As the palette of the system changes, so should a website using system changes
- Many systems, especially mobile, have adaptative themes that use light mode from sunrise to sunset and dark mode from sunset to sunrise
Code:
```html
---
type Props = {
"default-theme"?: "auto" | "dark" | "light";
};
const { "default-theme": defaultTheme = "auto" } = Astro.props;
---
<script is:inline data-default-theme={defaultTheme}>
window.theme ??= (() => {
const defaultTheme = document.currentScript.dataset.defaultTheme;
const storageKey = "theme";
const store =
typeof localStorage !== "undefined"
? localStorage
: {
getItem: () => null,
setItem: () => {},
};
const mediaMatcher = window.matchMedia("(prefers-color-scheme: light)");
let systemTheme = mediaMatcher.matches ? 'light' : 'dark';
mediaMatcher.addEventListener("change", (event) => {
systemTheme = mediaMatcher.matches ? "light" : "dark";
if (theme.getTheme() === "auto") {
applyTheme(systemTheme);
}
});
function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
const event = new CustomEvent('theme-changed', {
details: {
appliedTheme: theme,
selectedTheme: getTheme(),
},
});
document.dispatch(event);
}
function setTheme(theme = defaultTheme) {
store.setItem(storageKey, theme);
applyTheme(theme === 'auto' ? systemTheme : theme);
}
function getTheme() {
return store.getItem(storageKey) || defaultTheme;
}
return {
setTheme,
getTheme
};
})();
theme.setTheme(theme.getTheme())
</script>
```
### Styles
```html
<style>
/* Define CSS variables for light theme */
:root[data-theme="light"] {
--background-color: #ffffff;
--text-color: #000000;
--link-color: #0000ee;
}
/* Define CSS variables for dark theme */
:root[data-theme="dark"] {
--background-color: #333333;
--text-color: #ffffff;
--link-color: #0094ff;
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
</style>
```