feat: implement dark mode support

- Introduce CSS variables for theming in `global.css`
- Add `ThemeIcon` component for toggling light/dark modes
- Integrate theme initialization script in `BaseLayout` to prevent FOUC
- Update navigation, buttons, and tag styles for dark mode compatibility
- Configure Shiki for dual-theme syntax highlighting in `astro.config.mjs`
- Adjust link colors to accessible blue shades for both themes
This commit is contained in:
Daisuke Nakahara 2026-02-11 13:15:03 +09:00
parent e357f2ed68
commit 62aa324e16
11 changed files with 218 additions and 61 deletions

View file

@ -6,5 +6,14 @@ import preact from "@astrojs/preact";
// https://astro.build/config
export default defineConfig({
site: "https://blog.n-daisuke897.com/",
integrations: [preact()]
integrations: [preact()],
markdown: {
shikiConfig: {
themes: {
light: 'github-light',
dark: 'github-dark',
},
wrap: true,
},
},
});

View file

@ -12,11 +12,19 @@ const { title, url, datetime } = Astro.props;
font-size: 14px;
color: #666;
}
:global(.dark) .new-article-date {
color: #94a3b8;
}
.new-article-title {
font-size: 18px;
font-weight: bold;
color: #333f;
color: var(--text-color);
text-decoration: none;
}
.new-article-title:hover {
text-decoration: underline;
}
/* Removed specific dark mode color to inherit var(--text-color) */
</style>
<div class="new-article">
<div class="new-article-date">{datetime}</div>

View file

@ -9,3 +9,11 @@ import Navigation from "./Navigation.astro";
<Navigation />
</nav>
</header>
<style>
nav {
display: flex;
align-items: center;
flex-wrap: wrap;
}
</style>

View file

@ -1,8 +1,9 @@
---
import ThemeIcon from "./ThemeIcon.astro";
---
<div class="nav-links">
<ThemeIcon />
<a href="/">Home</a>
<a href="/about/">About</a>
<a href="/blog/">Articles</a>

View file

@ -0,0 +1,36 @@
---
---
<button id="themeToggle">
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path class="sun" d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 10.5H1v2h3v-2zm9-9.95h-2V3.5h2V.55zm7.45 3.91l-1.41-1.41-1.79 1.79 1.41 1.41 1.79-1.79zm-3.21 13.7l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM20 10.5v2h3v-2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm-1 16.95h2V19.5h-2v2.95zm-7.45-3.91l1.41 1.41 1.79-1.8-1.41-1.41-1.79 1.8z" />
<path class="moon" d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.27-4.4 2.27-3.03 0-5.5-2.47-5.5-5.5 0-1.82.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z" />
</svg>
</button>
<style>
#themeToggle {
border: 0;
background: none;
cursor: pointer;
display: inline-flex;
align-items: center;
padding: 0 10px;
}
.sun { fill: transparent; }
.moon { fill: transparent; }
:global(.dark) .moon { fill: white; }
:global(html:not(.dark)) .sun { fill: black; }
</style>
<script is:inline>
const handleToggleClick = () => {
const element = document.documentElement;
element.classList.toggle("dark");
const isDark = element.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
}
document.getElementById("themeToggle").addEventListener("click", handleToggleClick);
</script>

View file

@ -12,6 +12,22 @@ const { pageTitle } = Astro.props;
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{pageTitle + " | Naputo"}</title>
<script is:inline>
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
</script>
</head>
<body>
<Header />

View file

@ -30,7 +30,11 @@ const optionsForDate = {
<style>
a {
color: #00539f;
color: #3b82f6;
}
:global(.dark) a {
color: #60a5fa;
}
.tags {
@ -47,6 +51,11 @@ const optionsForDate = {
background-color: #f8fcfd;
}
:global(.dark) .tag {
background-color: #1e293b;
border-color: #475569;
}
.markdown-content {
font-family: sans-serif;
font-size: 1.1rem;

View file

@ -77,15 +77,28 @@ const pageTitle = "Articles";
text-decoration: none;
transition: background-color 0.3s ease, transform 0.3s ease;
}
:global(.dark) nav.pagination a,
:global(.dark) nav.pagination span {
background-color: #1e293b;
color: #f8fafc;
border: 1px solid #334155;
}
nav.pagination a:hover {
background-color: #cacaca;
transform: translateY(-2px);
}
:global(.dark) nav.pagination a:hover {
background-color: #334155;
}
nav.pagination a.active {
background-color: #cacaca;
font-weight: bold;
pointer-events: none;
}
:global(.dark) nav.pagination a.active {
background-color: #334155;
color: #f8fafc;
}
@media (max-width: 600px) {
nav.pagination {
flex-direction: column;

View file

@ -34,9 +34,17 @@ const sortedPosts = allPosts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pu
transition: background-color 0.3s ease, transform 0.3s ease;
cursor: pointer;
}
:global(.dark) .list-of-articles-button {
background-color: #1e293b;
color: #f8fafc;
border: 1px solid #334155;
}
.list-of-articles-button:hover {
background-color: #cacaca;
}
:global(.dark) .list-of-articles-button:hover {
background-color: #334155;
}
</style>
<BaseLayout pageTitle={pageTitle}>
<h3 class="header-new-posts">New Posts</h3>

View file

@ -20,7 +20,7 @@ const pageTitle = "Tag List";
<style>
a {
color: #00539f;
color: #3b82f6;
}
.tags {
@ -36,4 +36,13 @@ const pageTitle = "Tag List";
font-size: 1.15em;
background-color: #f8fcfd;
}
:global(.dark) .tag {
background-color: #1e293b;
border-color: #475569;
}
:global(.dark) .tag a {
color: #f8fafc;
}
</style>

View file

@ -1,5 +1,19 @@
/* Dark Mode support */
:root {
--bg-color: #f1f5f9;
--text-color: #333333;
--nav-hover: #e0e0e0;
}
html.dark {
--bg-color: #0f172a;
--text-color: #f8fafc;
--nav-hover: #1e293b;
}
html {
background-color: #f1f5f9;
background-color: var(--bg-color);
color: var(--text-color);
font-family: sans-serif;
}
@ -21,6 +35,14 @@ h1 {
}
/* nav styles */
a {
color: #3b82f6; /* Blue 500 - vivid but readable on light */
}
html.dark a {
color: #60a5fa; /* Blue 400 - lighter and softer for dark mode */
}
.hamburger {
padding-right: 20px;
cursor: pointer;
@ -31,17 +53,22 @@ h1 {
width: 32px;
height: 4px;
margin-bottom: 8px;
background-color: #333333;
background-color: var(--text-color);
}
.nav-links {
width: 100%;
display: flex;
display: none;
justify-content: center;
align-items: center;
margin: 0;
}
.nav-links.expanded {
display: flex;
flex-direction: column;
}
.nav-links a {
display: block;
text-align: center;
@ -51,12 +78,12 @@ h1 {
font-weight: bold;
text-transform: uppercase;
border-radius: 4px;
color: #333333;
color: var(--text-color);
}
.nav-links a:hover,
.nav-links a:focus {
background-color: #e0e0e0;
background-color: var(--nav-hover);
}
.nav-links img {
@ -66,14 +93,16 @@ h1 {
display: inline-block;
}
.expanded {
display: unset;
html.dark .nav-links img {
filter: invert(1) hue-rotate(180deg);
}
/* .expanded removed as it is handled by .nav-links.expanded */
@media screen and (min-width: 636px) {
.nav-links {
margin-left: 5em;
display: block;
display: flex;
position: static;
width: auto;
background: none;
@ -94,3 +123,14 @@ pre,
code {
font-family: "JetBrains Mono", "Fira Code", "Menlo", "Consolas", monospace;
}
pre {
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
}
html.dark pre {
background-color: #1e293b !important;
}