Skip to content

Get started

From purchase to first screen in 15 minutes

Nine steps. Tick them off as you go.

0 of 9 done

  1. Accept the invite and sign in to GitHub

    After payment you get an invite from GitHub. Accept it with the account you will install from. That same account must be signed in on this computer, not another one already saved here: gh auth status shows which one is active, gh auth switch changes it. On Windows the first install opens a GitHub sign-in window; if it says Select an account, pick the one that accepted the invite (a wrong account ends in 404 or authentication failed: remove the saved github.com login in Windows Credential Manager and try again). Prefer the command line? Install the GitHub CLI from cli.github.com, run the command below, choose HTTPS and say yes to authenticating Git.

    Terminal (optional)Copy code
    gh auth login
  2. Create a React app

    Skip this if you already have a React 19 app on Node 20.19 or newer, or Node 22.12 or newer. For a new one, pick one of the two commands below, then go into the new folder with the cd command. Use TypeScript and the src folder, as the file names in the next steps do. The Next.js command leaves Tailwind off, because Tessera brings its own styles.

    ViteCopy code
    npx create-vite@latest my-app --template react-ts
    Next.jsCopy code
    npx create-next-app@latest my-app --typescript --app --src-dir --no-tailwind --yes
    TerminalCopy code
    cd my-app
  3. Install the package

    One command, run inside your app folder. React 19 and Motion are peers, and npm adds them for you. The first install takes two to three minutes and prints little, so give it time. If npm stops with UNABLE_TO_VERIFY_LEAF_SIGNATURE, your network blocks a certificate: point Node to your trusted certificates with NODE_EXTRA_CA_CERTS, and never switch TLS verification off. A GitHub sign-in problem looks different: it says 404 or authentication failed. If you build on Vercel or another server, that machine also needs read access to this private repository (a GitHub token with read access to it); write to us if you want help setting that up.

    TerminalCopy code
    npm install github:tessera-ds/tessera
  4. Add the tokens, the base styles and the theme

    Vite: replace src/main.tsx with the first file. It no longer imports index.css, because the Vite starter styles fight with Tessera; you can delete index.css. Next.js: replace the contents of src/app/layout.tsx with the second file (the fonts and the theme script are already in it) and next.config.ts with the third. If your project already has its own settings in next.config.ts, keep them and just add transpilePackages: ['tessera-ds']. Do not add ‘use client’ to layout.tsx. You can delete src/app/globals.css, nothing uses it any more. base.css gives the page its background, text colour and font, so light and dark both read correctly.

    src/main.tsx (Vite)Copy code
    import { StrictMode } from 'react'
    import { createRoot } from 'react-dom/client'
    import 'tessera-ds/tokens.css'
    import 'tessera-ds/base.css'
    import { ThemeProvider } from 'tessera-ds'
    import App from './App.tsx'
    
    createRoot(document.getElementById('root')!).render(
      <StrictMode>
        <ThemeProvider>
          <App />
        </ThemeProvider>
      </StrictMode>,
    )
    src/app/layout.tsx (Next.js)Copy code
    import type { Metadata } from 'next'
    import 'tessera-ds/tokens.css'
    import 'tessera-ds/base.css'
    import { ThemeProvider, ThemeScript } from 'tessera-ds'
    
    export const metadata: Metadata = { title: 'Invoices' }
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en" suppressHydrationWarning>
          <head>
            <ThemeScript />
            <link rel="preconnect" href="https://fonts.googleapis.com" />
            <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
            {/* eslint-disable-next-line @next/next/no-page-custom-font -- App Router: layout.tsx applies to every page */}
            <link
              href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,400..700;1,14..32,400..500&family=JetBrains+Mono:wght@400;500&display=swap"
              rel="stylesheet"
            />
          </head>
          <body>
            <ThemeProvider>{children}</ThemeProvider>
          </body>
        </html>
      )
    }
    next.config.ts (Next.js)Copy code
    const nextConfig = { transpilePackages: ['tessera-ds'] }
    
    export default nextConfig
  5. Load the fonts and the theme script (Vite)

    Tessera uses Inter and JetBrains Mono. In Vite, add these lines inside the head of index.html. The small script sets light or dark before the first paint, so dark-mode visitors never see a light flash. Next.js buyers already did this in layout.tsx. Without the fonts the text falls back to the system font.

    index.html (Vite)Copy code
    <script>(function(){try{var p=localStorage.getItem('tessera-theme');var t=p==='light'||p==='dark'?p:(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t)}catch(e){}})()</script>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,400..700;1,14..32,400..500&family=JetBrains+Mono:wght@400;500&display=swap"
      rel="stylesheet"
    />
  6. Build your first screen

    Two stat cards, a table with status badges and a payment form, with the same prop names you see in Figma. Vite: save the first box as src/Invoices.tsx, replace src/App.tsx with the second box (you can then delete App.css and src/assets), and save invoices.css in src. Next.js: save the third box as src/app/page.tsx and invoices.css in src/app, and delete src/app/page.module.css, the starter’s unused stylesheet, so the token check in the last steps stays clean. It starts with ‘use client’ because the table gets functions. Start it with npm run dev and open the address it prints: you should see two cards, three invoices and an IBAN field. The Pay invoice button is only a demo and does nothing yet. Run npm run build to check that everything compiles. Then swap in your own data.

    src/Invoices.tsx (Vite)Copy code
    import { Badge, Button, Input, StatCard, Table } from 'tessera-ds'
    import './invoices.css'
    
    type Invoice = { id: string; client: string; amount: string; status: 'paid' | 'open' | 'late' }
    
    const invoices: Invoice[] = [
      { id: 'INV-2041', client: 'Alpwerk AG', amount: '4,280.00', status: 'paid' },
      { id: 'INV-2042', client: 'Seeblick Treuhand', amount: '1,950.50', status: 'open' },
      { id: 'INV-2043', client: 'Rhein Versicherungen', amount: '12,400.00', status: 'late' },
    ]
    
    const STATUS = {
      paid: { tone: 'success', label: 'Paid' },
      open: { tone: 'info', label: 'Open' },
      late: { tone: 'danger', label: 'Late' },
    } as const
    
    export default function Invoices() {
      return (
        <main className="invoices">
          <h1>Invoices</h1>
          <div className="invoices__stats">
            <StatCard stat="CHF 18,630.50" label="Billed this month" sublabel="3 invoices" />
            <StatCard stat="CHF 12,400" label="Overdue" sublabel="1 invoice" />
          </div>
          <Table
            aria-label="Invoices"
            getRowId={(r: Invoice) => r.id}
            rows={invoices}
            emptyState="No invoices yet"
            columns={[
              { id: 'id', header: 'Invoice', cell: (r: Invoice) => r.id },
              { id: 'client', header: 'Client', cell: (r: Invoice) => r.client },
              { id: 'amount', header: 'Amount (CHF)', numeric: true, cell: (r: Invoice) => r.amount },
              { id: 'status', header: 'Status', cell: (r: Invoice) => <Badge size="sm" status={STATUS[r.status].tone}>{STATUS[r.status].label}</Badge> },
            ]}
          />
          <form className="invoices__pay" onSubmit={(e) => e.preventDefault()}>
            <Input label="IBAN" placeholder="CH93 0076 2011 6238 5295 7" />
            <Button type="submit" variant="brand" size="md">Pay invoice</Button>
          </form>
        </main>
      )
    }
    src/App.tsx (Vite)Copy code
    import Invoices from './Invoices'
    
    export default function App() {
      return <Invoices />
    }
    src/app/page.tsx (Next.js)Copy code
    'use client'
    
    import { Badge, Button, Input, StatCard, Table } from 'tessera-ds'
    import './invoices.css'
    
    type Invoice = { id: string; client: string; amount: string; status: 'paid' | 'open' | 'late' }
    
    const invoices: Invoice[] = [
      { id: 'INV-2041', client: 'Alpwerk AG', amount: '4,280.00', status: 'paid' },
      { id: 'INV-2042', client: 'Seeblick Treuhand', amount: '1,950.50', status: 'open' },
      { id: 'INV-2043', client: 'Rhein Versicherungen', amount: '12,400.00', status: 'late' },
    ]
    
    const STATUS = {
      paid: { tone: 'success', label: 'Paid' },
      open: { tone: 'info', label: 'Open' },
      late: { tone: 'danger', label: 'Late' },
    } as const
    
    export default function Invoices() {
      return (
        <main className="invoices">
          <h1>Invoices</h1>
          <div className="invoices__stats">
            <StatCard stat="CHF 18,630.50" label="Billed this month" sublabel="3 invoices" />
            <StatCard stat="CHF 12,400" label="Overdue" sublabel="1 invoice" />
          </div>
          <Table
            aria-label="Invoices"
            getRowId={(r: Invoice) => r.id}
            rows={invoices}
            emptyState="No invoices yet"
            columns={[
              { id: 'id', header: 'Invoice', cell: (r: Invoice) => r.id },
              { id: 'client', header: 'Client', cell: (r: Invoice) => r.client },
              { id: 'amount', header: 'Amount (CHF)', numeric: true, cell: (r: Invoice) => r.amount },
              { id: 'status', header: 'Status', cell: (r: Invoice) => <Badge size="sm" status={STATUS[r.status].tone}>{STATUS[r.status].label}</Badge> },
            ]}
          />
          <form className="invoices__pay" onSubmit={(e) => e.preventDefault()}>
            <Input label="IBAN" placeholder="CH93 0076 2011 6238 5295 7" />
            <Button type="submit" variant="brand" size="md">Pay invoice</Button>
          </form>
        </main>
      )
    }
    invoices.cssCopy code
    .invoices {
      display: grid;
      gap: var(--space-600);
      max-width: calc(var(--space-600) * 32);
      margin: 0 auto;
      padding: var(--space-800) var(--space-600);
    }
    
    .invoices h1 {
      margin: 0;
    }
    
    /* Two cards side by side, one under the other on a phone. */
    .invoices__stats {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(calc(var(--space-600) * 10), 1fr));
      gap: var(--space-400);
    }
    
    .invoices__pay {
      display: flex;
      flex-wrap: wrap;
      align-items: flex-end;
      gap: var(--space-300);
    }
    TerminalCopy code
    npm run dev
    TerminalCopy code
    npm run build
  7. Open the Figma file

    Optional. Your code setup is complete after the previous step. Duplicate the file from your order page. Variables switch light and dark; component properties use the code’s words.

  8. Build with Claude

    Optional. Copy the Claude pack into your project. Your agent then uses the components and tokens instead of raw values. The second command checks your CSS for raw colours, pixel sizes and durations. On the fresh screen from this guide it passes; on older CSS of your own it lists what to change. On Windows Command Prompt, use copy or run the first command in PowerShell.

    TerminalCopy code
    cp -r node_modules/tessera-ds/claude/. .claude/
    npx tessera-lint src
  9. Explore every component

    Every component has its states, do and don’t, keyboard and accessibility notes in the documentation.

A support specialist with a headset looks into the camera with a calm, warm smile.

Talk to a person

Tell us what you need. A person answers within three working days, within two for Team licences.

Close