Supasheet.

Custom Components

Extend the UI when auto-generated interfaces aren't enough

When to Use Custom Components

While Supasheet auto-generates most UI, you may need custom components for:

  • Complex workflows or multi-step processes
  • Custom visualizations beyond standard charts
  • Integration with third-party services
  • Specialized input controls
  • Custom business logic presentation

Creating Custom Pages

Supasheet uses TanStack Router with file-based routing. Add a new file under src/routes/ to create a new page.

Basic Custom Page

// src/routes/custom-feature/index.tsx
import { createFileRoute } from '@tanstack/react-router';
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase/client';

export const Route = createFileRoute('/custom-feature/')({
  component: CustomFeaturePage,
});

function CustomFeaturePage() {
  const { data: products } = useQuery({
    queryKey: ['products'],
    queryFn: async () => {
      const { data } = await supabase.from('products').select('*').limit(10);
      return data;
    },
  });

  return (
    <div className="container mx-auto p-6">
      <h1 className="text-2xl font-bold mb-4">Custom Feature</h1>
      <div className="grid grid-cols-3 gap-4">
        {products?.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

With Mutations

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase/client';

export function DeleteButton({ id }: { id: string }) {
  const queryClient = useQueryClient();

  const { mutate, isPending } = useMutation({
    mutationFn: async (id: string) => {
      const { error } = await supabase.from('products').delete().eq('id', id);
      if (error) throw error;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['products'] });
    },
  });

  return (
    <button onClick={() => mutate(id)} disabled={isPending}>
      {isPending ? 'Deleting...' : 'Delete'}
    </button>
  );
}

Custom Forms

Using TanStack Form

import { useForm } from '@tanstack/react-form';
import { supabase } from '@/lib/supabase/client';

export function CustomOrderForm() {
  const form = useForm({
    defaultValues: {
      name: '',
      email: '',
      quantity: 1,
    },
    onSubmit: async ({ value }) => {
      const { error } = await supabase.from('orders').insert(value);
      if (error) console.error(error);
    },
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        form.handleSubmit();
      }}
      className="space-y-4"
    >
      <form.Field name="name">
        {(field) => (
          <div>
            <input
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
              placeholder="Name"
            />
            {field.state.meta.errors && (
              <span className="text-red-500">{field.state.meta.errors[0]}</span>
            )}
          </div>
        )}
      </form.Field>

      <form.Field name="email">
        {(field) => (
          <input
            value={field.state.value}
            onChange={(e) => field.handleChange(e.target.value)}
            placeholder="Email"
          />
        )}
      </form.Field>

      <form.Field name="quantity">
        {(field) => (
          <input
            type="number"
            value={field.state.value}
            onChange={(e) => field.handleChange(Number(e.target.value))}
          />
        )}
      </form.Field>

      <form.Subscribe selector={(state) => state.isSubmitting}>
        {(isSubmitting) => (
          <button type="submit" disabled={isSubmitting}>
            {isSubmitting ? 'Submitting...' : 'Submit'}
          </button>
        )}
      </form.Subscribe>
    </form>
  );
}

Multi-Step Form

import { useState } from 'react';
import { useForm } from '@tanstack/react-form';

export function MultiStepForm() {
  const [step, setStep] = useState(1);

  const step1 = useForm({
    defaultValues: { name: '', email: '' },
    onSubmit: () => setStep(2),
  });

  const step2 = useForm({
    defaultValues: { address: '', city: '' },
    onSubmit: async ({ value }) => {
      // combine and submit
    },
  });

  return (
    <div>
      {step === 1 && (
        <form onSubmit={(e) => { e.preventDefault(); step1.handleSubmit(); }}>
          <step1.Field name="name">
            {(field) => (
              <input
                value={field.state.value}
                onChange={(e) => field.handleChange(e.target.value)}
                placeholder="Name"
              />
            )}
          </step1.Field>
          <button type="submit">Next</button>
        </form>
      )}

      {step === 2 && (
        <form onSubmit={(e) => { e.preventDefault(); step2.handleSubmit(); }}>
          <step2.Field name="address">
            {(field) => (
              <input
                value={field.state.value}
                onChange={(e) => field.handleChange(e.target.value)}
                placeholder="Address"
              />
            )}
          </step2.Field>
          <button type="button" onClick={() => setStep(1)}>Back</button>
          <button type="submit">Submit</button>
        </form>
      )}
    </div>
  );
}

Custom Data Tables

Using TanStack Table

import { useDataTable } from '@/components/data-table/hooks/use-data-table';
import { DataTable } from '@/components/data-table/components/data-table';

export function CustomDataTable({ data }: { data: any[] }) {
  const table = useDataTable({
    data,
    columns: [
      {
        accessorKey: 'name',
        header: 'Name',
      },
      {
        accessorKey: 'status',
        header: 'Status',
        cell: ({ row }) => (
          <span className={`px-2 py-1 rounded ${
            row.original.status === 'active' ? 'bg-green-100' : 'bg-red-100'
          }`}>
            {row.original.status}
          </span>
        ),
      },
      {
        id: 'actions',
        cell: ({ row }) => (
          <button onClick={() => handleEdit(row.original)}>Edit</button>
        ),
      },
    ],
  });

  return <DataTable table={table} />;
}

Custom Charts

Using Recharts

import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase/client';

export function RevenueChart() {
  const { data } = useQuery({
    queryKey: ['revenue-chart'],
    queryFn: async () => {
      const { data } = await supabase
        .from('daily_revenue')
        .select('*')
        .order('date', { ascending: true });
      return data;
    },
  });

  return (
    <LineChart width={800} height={400} data={data}>
      <CartesianGrid strokeDasharray="3 3" />
      <XAxis dataKey="date" />
      <YAxis />
      <Tooltip />
      <Line type="monotone" dataKey="revenue" stroke="#8884d8" />
    </LineChart>
  );
}

Styling Custom Components

Using Tailwind CSS

export function StyledCard({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <div className="rounded-lg border bg-card p-6 shadow-sm">
      <h3 className="text-lg font-semibold mb-4">{title}</h3>
      <div className="space-y-4">{children}</div>
    </div>
  );
}

Using shadcn Components

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

export function ShippingForm() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Shipping Information</CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        <Input placeholder="Address" />
        <Input placeholder="City" />
        <Input placeholder="Postal Code" />
        <Button>Save Address</Button>
      </CardContent>
    </Card>
  );
}

Adding to Navigation

To add custom pages to the sidebar, modify src/components/layouts/default-sidebar.tsx:

// src/components/layouts/default-sidebar.tsx
import { Package } from 'lucide-react';

const data = {
  navMain: [
    {
      title: 'Dashboard',
      url: '/dashboard',
      icon: HomeIcon,
    },
    // ...
  ],
  navSecondary: [
    {
      title: 'Storage',
      url: '/storage',
      icon: FolderIcon,
    },
    {
      title: 'Audit Logs',
      url: '/core/audit_logs',
      icon: ScrollTextIcon,
    },
    // Add your custom navigation item here
    {
      title: 'Custom Feature',
      url: '/custom-feature',
      icon: Package,
    },
  ],
};

The sidebar is structured with:

  • NavMain - Primary navigation (Dashboard, Chart, Report)
  • NavResources - Auto-generated resource navigation
  • NavSecondary - Secondary items (Storage, Audit Logs), add custom pages here

Next Steps