Building Your Own Features

Thunder UI is a normal React + Vite app - the CRUD generator removes repetition, it does not define your boundaries.

The CRUD generator removes the repetitive part - one list page and one form per backend module - and nothing more. The rest is an ordinary React 19 + Vite application.

You are free to:

  • Add pages with any content, generated or hand-written, CRUD-related or not.
  • Add components, hooks, contexts and utilities anywhere outside src/core/.
  • Install any npm dependency and use it normally.
  • Call the SDK directly, call your own endpoints, or call a third-party API.
  • Build features with no backend module behind them at all - a dashboard, a report, a wizard, a landing page.
  • Add as many layouts and themes as you want and switch between them.
  • Restyle everything through theme tokens, or ignore the tokens and write your own CSS.

The only hard rule is the one from Project Structure: don't edit src/core/.

A Plain Page

Nothing schema-driven about this one - it is just React:

src/pages/analytics.tsx
import { useEffect, useState } from "react"
import { Card, CardContent, CardHeader } from "@/components/ui/card"

export default function Analytics() {
  const [data, setData] = useState<number[]>([])

  useEffect(() => {
    fetch("/analytics/api/daily")
      .then((r) => r.json())
      .then(setData)
  }, [])

  return (
    <Card>
      <CardHeader>Daily signups</CardHeader>
      <CardContent>{data.length} days loaded</CardContent>
    </Card>
  )
}

Making It Reachable

Register it in the routes override and it appears in navigation like any generated module:

src/overrides/routes.tsx
import { IconChartBar } from "@tabler/icons-react"
import Analytics from "@/pages/analytics"
import type { TRoutesOverride } from "@/core/types"

export const routes: TRoutesOverride = {
  analytics: { name: "Analytics", icon: IconChartBar, Component: Analytics },
}

Routes are grouped by group ?? "Other", so a custom route with no group lands in a group literally named Other. Always set group unless that is what you want - see Route Overrides.

Custom pages participate in the layout's breadcrumb trail:

import { useBreadcrumbTitle } from "@/core/layouts/shared/breadcrumb"

useBreadcrumbTitle(post?.title) // sets the trailing crumb

Navigating with router state does the same thing from the outside - navigate("./123", { state: { name: "Senior Engineer" } }) sets the destination's trailing crumb.

Reusing the Generated Screens

Your own routes can still render Thunder UI's schema-driven pages. ListPage, ViewPage and FormPage are exported from core and take group and name:

import { ListPage } from "@/core/crud/ListPage.tsx"

<ListPage group="Careers" name="careers" />

That is the basis for rebuilding a module's children yourself - keep the generated screens while changing the URL layout around them.


On this page