> ## Documentation Index
> Fetch the complete documentation index at: https://ship.paralect.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Styling

> Tailwind v4, shadcn/ui, and one cn() helper. Theme tokens, not hardcoded colors.

`apps/web` styles with [Tailwind CSS v4](https://tailwindcss.com/) for utility-first classes and [shadcn/ui](https://ui.shadcn.com/) 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:

```css src/globals.css theme={null}
@import 'tailwindcss';
@plugin '@tailwindcss/typography';

@custom-variant dark (&:where(.dark, .dark *));

@theme {
  --color-background: hsl(0 0% 100%);
  --color-foreground: hsl(222.2 84% 4.9%);
  --color-primary: hsl(222.2 47.4% 11.2%);
  --color-primary-foreground: hsl(210 40% 98%);
  --color-border: hsl(214.3 31.8% 91.4%);

  --radius-lg: 0.5rem;
  --radius-md: calc(var(--radius-lg) - 2px);
  --radius-sm: calc(var(--radius-lg) - 4px);
}
```

Theme tokens defined in `@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-foreground`
* `bg-primary` / `text-primary-foreground`
* `text-muted-foreground`, `border-border`, `bg-card`

Use mobile-first breakpoints (`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:

```css src/globals.css theme={null}
@custom-variant dark (&:where(.dark, .dark *));

.dark {
  --color-background: hsl(59.31 3.23% 6.08%);
  --color-foreground: hsl(0 0% 95%);
  --color-primary: hsl(0 0% 98%);
  --color-border: hsl(240 5% 20%);
}
```

The `.dark` class is driven by [`next-themes`](https://github.com/pacocoursey/next-themes) through the `ThemeProvider` in `src/components/theme-provider.tsx`:

```tsx src/components/theme-provider.tsx theme={null}
import { ThemeProvider as NextThemesProvider } from 'next-themes';

export const ThemeProvider = ({ children, ...props }: React.ComponentProps<typeof NextThemesProvider>) => {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
};
```

Because everything reads theme tokens, no component needs to know which theme is active.

## 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:

```tsx src/lib/utils.ts theme={null}
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
```

```tsx theme={null}
import { cn } from '@/lib/utils';

<div className={cn('rounded-lg p-4', isActive && 'bg-primary text-primary-foreground')} />;
```

## shadcn/ui components

UI primitives live in `src/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:

```tsx theme={null}
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
```

Add more primitives with the [shadcn CLI](https://ui.shadcn.com/docs/cli):

```bash theme={null}
npx shadcn@latest add <component>
```

<Note>
  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.
</Note>

## Icons

Use [lucide-react](https://lucide.dev/) for icons:

```tsx theme={null}
import { Plus } from 'lucide-react';

<Button>
  <Plus className="h-4 w-4" /> Add item
</Button>;
```

## Component organisation

The web app is a [TanStack Start](https://tanstack.com/start) SPA with file-based routes in `src/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                  |

TanStack Router **ignores any folder or file prefixed with `-`**, 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.

<Tip>
  Avoid `useEffect` for styling state and data sync — there's a skill that enforces it. Reach for derived values, TanStack Query, and route loaders instead.
</Tip>
