Skip to main content
This guide is for developers who want to write code directly in the Vibe Coding Block, rather than prompting the AI. Use it when you need full control over your block’s markup, logic, and data.

The Basics

Block’s source code is a TypeScript file with a default-exported React component:
export default function Block() {
  return (
    <div className="container">
      <div className="content">
        <h1 className="text-4xl font-extrabold tracking-tight text-balance">
          Hello, world!
        </h1>
      </div>
    </div>
  );
}
This only runs in the browser - you can fetch and mutate data from connected data sources, but you cannot run server-side code or use Node.js APIs. Vibe coding block is configured to use Tailwind for styling and shadcn/ui for components, but you are free to import any public npm package as needed. In fact, any npm package import you add will automatically install it for you.
import { format } from "date-fns"; // will be resolved at compile time

export default function Block() {
  const today = format(new Date(), "MMMM d, yyyy");
  return <div>Today's date is {today}</div>;
}

shadcn/ui

shadcn/ui components are already pre-configured and follow your app’s theme out of the box. They’re available under the @/components/ui path, so you can import them like this:
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
Currently the following components are available: accordion, alert, alert-dialog, aspect-ratio, avatar, badge, button, calendar, card, carousel, chart, checkbox, collapsible, command, context-menu, dialog, drawer, dropdown-menu, empty, hover-card, input, input-group, input-otp, item, kbd, label, menubar, native-select, navigation-menu, pagination, popover, progress, radio-group, resizable, scroll-area, select, separator, sheet, skeleton, slider, sonner, spinner, switch, table, tabs, textarea, toggle, toggle-group, tooltip Check out the shadcn/ui docs for usage details and examples for each component.

Icons

Preferred icon pack is Lucide Icons, but you can opt for a different one.
import { TrendingUp, User, Settings } from "lucide-react";

Styling

We follow the default shadcn/ui naming convention for background/foreground color pairs (e.g. bg-primary / text-primary-foreground) which maps to your app’s theme colors. By default, block occupies full width of the page but special classes - container and content are available to constrain the width of content to match app’s max width settings to ensure visual consistency with other blocks:
<div className="container">
  <div className="content">
    {/* your content here */}
  </div>
</div>

Data from Your Datasource

Import data hooks from @/lib/datasource. Use these when you want to display records from your connected data source. All data fetching hooks follow a query builder pattern so you can be quite expressive with your queries.

Defining a select query

Field mappings have to be static, we also use static analysis to determine which fields your block actually uses so we don’t overfetch and don’t accidentally expose potentially sensitive data.
import { q } from "@/lib/datasource";

const select = q.select({
  title: "FIELD_ID1",    // key = alias used in code, value = actual field ID
  description: "FIELD_ID2",
  createdAt: "FIELD_ID3",
});
So something like this is not allowed as it breaks static analysis:
const select = q.select({
  [dynamicKey]: "FIELD_ID1",   // ❌ dynamic keys not allowed
  title: getFieldId(),         // ❌ dynamic values not allowed
});

useRecords — fetch a list of records

import { useRecords, q } from "@/lib/datasource";

export default function Block() {
  const {
    data,                // array of pages with shape { items: Record[]; total: number; offset: string | number | null }
    status,              // "pending" | "success" | "error"
    error,               // Error object if status is "error"
    fetchNextPage,       // function to fetch the next page of results
    hasNextPage,         // boolean indicating if there are more pages to fetch
    isFetching,          // boolean indicating if any page is currently being fetched
    isFetchingNextPage,  // boolean indicating if the next page is currently being fetched
    refetch,             // function to refetch the data (e.g. after a mutation)
    isRefetching,        // boolean indicating if a refetch is currently in progress
    isRefetchError,      // boolean indicating if the last refetch resulted in an error
  } = useRecords({
    select: q.select({
      name: "FIELD_ID1",
      email: "FIELD_ID2",
    }),
    count: 6,            // records per page, default 6, max 100
    // where: ...,       // optional filter
    // orderBy: ...,     // optional sort
    // enabled: ...,     // optional boolean to defer loading (e.g. until component is visible)
  });

  if (status === "pending") return <div>Loading...</div>;
  if (status === "error") return <div>Error loading data</div>;

  const items = data.pages.flatMap(page => page.items);

  return (
    <div className="container py-8">
      <div className="content space-y-4">
        {items.map((item) => (
          <div key={item.id}>
            <p className="font-semibold">{item.fields.name}</p>
            <p className="text-muted-foreground">{item.fields.email}</p>
          </div>
        ))}
        {hasNextPage && (
          <button onClick={() => fetchNextPage()} disabled={isFetching}>
            {isFetchingNextPage ? "Loading..." : "Load More"}
          </button>
        )}
      </div>
    </div>
  );
}

useRecord — fetch a single record by ID

Use with useCurrentRecordId() to display details of the record currently shown in a list/detail context:
import { useRecord, useCurrentRecordId, q } from "@/lib/datasource";

export default function Block() {
  const recordId = useCurrentRecordId(); // resolves the ID of the current record from page, can also be null
  const { data, status } = useRecord({
    select: q.select({
      title: "FIELD_ID1",
      description: "FIELD_ID2",
    }),
    recordId,
  });

  if (status === "pending") return <div>Loading...</div>;
  if (status === "error") return <div>Error</div>;
  if (!data) return <div>Not found</div>;

  return (
    <div className="container py-8">
      <div className="content">
        <h1>{data.fields.title}</h1>
        <p>{data.fields.description}</p>
      </div>
    </div>
  );
}

Filtering records

Use the q query builder for filters. Filters support up to 2 levels of nesting.
useRecords({
  select,
  where: q.and(
    q.text("name").contains("Alice"),
    q.number("age").gte(18),
    q.or(
      q.boolean("isActive").is(true),
      q.text("notes").isNotEmpty()
    )
  ),
});
Available filter methods:
BuilderMethods
q.text(field)is, isNot, contains, startsWith, endsWith, isOneOf, isNoneOf, hasAllOf, isEmpty, isNotEmpty
q.number(field)is, isNot, gt, gte, lt, lte, between, isEmpty, isNotEmpty
q.boolean(field)is, isNot, isEmpty, isNotEmpty
q.date(field)is, isNot, gt, gte, lt, lte, between, isNotBetween, isEmpty, isNotEmpty
q.array(field)is, isOneOf, isNoneOf, hasAllOf, isEmpty, isNotEmpty
q.and(...)combine filters with AND
q.or(...)combine filters with OR

Sorting records

useRecords({
  select,
  orderBy: q.desc("createdAt"), // or q.asc("createdAt")
});

// Multiple sort fields (tie-breakers):
useRecords({
  select,
  orderBy: [q.asc("lastName"), q.asc("firstName")],
});

useLinkedRecords — fetch options from a linked table

Use for dropdowns, comboboxes, or tag pickers where you need to show values from a related table:
import { q, useLinkedRecords } from "@/lib/datasource";

export default function Block() {
  const {
    data,
    status,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    refetch,
    isRefetching,
    isRefetchError,
  } = useLinkedRecords({
    select: q.select({ category: "$CATEGORY_FIELD_ID" }),

    field: "category",    // alias from select that holds the linked field
    sortOrder: "ASC",     // "ASC" | "DESC"
    search: "",           // optional search term
    enabled: true,        // set false to defer loading (e.g. until dropdown opens)
    // count: ...,        // optional page size, defaults to 100, max 1000
  });

  const options = data?.pages.flatMap(p => p.items) ?? [];

  return (
    <div>
      {options.map(opt => (
        <span key={opt.id}>{opt.title}</span>
      ))}
    </div>
  );
}

Mutating Records

All mutation hooks expose an enabled boolean — always check it before rendering the mutation UI or calling the function. It reflects whether the current user has sufficient permissions. If called without checking enabled, the mutation will throw an error.

useRecordCreate

import { useRecordCreate, q } from "@/lib/datasource";
import { toast } from "sonner";

export default function Block() {
  const {
    enabled,      // boolean indicating if the user has permission to create records
    mutate,       // void function to create a record, accepts an object with shape from provided fields, in this case - `{ name: string; email: string }`
    mutateAsync,  // async version of mutate that returns a promise with the created record
    status,       // "idle" | "pending" | "success" | "error"
    error,        // Error object if status is "error"
    reset,        // function to reset the status and error state back to "idle" and null, useful for showing multiple create forms in a row
  } = useRecordCreate({
    fields: q.select({
      name: "FIELD_ID1",
      email: "FIELD_ID2",
    }),
    onSuccess: (newRecord) => toast.success("Created!"),
    onError: (error) => toast.error(error.message),
  });

  return (
    <div>
      {enabled && (
        <button
          onClick={() => mutate({ name: "Jane", email: "jane@example.com" })}
          disabled={status === "pending"}
        >
          {status === "pending" ? "Saving..." : "Add Record"}
        </button>
      )}
    </div>
  );
}

useRecordUpdate

import { useRecordUpdate, q } from "@/lib/datasource";

export default function Block() {
  const { data, refetch } = useRecords({ ... });
  const {
    enabled,
    mutate,
    mutateAsync,
    status,
    error,
    reset,
  } = useRecordUpdate({
    fields: q.select({ status: "FIELD_ID1" }),
    onSuccess: async (updatedRecord) => {
      // refetch the data first to ensure the UI shows the latest value before notifying the user.
      await refetch();
      toast.success("Updated!");
    },
    onError: (error) => toast.error(error.message),
  });

  const onUpdateClick = () => {
    if (!enabled) return;
    mutate({
      recordId: "RECORD_ID", // ID of the record to update
      fields: { status: "active" }, // updated field values
    });
  };
}

useRecordDelete

import { useRecordDelete } from "@/lib/datasource";

export default function Block() {
  const {
    enabled,
    mutate,
    mutateAsync,
    status,
    error,
    reset,
  } = useRecordDelete({
    onSuccess: async ({ recordId }) => {
      await refetch();
      toast.success("Deleted!");
    },
    onError: (error) => toast.error(error.message),
  });

  const onDeleteClick = () => {
    if (!enabled) return;
    mutate("RECORD_ID"); // ID of the record to delete
  };
}

Uploading Files

Use useUpload from @/lib/datasource to upload files and get back a URL to store in a record.
import { useUpload, useRecordCreate, q } from "@/lib/datasource";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { useState } from "react";

const fields = q.select({
  name: "$NAME_FIELD_ID",
  attachment: "$ATTACHMENT_FIELD_ID",
});

export default function Block() {
  const [name, setName] = useState("");
  const [file, setFile] = useState<File | null>(null);
  const { uploadAsync, isUploading } = useUpload();
  const createRecord = useRecordCreate({
    fields,
    onSuccess: () => toast.success("Submitted!"),
  });

  const handleSubmit = async () => {
    if (!file) return;
    const [result] = await uploadAsync(file);
    if (result.status === "completed") {
      createRecord.mutate({
        name,
        attachment: { filename: result.file.name, url: result.url },
      });
    } else {
      toast.error(result.error?.message ?? "Upload failed");
    }
  };

  return (
    <div className="container py-6">
      <div className="content space-y-4 max-w-md">
        <Input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
        <Input type="file" onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
        <Button onClick={handleSubmit} disabled={isUploading || !file}>
          {isUploading ? "Uploading..." : "Submit"}
        </Button>
      </div>
    </div>
  );
}
For multiple files:
const results = await uploadAsync(Array.from(e.target.files));
const completed = results.filter(r => r.status === "completed");

Current User

Get info about the logged-in user with useCurrentUser from @/lib/user. Returns null if no user is logged in.
import { useCurrentUser } from "@/lib/user";

export default function Block() {
  const user = useCurrentUser();

  if (!user) return <div>Please log in to continue.</div>;

  return (
    <div className="container py-8">
      <div className="content flex items-center gap-4">
        {user.avatar && (
          <img src={user.avatar} alt={user.fullName ?? ""} className="w-10 h-10 rounded-full" />
        )}
        <div>
          <p className="font-semibold">{user.fullName}</p>
          <p className="text-muted-foreground text-sm">{user.email}</p>
        </div>
      </div>
    </div>
  );
}
Available fields:
  • id: string | null (only present when user sync is enabled)
  • fullName: string | null
  • email: string | null
  • avatar: string | null

Metrics & Charts

useMetric — single aggregated value

Useful for KPI cards (total sales, average rating, etc.):
import { useMetric, q, metric } from "@/lib/datasource";

export default function Block() {
  const { data, status } = useMetric({
    select: q.select({ revenue: "$REVENUE_FIELD_ID" }),
    metric: metric.sum("revenue"),
    // where: q.date("createdAt").gte("2025-01-01"),
  });

  if (status === "pending") return <div>Loading...</div>;

  return <div className="text-4xl font-bold">${data?.toFixed(2)}</div>;
}
Aggregations: metric.sum(field), metric.avg(field), metric.max(field), metric.min(field), metric.distinct(field), metric.count()

useChartData — grouped data for charts

import { useChartData, q, metric } from "@/lib/datasource";
import { LineChart, Line, XAxis, CartesianGrid } from "recharts";
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/chart";

const chartConfig = {
  revenue: { label: "Revenue", color: "var(--chart-1)" },
} satisfies ChartConfig;

export default function Block() {
  const { data, status } = useChartData({
    select: q.select({
      date: "$DATE_FIELD_ID",
      revenue: "$REVENUE_FIELD_ID",
    }),
    orderBy: q.asc("date"),
    metric: { revenue: metric.sum("revenue") },
    groupBy: metric.groupBy("date", metric.bucket.month.long),
  });

  if (status === "pending") return <div>Loading...</div>;

  return (
    <div className="container py-8">
      <div className="content">
        <ChartContainer config={chartConfig}>
          <LineChart data={data} margin={{ left: 12, right: 12 }}>
            <CartesianGrid vertical={false} />
            <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
            <ChartTooltip content={<ChartTooltipContent />} />
            <Line dataKey="revenue" stroke="var(--color-revenue)" strokeWidth={2} dot={false} />
          </LineChart>
        </ChartContainer>
      </div>
    </div>
  );
}
Grouping buckets:
BucketFormat example
metric.bucket.year2025
metric.bucket.month.iso"2025-03"
metric.bucket.month.long"March 2025"
metric.bucket.day.iso"2025-03-15"
metric.bucket.day.long"Mar 15, 2025"

Editable Settings

Editable settings let builders modify block content through the editor UI (Content → Settings tab) without touching code. Always use them for any text, images, icons, or lists that might change between block instances. Import from @/lib/editable-settings.

useTextSetting

Returns a string. Use for titles, descriptions, button labels, URLs, etc.
import { useTextSetting } from "@/lib/editable-settings";

export default function Block() {
  const title = useTextSetting({
    name: "title",           // unique identifier — changing this resets the value
    label: "Title",          // shown in the editor UI
    initialValue: "Welcome", // starting value
    required: false,         // optional, default false
  });

  return <h1>{title}</h1>;
}

useImageSetting

Returns { src: string; alt: string }.
import { useImageSetting } from "@/lib/editable-settings";

export default function Block() {
  const image = useImageSetting({
    name: "hero-image",
    label: "Hero Image",
    initialValue: {
      src: "https://images.unsplash.com/photo-...",
      alt: "A hero image",
    },
  });

  return <img src={image.src} alt={image.alt} className="w-full rounded-lg" />;
}

useVideoSetting

Returns { src: string }.
import { useVideoSetting } from "@/lib/editable-settings";

export default function Block() {
  const video = useVideoSetting({
    name: "intro-video",
    label: "Intro Video",
    initialValue: { src: "https://example.com/video.mp4" },
  });

  return <video src={video.src} controls className="w-full" />;
}

useVibeCodingBlockIconSetting

Returns { icon: string } where icon is a lucide-react icon name. Render it with the DynamicIcon component.
import { useVibeCodingBlockIconSetting } from "@/lib/editable-settings";
import { DynamicIcon } from "@/components/dynamic-icon";

export default function Block() {
  const { icon } = useVibeCodingBlockIconSetting({
    name: "feature-icon",
    label: "Feature Icon",
    initialValue: { icon: "trending-up" },
  });

  return <DynamicIcon name={icon} className="w-6 h-6" />;
}

useNavigationSetting

Returns { action: "OPEN_URL" | "OPEN_PAGE"; destination: string; openIn: "SELF" | "TAB" } | { action: "OPEN_CHAT" }.
import { NavigationAction } from "@/components/navigation-action";
import { Button } from "@/components/ui/button";
import { useNavigationSetting } from "@/lib/editable-settings";

export default function Block() {
  const navigation = useNavigationSetting({
    name: "cta-navigation",
    label: "CTA Navigation",
    initialValue: {
      action: "OPEN_PAGE",
      destination: "/pricing",
      openIn: "TAB",
    },
  });

  return (
      <Button asChild>
        <NavigationAction navigation={navigation}>
          Click me!
        </NavigationAction>
      </Button>
  );
}

useArraySetting

Returns an array of items with a consistent shape. Use for feature lists, team members, FAQs, testimonials, etc.
import { useArraySetting } from "@/lib/editable-settings";
import { DynamicIcon } from "@/components/dynamic-icon";

export default function Block() {
  const features = useArraySetting({
    name: "features",
    label: "Features",
    schema: {
      title: { type: "text", label: "Title", initialValue: "Feature" },
      description: { type: "text", label: "Description" },
      icon: { type: "vibeCodingBlockIcon", label: "Icon" },
      image: { type: "image", label: "Image" },
    },
    initialValue: [
      {
        title: "Fast",
        description: "Blazing fast performance.",
        icon: { icon: "zap" },
        image: { src: "", alt: "" },
      },
      {
        title: "Reliable",
        description: "99.9% uptime guaranteed.",
        icon: { icon: "shield" },
        image: { src: "", alt: "" },
      },
    ],
  });

  return (
    <div className="container py-12">
      <div className="content grid md:grid-cols-2 gap-6">
        {features.map((feature, index) => (
          <div key={index} className="flex gap-4">
            <DynamicIcon name={feature.icon.icon} className="w-6 h-6 text-primary" />
            <div>
              <h3 className="font-semibold">{feature.title}</h3>
              <p className="text-muted-foreground text-sm">{feature.description}</p>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

useBooleanSetting

Returns a boolean. Use for toggles, switches, show/hide elements, etc.
import { useBooleanSetting } from "@/lib/editable-settings";

export default function Block() {
  const showHeader = useBooleanSetting({
    name: "toggleHeader",     // unique identifier — changing this resets the value
    label: "Toggle header",   // shown in the editor UI
    initialValue: false,      // starting value (default is true)
  });

  return <>{showHeader && <Header />}</>;
}
Schema field types: "text", "image", "video", "vibeCodingBlockIcon" Constraints:
  • Schema cannot contain nested arrays — for list-like text, use a "text" field with a separator (e.g. comma) and split it in code
  • Do not put a vibeCodingBlockIcon field as the first field in the schema
  • Calling two settings hooks with the same name is not allowed

Complete Example — Feature Showcase

A full-featured block combining editable settings, datasource records, and shadcn/ui:
import { useTextSetting, useArraySetting } from "@/lib/editable-settings";
import { useRecords, q } from "@/lib/datasource";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { DynamicIcon } from "@/components/dynamic-icon";

const select = q.select({
  name: "$PRODUCT_NAME_FIELD",
  category: "$CATEGORY_FIELD",
  price: "$PRICE_FIELD",
});

export default function Block() {
  // Editable settings for the section header
  const heading = useTextSetting({
    name: "heading",
    label: "Section heading",
    initialValue: "Our Products",
  });
  const subheading = useTextSetting({
    name: "subheading",
    label: "Section subheading",
    initialValue: "Explore our latest offerings",
  });

  // Live records from datasource
  const { data, status, hasNextPage, fetchNextPage, isFetching } = useRecords({
    select,
    count: 6,
    orderBy: q.asc("name"),
  });

  const items = data?.pages.flatMap(p => p.items) ?? [];

  return (
    <div className="container py-12">
      <div className="content">
        <div className="text-center mb-10">
          <h2 className="text-3xl font-heading font-bold">{heading}</h2>
          <p className="text-muted-foreground mt-2">{subheading}</p>
        </div>

        {status === "pending" && (
          <div className="flex justify-center py-12">
            <div className="text-muted-foreground">Loading...</div>
          </div>
        )}

        {status === "error" && (
          <div className="text-destructive text-center">Failed to load products.</div>
        )}

        <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
          {items.map((item) => (
            <Card key={item.id}>
              <CardHeader>
                <Badge variant="secondary" className="w-fit">
                  {item.fields.category?.label ?? "Uncategorized"}
                </Badge>
                <CardTitle className="mt-2">{item.fields.name}</CardTitle>
              </CardHeader>
              <CardContent>
                <p className="text-2xl font-bold">
                  ${(item.fields.price as number)?.toFixed(2)}
                </p>
              </CardContent>
            </Card>
          ))}
        </div>

        {hasNextPage && (
          <div className="flex justify-center mt-8">
            <button
              onClick={() => fetchNextPage()}
              disabled={isFetching}
              className="text-primary underline underline-offset-4 hover:no-underline"
            >
              {isFetching ? "Loading..." : "Load more"}
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

Fetching field options

Use useFieldOptions to fetch available options for SELECT or multi-select fields. This is useful for building filters, dropdowns, badges, or any UI that needs to display the available choices from the datasource without hardcoding them. Example to build a filter UI using useFieldOptions:
import { useState } from "react";
import { q, useRecords, useFieldOptions } from "@/lib/datasource";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";

const select = q.select({
  title: "$TITLE_FIELD_ID",
  productStatus: "$PRODUCT_STATUS_FIELD_ID",
});

export default function Block() {
  const [selectedProductStatus, setSelectedProductStatus] = useState<string | null>(null);
  const { options } = useFieldOptions({ select, field: "productStatus" });

  const { data, status } = useRecords({
    select,
    where: selectedProductStatus ? q.text("productStatus").is(selectedProductStatus) : undefined,
    count: 10,
  });

  const items = data?.pages.flatMap((p) => p.items) ?? [];

  return (
    <div className="container py-6">
      <div className="content space-y-4">
        <Select
          value={selectedProductStatus ?? "all"}
          onValueChange={(val) => setSelectedProductStatus(val === "all" ? null : val)}
        >
          <SelectTrigger className="w-48">
            <SelectValue placeholder="Filter by product status" />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="all">All Product Statuses</SelectItem>
            {options.map((opt) => (
              <SelectItem key={opt.id} value={opt.id}>
                {opt.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>

        {status === "pending" && <div>Loading...</div>}
        {items.map((item) => (
          <div key={item.id}>
            <div>{item.fields.title}</div>
            <div>{item.fields.productStatus?.label || "Unknown"}</div>
          </div>
        ))}
      </div>
    </div>
  );
}