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:
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:
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.
Breadcrumbs From a Custom Page
Custom pages participate in the layout's breadcrumb trail:
import { useBreadcrumbTitle } from "@/core/layouts/shared/breadcrumb"
useBreadcrumbTitle(post?.title) // sets the trailing crumbNavigating 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.