Skip to content
Build With SyntaxKit

Customization

Retheme with OKLCH tokens in one CSS file, swap fonts and dark mode, extend shadcn primitives in packages/ui, and drive homepage branding from brand and marketing config.

Last updated on

6 min read

On This Page

How Customization Is Layered

Theme tokens flow into base components flow into app components flow into routes. App and marketing config layer in beside the components.

Four layers, top to bottom:

  1. Tokens. CSS variables in packages/ui/src/styles/globals.css (colors, radius, fonts).
  2. Primitives. shadcn-style components in packages/ui/src/components/ that consume those tokens.
  3. App components. Product UI in apps/web/components/ that composes the primitives.
  4. Routes. Next.js pages that mount the app components.

Brand and marketing config sit beside the components. They supply strings, ordering, and assets without editing component source. Always edit the lowest layer that solves the problem.

Package Layout

index.tsBrand name, wordmark, URLs, social handles, legal entity
components.jsonshadcn CLI config: style, baseColor, aliases, registries
icons.tsRe-exports lucide-react as @syntaxkit/ui/icons

Brand Tokens

Every color, radius, and font resolves to a CSS variable in packages/ui/src/styles/globals.css. Tailwind v4 reads them through @theme inline, so utilities like bg-primary and rounded-lg track whatever you set in :root and .dark.

Fastest full retheme: the shadcn/ui Create tool. Pick style, colors, fonts, and radius, then apply the preset to the kit.

Configure your theme

Open ui.shadcn.com/create. Choose style, base color, primary and accent, chart palette, fonts, icon library, and radius.

Copy the preset code

Copy the preset identifier the page generates.

Apply it to the kit

Run the shadcn CLI scoped to @syntaxkit/ui so it uses packages/ui/components.json:

pnpm --filter @syntaxkit/ui dlx shadcn@latest init --preset [CODE]

The CLI rewrites the :root and .dark blocks in globals.css and updates font/icon entries in components.json.

Reload

Tailwind picks up the change on the next page reload. No rebuild and no tailwind.config.js.

Tailwind v4 reads tokens from CSS via @theme inline. The kit ships no tailwind.config.js. The top of packages/ui/src/styles/globals.css (@import "tailwindcss", @source ..., @theme inline { ... }) replaces the old JS config.

Fonts

The product app loads Geist and Geist Mono with Next font in apps/web/app/layout.tsx and exposes them as CSS variables on <body>:

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

@theme inline in globals.css aliases --font-sans and --font-mono to those variables, so font-sans and font-mono resolve correctly. To swap fonts, change the import in layout.tsx and keep the same variable names.

Light And Dark Mode

Dark mode uses next-themes, mounted in apps/web/app/layout.tsx:

        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >

attribute="class" toggles a dark class on <html>, which activates the .dark block in globals.css. defaultTheme="system" follows the OS until the user picks a theme. disableTransitionOnChange avoids a flash on swap.

The shipped toggle is apps/web/components/theme-toggle.tsx. It uses useTheme from @syntaxkit/ui/components/theme-provider:

import { ThemeToggle } from "@/components/theme-toggle";

export function Header() {
  return (
    <header>
      {/* ... */}
      <ThemeToggle />
    </header>
  );
}

Base Components

Primitives live in packages/ui/src/components/. They are shadcn-style: source in your repo, not a versioned package. Each one combines Radix UI behavior, class-variance-authority for typed variants, and cn for class merging. button.tsx is the reference:

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive:
          "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
        outline:
          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost:
          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2 has-[>svg]:px-3",
        xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
        icon: "size-9",
        "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
        "icon-sm": "size-8",
        "icon-lg": "size-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

Variants and sizes are typed through VariantProps<typeof buttonVariants>. To add a variant:

Edit the cva block

In packages/ui/src/components/button.tsx, add a key under variants.variant:

gradient:
  "bg-gradient-to-r from-primary to-chart-2 text-primary-foreground hover:opacity-90",

Use it

<Button variant="gradient">Upgrade</Button>

TypeScript narrows the union from cva. Misspelled variants fail at compile time.

cn (packages/ui/src/lib/utils.ts) is clsx plus tailwind-merge. Caller classes win for the same utility: <Button className="rounded-none"> overrides the variant's rounded-md.

Composing App Components

App components live in apps/web/components/ and import from @syntaxkit/ui/components/<name>. Most day-to-day UI work happens here.

Logo (apps/web/components/logo.tsx). Reads text-primary for the colored half of the wordmark. Change it once to rebrand headers, footers, and auth screens.

import { cn } from "@syntaxkit/ui/lib/utils";

export const Logo = ({ className, ...props }: React.ComponentProps<"span">) => (
  <span className={cn("font-bold tracking-tight", className)} {...props}>
    Syntax<span className="text-primary">Kit</span>
  </span>
);

ThemeToggle (apps/web/components/theme-toggle.tsx). Composes <Button>, <DropdownMenu>, and useTheme. Use it as a template when you need a primitive plus a Radix menu and local state.

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon-sm">
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Toggle theme</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>
          Light
          {theme === "light" && <Check className="ml-auto size-4" />}
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>
          Dark
          {theme === "dark" && <Check className="ml-auto size-4" />}
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>
          System
          {theme === "system" && <Check className="ml-auto size-4" />}
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

Same folder for product surfaces: marketing/*, dashboard/*, auth/*. New product components go here with the kebab-case filenames from Conventions.

Marketing And Site Config

Two TypeScript objects own most non-component branding. Edit either to change the homepage without touching components. The dev server hot-reloads both.

ConfigPathOwns
Brandpackages/brand/src/index.tsName, wordmark split, description, app/docs/pricing URLs, social handles, legal entity, SEO defaults. Used by metadata, OpenGraph, Logo, footers, email, and legal pages.
Marketingapps/web/config/marketing.tsHomepage section visibility and order, hero badge href, feature lists, pricing tiers, testimonials, FAQ. Structural and asset choices only; translatable copy lives in messages/*.json.

Dashboard sidebar groups and breadcrumb labels: apps/web/config/navigation.ts.

Adding A New shadcn Primitive

When you need a component the kit does not ship (Calendar, Carousel, AI Elements), add it into packages/ui:

pnpm --filter @syntaxkit/ui dlx shadcn@latest add <component>

The CLI reads packages/ui/components.json for style, base color, aliases, and registries:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/styles/globals.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "iconLibrary": "lucide",
  "aliases": {
    "components": "@syntaxkit/ui/components",
    "utils": "@syntaxkit/ui/lib/utils",
    "ui": "@syntaxkit/ui/components",
    "lib": "@syntaxkit/ui/lib",
    "hooks": "@syntaxkit/ui/hooks"
  },
  "registries": {
    "@ai-elements": "https://ai-sdk.dev/elements/api/registry/{name}.json"
  }
}

New files land in packages/ui/src/components/. Import as @syntaxkit/ui/components/<name>. The AI Elements registry is already wired:

pnpm --filter @syntaxkit/ui dlx shadcn@latest add @ai-elements/conversation

That pulls chat primitives (Conversation, Message, PromptInput, Suggestion) into the same folder. The kit's AI chat surface uses them; see AI.

Where To Go Next

Also useful: Internationalization, AI, and Authentication.

Was this page helpful?

On this page