JPN Web Design uses Tailwind CSS v4 with CSS custom properties for theming. All colors are defined as CSS variables in app/globals.css and bridged into Tailwind through the @theme inline block. Fonts are loaded via next/font/google in the root layout.
CSS custom properties
All color tokens are declared in globals.css as CSS custom properties. Light mode values live on :root; dark mode values override them inside the .dark class (applied to <html> by next-themes).
/* app/globals.css */
:root {
--background: hsl(48, 23%, 87%); /* Warm parchment */
--foreground: hsl(0, 0%, 18%); /* Near-black ink */
--accent: hsl(2, 47%, 50%); /* Muted red */
--dark-gray: hsl(0, 0%, 29%);
--light-gray: hsl(0, 0%, 42%);
--primary: hsl(346, 100%, 37%); /* Deep crimson */
}
.dark {
--background: hsl(0, 0%, 10%); /* Near-black ink (Sumi-e) */
--foreground: hsl(48, 20%, 90%); /* Bone white / rice paper */
--accent: hsl(346, 80%, 55%); /* Brighter red for contrast */
--dark-gray: hsl(0, 0%, 80%);
--light-gray: hsl(0, 0%, 65%);
--primary: hsl(346, 90%, 60%); /* Vibrant crimson */
}
Color token reference
| Token | Light value | Dark value | Usage |
|---|
--background | Warm parchment | Near-black | Page background |
--foreground | Near-black ink | Bone white | Body text |
--primary | Deep crimson | Vibrant crimson | Accent lines, icons, badges |
--accent | Muted red | Brighter red | Buttons, interactive highlights |
--dark-gray | hsl(0,0%,29%) | hsl(0,0%,80%) | Secondary text |
--light-gray | hsl(0,0%,42%) | hsl(0,0%,65%) | Muted labels |
Changing the primary color
To change the primary crimson to another color, update the --primary (and optionally --accent) values in both :root and .dark:
Default (Crimson)
Indigo
Forest Green
:root {
--primary: hsl(346, 100%, 37%);
--accent: hsl(2, 47%, 50%);
}
.dark {
--primary: hsl(346, 90%, 60%);
--accent: hsl(346, 80%, 55%);
}
:root {
--primary: hsl(243, 75%, 45%);
--accent: hsl(243, 60%, 55%);
}
.dark {
--primary: hsl(243, 80%, 65%);
--accent: hsl(243, 70%, 60%);
}
:root {
--primary: hsl(150, 60%, 30%);
--accent: hsl(150, 45%, 40%);
}
.dark {
--primary: hsl(150, 60%, 50%);
--accent: hsl(150, 50%, 55%);
}
Every component that uses text-primary, border-primary, or bg-primary will update automatically — the tokens are used consistently throughout the codebase.
Tailwind CSS v4 configuration
Tailwind v4 does not use a tailwind.config.js file for colors. Instead, custom colors are registered inside a @theme inline block in globals.css, which maps the CSS custom properties into Tailwind utility classes.
/* app/globals.css */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-accent: var(--accent);
--color-dark-gray: var(--dark-gray);
--color-light-gray: var(--light-gray);
--color-primary: var(--primary);
--font-sans: var(--font-noto-sans);
--font-serif: var(--font-noto-serif);
}
This makes the following Tailwind utilities available throughout the project:
| Tailwind class | Maps to |
|---|
bg-background / text-background | --background |
bg-foreground / text-foreground | --foreground |
bg-primary / text-primary / border-primary | --primary |
bg-accent / text-accent | --accent |
text-dark-gray | --dark-gray |
text-light-gray | --light-gray |
font-sans | Noto Sans JP |
font-serif | Noto Serif JP |
Adding a new color
To add a brand-new color token:
Declare it as a CSS variable
:root {
--brand: hsl(210, 80%, 50%);
}
.dark {
--brand: hsl(210, 80%, 65%);
}
Register it in the @theme block
@theme inline {
--color-brand: var(--brand);
}
Use it with Tailwind utilities
<div className="bg-brand text-white" />
Fonts
JPN Web Design uses two Google Fonts optimized for Japanese text, loaded via next/font/google in the root layout.
// app/layout.tsx
import { Noto_Sans_JP, Noto_Serif_JP } from "next/font/google";
const notoSans = Noto_Sans_JP({
variable: "--font-noto-sans",
subsets: ["latin"],
weight: ["400", "500", "700"],
});
const notoSerif = Noto_Serif_JP({
variable: "--font-noto-serif",
subsets: ["latin"],
weight: ["400", "600"],
});
The font CSS variables are applied to the <body> tag:
<body className={`${notoSans.variable} ${notoSerif.variable} antialiased`}>
| Font | Variable | Default usage |
|---|
| Noto Sans JP | --font-noto-sans | Body text (font-sans) |
| Noto Serif JP | --font-noto-serif | Japanese labels (font-serif, .text-japanese) |
The .text-japanese utility class (defined in globals.css) applies the serif font with tracking optimized for Japanese characters:
.text-japanese {
font-family: var(--font-noto-serif), serif;
letter-spacing: 0.05em;
}
To swap the fonts, replace Noto_Sans_JP / Noto_Serif_JP with any other next/font/google import and update the variable names. The rest of the theme will follow automatically.
Dark / light mode
Theme switching is handled by next-themes. The system is wired up across three files.
ThemeProvider
ThemeProvider wraps the app in layout.tsx and sets the initial theme:
// app/layout.tsx
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<Navigation />
<main className="min-h-screen">{children}</main>
...
</ThemeProvider>
attribute="class" — next-themes adds a class="dark" to <html> when dark mode is active, which activates the .dark { } block in globals.css.
defaultTheme="system" — respects the operating system preference on first visit.
enableSystem — allows the "system" option to follow the OS setting.
ThemeProvider component
// components/ThemeProvider.tsx
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ComponentProps } from "react";
export function ThemeProvider({
children,
...props
}: ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
ThemeToggle component
ThemeToggle renders the sun/moon button in the navigation bar:
// components/ThemeToggle.tsx
"use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<button type="button" className="p-2 rounded-full bg-accent/10 text-accent opacity-50" disabled>
<div className="w-5 h-5" />
</button>
);
}
return (
<button
type="button"
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
className="p-2 rounded-full bg-accent/10 hover:bg-accent/20 text-accent transition-colors duration-300"
aria-label="Alternar modo oscuro"
>
{resolvedTheme === "dark" ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
</button>
);
}
The mounted guard prevents a hydration mismatch: the server does not know the user’s preferred theme, so the button is rendered as a disabled placeholder until the client has mounted.
Personal Info
Update your name, experience, projects, and social links
Japanese Patterns
Apply traditional Japanese background textures