apps/web styles with Tailwind CSS v4 for utility-first classes and shadcn/ui for accessible, owned-in-your-repo components. No CSS modules, no styled-components — utilities and semantic theme tokens.
Tailwind v4
Tailwind is wired in through the Vite plugin (@tailwindcss/vite) and a single stylesheet, src/globals.css, that imports the framework and declares your theme:
src/globals.css
@theme become utilities automatically — --color-primary gives you bg-primary / text-primary, --radius-md gives you rounded-md. Style with these semantic tokens, never hardcoded hex values, so light and dark modes stay consistent:
bg-background/text-foregroundbg-primary/text-primary-foregroundtext-muted-foreground,border-border,bg-card
sm:, md:, lg:) and state variants (hover:, focus-visible:, disabled:) as usual.
Dark mode
Dark mode is class-based.globals.css registers a dark variant and redefines the same tokens under .dark, so every component reacts to one class toggle:
src/globals.css
.dark class is driven by next-themes through the ThemeProvider in src/components/theme-provider.tsx:
src/components/theme-provider.tsx
Conditional classes with cn()
Compose and de-conflict classes with cn() from @/lib/utils — a clsx + tailwind-merge wrapper, so the last conflicting utility wins:
src/lib/utils.ts
shadcn/ui components
UI primitives live insrc/components/ui/ — button.tsx, input.tsx, card.tsx, dialog.tsx, form.tsx, table.tsx, and more. They’re plain files in your repo (like everything in Ship), so you own and can edit them. Import with the @/ alias:
Prefer wrapping shadcn primitives in your own components over editing them in place — it keeps the base components clean and your customisations in one obvious spot.
Icons
Use lucide-react for icons:Component organisation
The web app is a TanStack Start SPA with file-based routes insrc/routes/**. Components live in two places — shared ones under src/components, and route-private ones colocated with the route that uses them:
| Location | Scope | Import pattern |
|---|---|---|
src/components/ui/ | shadcn/ui primitives | @/components/ui/button |
src/components/ | shared app components | 'components' barrel (src/components/index.ts) |
src/routes/-<name>/ | route-private components | relative import within the route |
-, so route-private pieces never become routes. Colocate them next to the route — e.g. src/routes/-sign-up-components/ holds the components only the sign-up route needs.
