Appearance Customization
This guide explains how to customize KUKAN when you fork the repository for your organization. Changes can be applied incrementally — start with the simplest approach.
Scope of Changes
Section titled “Scope of Changes”Files you modify in a fork are limited to apps/web/brands/default/. Its contents:
Path (under brands/default/) | Contents | Section |
|---|---|---|
brand-config.ts | Site name, description, logo, footer links, OG image, favicon, GA ID | Tier 2 |
theme.css | CSS variable overrides (colors, sizing) | Tier 1 |
messages/ja.json / en.json | UI text (i18n message) overrides | i18n Message Overrides |
overrides/ | Component replacements for Header / Footer / TopPage (slots) | Tier 3 |
pages/ | Custom static pages (terms, privacy policy, etc.) | Static Pages |
public/ | Static files: logo, favicon, OG image, etc. | Static Files |
index.ts / messages/index.ts | Export barrels; normally no need to edit | — |
Do not modify files outside this directory. Doing so will cause merge conflicts when pulling upstream updates.
Tier 1: Colors and Sizing
Section titled “Tier 1: Colors and Sizing”Write CSS variables in brand/theme.css to change colors and sizes across the entire site.
:root { --primary: 142 64% 32%; /* Main color (HSL) */ --primary-foreground: 0 0% 100%; /* Text color on main color */ --kukan-header-height: 72px; /* Header height */ --kukan-container-max-width: 1400px;}Available CSS Variables
Section titled “Available CSS Variables”You can override shadcn/ui standard variables and KUKAN-specific variables (prefixed with --kukan-*). See apps/web/src/app/globals.css for the full list.
Each variable carries an override rating to make the go/no-go decision easier:
- Recommended — variables intended to be changed to match your brand
- Caution — overridable, but shared across multiple components and states. Check the “Purpose and main usage” column and preserve the default light/dark relationship
- Not recommended — variables that convey UI meaning (error, success, warning). Changing them for branding reasons is discouraged
| Variable | Rating | Purpose and main usage |
|---|---|---|
--primary / --primary-foreground | Recommended | Main brand color. Buttons (default), links, progress bars, text selection, checked states of checkboxes etc. |
--highlight | Recommended | Search keyword highlighting (only affects search result display) |
--radius | Recommended | Base border radius (all components) |
--kukan-* | Recommended | Layout values such as header height, logo height, container width |
--background / --foreground | Caution | Page background and base text. Affects the whole site |
--card / --popover (each with -foreground) | Caution | Backgrounds and text of cards and popups such as dropdowns and selects |
--secondary / --secondary-foreground | Caution | Backgrounds of secondary buttons and badges. Expected to stay light |
--muted / --muted-foreground | Caution | Supplementary text (the most widely used variable on the site), placeholders, skeletons, tinted backgrounds. Expected to stay light |
--accent / --accent-foreground | Caution | Hover and selection color. Hover of outline/ghost buttons, focused items in selects/dropdowns, dialog close buttons, etc. Expected to stay light |
--border / --input | Caution | Borders and input outlines across all components |
--ring | Caution | Focus ring. Directly affects keyboard-navigation visibility — keep sufficient contrast against backgrounds |
--destructive / --destructive-foreground | Not recommended | Semantic color for errors and deletion (destructive buttons, form errors, delete menu items). Solid fill and text on it |
--success / --warning | Not recommended | Semantic colors for success / warning states (alerts, pipeline status, form warnings). Used for borders, tinted backgrounds, icons |
--success-tint-foreground / --warning-tint-foreground | Not recommended | Text color on the tinted (/10–/15) success / warning surfaces; not for solid fills |
Rules for Overriding
Section titled “Rules for Overriding”Follow these rules to swap colors safely:
- Override raw tokens only — redeclare variables like
--primaryas-is. Never touch the--color-*mappings inglobals.css(they wire tokens into Tailwind and are managed upstream). - Keep the bare HSL triplet format — write
--primary: 142 64% 32%;with no commas and nohsl()wrapper. Hex values orhsl(...)-wrapped values will not work. - Do not bake in opacity — transparency is applied at usage sites (e.g.
bg-success/10). Tokens must hold fully opaque base colors. - Update paired tokens together and check contrast — when changing
--primary, also update--primary-foregroundand keep a contrast ratio of at least 4.5:1 (WCAG AA). When changing--success/--warning, set the matching-tint-foregroundto a dark shade of the same hue and verify at least 4.5:1 on the/15tinted surface (unlike-foregroundtokens, these are not meant for solid fills). - Do not introduce
dark:classes — KUKAN’s theme tokens are light-only. Addingdark:in override components breaks the display only for users whose OS prefers dark mode. - Avoid raw palette colors in override components too — use semantic classes like
text-successinstead oftext-green-600, so a future recoloring only takes edits totheme.css.
Adding Custom Tokens (--brand-*)
Section titled “Adding Custom Tokens (--brand-*)”When a designer-specified palette includes colors that do not fit any official token role (primary, accent, etc.), you can add custom tokens with the --brand- prefix in brand/theme.css.
:root { --brand-accent-green: 183 29% 87%; --brand-link: 183 79% 28%;}--brand-*is a fork-only namespace — upstream never defines or references--brand-*tokens, so they cannot conflict with upstream updates. Conversely,--kukan-*is reserved for upstream; do not use it for custom tokens- Use the same bare HSL triplet format as official tokens (so opacity modifiers like
/10work at usage sites) - Custom tokens can only be consumed in fork-owned components (
brand/overrides//brand/pages/). Upstream components never reference them - No Tailwind utility classes are generated —
theme.cssis a separate CSS unit fromglobals.css, so@themecannot generate classes for it. Reference custom tokens with the arbitrary value syntax:
<a className="text-[hsl(var(--brand-link))] bg-[hsl(var(--brand-accent-green))]">...</a>Changing Colors on Non-Overridden Screens
Section titled “Changing Colors on Non-Overridden Screens”If you want to recolor a single component on an upstream screen you have not overridden, overriding official tokens often has too wide a blast radius (see the table above). For this case, the upstream repository adds component-scoped alias tokens on demand (e.g. --kukan-header-bg, defaulting to var(--primary) so nothing changes unless a fork overrides it). Propose the addition via an issue on the upstream repository when you need one.
Tier 2: Text and Metadata
Section titled “Tier 2: Text and Metadata”Edit brand/brand-config.ts to change the site name, copyright, and metadata.
import type { BrandConfig } from '@/types/brand'
export const brandConfig: BrandConfig = { siteName: 'City of Example Open Data Catalog', siteDescription: 'Search and download open data published by the City of Example', copyright: 'City of Example.', copyrightUrl: 'https://www.city.example.lg.jp',
logo: { type: 'image', src: '/brand/logo.svg', width: 160, height: 40, alt: 'City of Example' },
headerNavExtra: [ { label: 'Official Site', href: 'https://www.city.example.lg.jp', external: true }, ], footerLinks: [ { label: 'Terms of Use', href: '/terms' }, { label: 'Contact', href: '/contact' }, ],
ogImage: '/brand/og-image.png', faviconPath: '/brand/favicon.ico',}Field Reference
Section titled “Field Reference”| Field | Description |
|---|---|
siteName | Site title (browser tab, header, etc.) |
siteDescription | meta description (for search engines) |
copyright | Footer copyright text (© 2026 is prepended automatically) |
copyrightUrl | Link target for the site name in the footer (omit for no link) |
logo | Header logo configuration (see below) |
headerNavExtra | Additional navigation items for the header |
footerLinks | Footer link list |
ogImage | OGP image path (relative to public/) |
faviconPath | Favicon path |
The example-query chips under the search box are configured from the admin Site Management page (/dashboard/admin/site), not the brand config.
Logo Configuration
Section titled “Logo Configuration”The logo field supports two modes:
// Default (KUKAN logo)logo: { type: 'default' }
// Image file (recommended: 32-40px height, SVG format)logo: { type: 'image', src: '/brand/logo.svg', width: 160, height: 40, alt: 'City of Example' }i18n Message Overrides
Section titled “i18n Message Overrides”You can override UI translation text for your organization. Specify only the keys you want to change in brand/messages/{locale}.json.
{ "home": { "title": "○○市オープンデータカタログ", "description": "○○市のオープンデータを検索・活用できるポータル" }}{ "home": { "title": "City of Example Open Data Catalog", "description": "A portal to search and utilize open data from the City of Example" }}- Only specified keys are overridden; all others use the defaults from
apps/web/messages/{locale}.json - Nested objects are merged recursively — you don’t need to repeat sibling keys
- See
apps/web/messages/en.jsonfor the full list of available keys
Choosing Between brandConfig and Messages
Section titled “Choosing Between brandConfig and Messages”| Use case | Where to configure |
|---|---|
| Metadata, OGP, etc. (language-independent) | brand-config.ts |
| UI display text (per-language) | brand/messages/{locale}.json |
Tier 3: Component Replacement
Section titled “Tier 3: Component Replacement”To change the structure of the Header or Footer, create custom components and register them as overrides.
1. Create a custom component
import { getCurrentUser } from '@/lib/server-api'import { LanguageSwitcher } from '@/components/layout/language-switcher'import { MobileNav } from '@/components/layout/mobile-nav'import { UserMenu } from '@/components/auth/user-menu'
export async function Header() { const user = await getCurrentUser()
return ( <header className="sticky top-0 z-40 bg-[hsl(var(--primary))]"> <div className="mx-auto flex h-[var(--kukan-header-height)] max-w-[var(--kukan-container-max-width)] items-center justify-between px-4"> <img src="/brand/logo.svg" alt="City of Example" className="h-8" /> <div className="flex items-center gap-2"> <LanguageSwitcher /> {user && <UserMenu user={user} />} <MobileNav user={user} /> </div> </div> </header> )}2. Register in overrides/index.ts
import type { BrandOverrides } from '@/types/brand'import { Header } from './header'
export const overrides: BrandOverrides = { Header,}That’s all it takes to replace the Header.
Available Slots
Section titled “Available Slots”| Slot | Replaces |
|---|---|
Header | Entire site header |
Footer | Entire site footer |
TopPage | Entire top page (home page) |
Reusing Default Parts
Section titled “Reusing Default Parts”Inside custom components, you can import and reuse parts of the default implementation.
Use DefaultHeader / DefaultFooter instead of Header / Footer to avoid circular references.
import { DefaultHeader } from '@/components/layout/header'import { DefaultFooter } from '@/components/layout/footer'Organizing Override Files
Section titled “Organizing Override Files”If you have many overrides, organize them into subdirectories:
brand/overrides/├── index.ts├── layout/│ ├── header.tsx│ └── footer.tsx└── pages/ └── hero-section.tsxThe internal structure is up to you as long as index.ts exports everything.
Static Pages
Section titled “Static Pages”You can add organization-specific static pages such as terms of use or privacy policies.
A sample terms page (/terms) is included by default. Remove it if not needed.
1. Create a page component in brand/pages/
import type { Metadata } from 'next'
export const metadata: Metadata = { title: 'Privacy Policy',}
export default function PrivacyPage() { return ( <article className="mx-auto max-w-3xl px-4 py-12"> <h1 className="mb-8 text-2xl font-bold">Privacy Policy</h1> <p>...</p> </article> )}2. Register in brand/pages/index.ts
export const pages: Record<string, () => Promise<BrandPage>> = { terms: () => import('./terms'), privacy: () => import('./privacy'), // added}The page is now accessible at /privacy.
3. Optionally add links to headerNavExtra or footerLinks in brand-config.ts
headerNavExtra: [ { label: 'Privacy Policy', href: '/privacy' },],footerLinks: [ { label: 'Terms of Use', href: '/terms' }, { label: 'Privacy Policy', href: '/privacy' },],Removing Pages
Section titled “Removing Pages”To remove a page, delete its entry from the pages map in brand/pages/index.ts and delete the corresponding .tsx file.
Static Files
Section titled “Static Files”Place logos, favicons, and OG images in apps/web/brands/default/public/.
apps/web/brands/default/public/├── logo.svg├── favicon.ico└── og-image.pngFiles placed here are served at URLs like /brand/logo.svg.
Multiple Brands (Multiple Sites in One Fork)
Section titled “Multiple Brands (Multiple Sites in One Fork)”If one fork operates multiple sites (Multi-Site Operation) and you want a different look per site, you can hold multiple brands (ADR-042).
- The default brand is
apps/web/brands/default/. Add a new brand by copyingbrands/default/toapps/web/brands/<name>/and editing it - Passing
KUKAN_BRAND=<name>at build time switches what@/brandresolves to (unset →brands/default/). Only the selected brand enters the build - On AWS, set
brand: '<name>'on each site inenvironments.tsand the pipeline builds a web image per site with the matchingKUKAN_BRAND(Environment Configuration Reference). An unknown brand fails the image build - The
BrandConfig/BrandOverridescontract and the CSS variable rules are common to all brands; each brand only supplies values and override components
Pulling Upstream Updates
Section titled “Pulling Upstream Updates”Periodically pull updates from the upstream main branch:
git remote add upstream https://github.com/kukan-project/kukan.gitgit fetch upstreamgit merge upstream/mainAs long as your changes are limited to brands/default/ (including its public/), merge conflicts should not occur.
If Conflicts Occur
Section titled “If Conflicts Occur”When the upstream adds new fields to src/types/brand.ts, you may need to add them to your brand-config.ts. TypeScript will report type errors — follow the error messages to add the missing fields.
Testing
Section titled “Testing”The upstream unit tests are designed to be brand-independent: inside tests, the contents of brands/default/ are pinned to the KUKAN defaults. No matter how you customize the brand, the upstream tests (CI) will keep passing.
- Do not modify upstream test files. If a customization breaks an upstream test, report it to the upstream repository via an issue instead of patching it in your fork
- Place tests for your custom components in
apps/web/brands/default/__tests__/. Files named*.test.ts/*.test.tsxare discovered automatically and run withpnpm test. Whether to write tests is up to your fork
import { describe, it, expect } from 'vitest'import { render, screen } from '@testing-library/react'import { Header } from '../overrides/header'
describe('Custom Header', () => { it('should render the organization logo', async () => { render(await Header()) expect(screen.getByAltText('City logo')).toBeInTheDocument() })})Summary
Section titled “Summary”| Goal | Method | Estimated time |
|---|---|---|
| Change colors | Write CSS variables in brand/theme.css | 5 min |
| Use custom colors outside official tokens | Define --brand-* tokens in brand/theme.css | 5 min |
| Change site name and text | Edit brand/brand-config.ts | 30 min |
| Override UI translation text | Add override keys to brand/messages/{locale}.json | 10 min |
| Replace logo | Place file in brands/default/public/ + edit config | 10 min |
| Change header/footer structure | Create components in brand/overrides/ | A few hours |
| Replace the top page | Create component in brand/overrides/ | A few hours |
| Add static pages | Create components in brand/pages/ + register | 30 min |