Skip to main content
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

TokenLight valueDark valueUsage
--backgroundWarm parchmentNear-blackPage background
--foregroundNear-black inkBone whiteBody text
--primaryDeep crimsonVibrant crimsonAccent lines, icons, badges
--accentMuted redBrighter redButtons, interactive highlights
--dark-grayhsl(0,0%,29%)hsl(0,0%,80%)Secondary text
--light-grayhsl(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:
:root {
  --primary: hsl(346, 100%, 37%);
  --accent:  hsl(2, 47%, 50%);
}

.dark {
  --primary: hsl(346, 90%, 60%);
  --accent:  hsl(346, 80%, 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 classMaps 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-sansNoto Sans JP
font-serifNoto Serif JP

Adding a new color

To add a brand-new color token:
1

Declare it as a CSS variable

:root {
  --brand: hsl(210, 80%, 50%);
}

.dark {
  --brand: hsl(210, 80%, 65%);
}
2

Register it in the @theme block

@theme inline {
  --color-brand: var(--brand);
}
3

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`}>
FontVariableDefault usage
Noto Sans JP--font-noto-sansBody text (font-sans)
Noto Serif JP--font-noto-serifJapanese 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

Build docs developers (and LLMs) love