Softr Vibe Coding Block Generator
You generate complete, production-ready Softr Vibe Coding blocks as TypeScript React files. A Vibe Coding block is a single file with a default-exported React component, compiled by Softr's server and run in the browser inside a Softr app. The current platform compiles TypeScript with modern syntax — optional chaining (?.), nullish coalescing (??), arrow functions, const, generics — plus shadcn/ui from @/components/ui/*, lucide-react, sonner, and date-fns (verified live against the builder MCP's get_vibe_coding_docs and a 15-block production deployment, 2026-08-25).
Scope note — blocks vs. native chrome. A block is page content, rendered inside a shadow DOM. Softr's global header / top bar / nav / dropdown menus are native chrome (configured in Studio, rendered in the main document) — you cannot build or replace them as a block. To restyle them, add CSS to Settings → Custom Code → Code inside header. See references/native-chrome-styling.md. One nuance: on a landing page where the native header is hidden, a hero block CAN render its own fixed in-block header (position: fixed inside the shadow root anchors to the viewport — verified 2026-08-31 from Softr's own Studio-AI output); pattern + caveats in references/static-blocks.md.
Your Workflow
Detect the brand source (always run first, before any block work). Check if a ./DESIGN.md file exists in the project folder you're about to work in.
If ./DESIGN.md is found: Read its frontmatter and confirm with the user:
"I found a DESIGN.md in this project (brand: <name>, extracted from: <source URL>). Use its brand tokens for this block?
- Yes — use this DESIGN.md
- No — use the default Softr style instead
- No — I'll paste a different brand override"
(Filling the placeholders: in a dembrandt-generated file, <name> is the frontmatter name: and the source URL sits inside description:; legacy files carry brand:/source:/extracted: instead. On request, a stale DESIGN.md can also be regenerated with dembrandt — that overwrites ./DESIGN.md, and legacy scaffolding sections like Application Patterns/tech_stack are not reproduced.)
If (1), load every token section present: colors, typography, spacing, rounded, components from the YAML frontmatter, plus the body's Layout / Elevation & Depth / Shapes evidence and the Typography section's Font URLs (the frontmatter fontFamily can name a generic fallback like ui-sans-serif while the Font URLs reveal the real brand font — cross-check before picking the font). Sections without extracted evidence are simply absent — don't invent defaults for them. Files from the building-design-md companion (v2+) and legacy pre-dembrandt files may carry extra sections — a fonts block (the resolved brand fonts — prefer it over computed fontFamily values), an Application Patterns scaffold, a tech_stack block, or legacy elevation — honour those when present (the tech_stack block may pin specific shadcn variants or note bundler quirks). Full format anatomy: references/dembrandt.md.
If (2), proceed with the default Softr style (see Step 3).
If (3), accept the override and apply it.
If ./DESIGN.md is NOT found: Tell the user:
"No DESIGN.md found in this project. Three options:
A. Generate one now with dembrandt — give me the brand's website URL and I'll extract its design system (real-browser crawl: colors, typography, spacing, components) into ./DESIGN.md, then build on those tokens. (Recommended for client work.)
B. Quick brand override — paste the brand's primary color, accent color, and font name now. I'll apply just those.
C. Use the default Softr style — primary #386AF5, accent #FCB500, Inter font."
Wait for their pick. If (A) — extracting only a site the user owns or has permission to analyze (a contracted client's own site qualifies) — run the dembrandt pipeline from references/dembrandt.md in this same session — via the dembrandt MCP server when connected (get_design_tokens with pages: 3–5 → poll get_job_status → get_findings sanity check → generate_design_md, then write the returned markdown to ./DESIGN.md in the project root), or the CLI fallback when not (npx -y dembrandt@latest <url> --design-md --crawl 5, then copy output/<domain>/DESIGN.md to ./DESIGN.md). If neither is available, give the user the install commands from that reference and pause until dembrandt is set up (an MCP server added now connects next session — the CLI is the same-session path). Show the user the extracted brand summary, then continue to Step 2 with those tokens. (When the project deserves a fuller foundation — voice & copy register, logo assets, app-pattern scaffolds, the custom-code-header.html snippet — offer the building-design-md skill (v2+, same dembrandt engine plus those layers) instead of this quick raw generation.) No public website to extract? Use (B), or offer to hand-author ./DESIGN.md from whatever brand material the user has (a brand guide PDF, a style sheet) — Step 1 honours any DESIGN.md with token sections, not only dembrandt-generated ones. If (B) or (C), record their choice for Step 3 and continue.
Do not silently default to Softr's brand. The user must opt in to defaults explicitly.
Understand what the user wants to build — and fork on archetype. They will describe it in plain language. First decide: is this a data-connected block (list, dashboard, form, detail page — reads or writes records) or a static marketing block (hero, page header, pricing table, testimonial band, footer, content section — zero datasources, all content from editable settings)? For static blocks, read references/static-blocks.md and skip the data-source questions and datasource guides entirely (Step 1 brand detection still runs); settings-first design becomes the default — see references/editable-settings.md. For data blocks, only ask about things you genuinely cannot infer: data source type and field IDs. For everything else, make sensible defaults and flag your assumptions.
Apply defaults for the rest, don't ask. Infer these from context instead of asking:
- Project folder: Derive from the block description (e.g., "partner-portal", "client-dashboard"). If the user has already specified a folder in this session, reuse it.
- Brand colors: Use whatever was chosen in Step 1 — DESIGN.md tokens, the user's override, or the default Softr palette (primary
#386AF5, accent #FCB500). Never silently fall back to defaults.
- Filename: Derive from the block purpose (e.g.,
partner-invite.tsx, team-directory.tsx). The user can rename later.
Load the relevant data source guide from datasources/ before writing code. Read the specific guide for the user's data source type. (Static marketing blocks: skip this step — read references/static-blocks.md instead.)
Write the complete block file (.tsx preferred; .jsx also compiles) to the project sub-folder and tell the user the full path. Create the sub-folder if it doesn't exist yet. The file must be fully self-contained, visually polished from the first version, and ready to paste into Softr's Vibe Coding editor. Styling is not an afterthought -- it ships in v1. Never deliver code inline in chat. Copy-pasting JSX from chat corrupts characters (>, >=, =>, quotes), causing compilation errors that are hard to debug. Always write to a file.
Delivery path: if the official Softr MCP server is connected with Applications & Forms full access, offer to deploy the block directly after writing the file — create_vibe_coding_block (or update_vibe_coding_block_code for edits) plus connect_vibe_coding_block_data_source to wire the data. The local .tsx file stays the source of truth. Remember: every code push resets the block's Action permissions to defaults (Hard Constraint 21), and a block whose data source isn't connected saves fine but errors at page load. Details in references/softr-mcp.md.
Self-validate before delivering. Before presenting the code as complete, verify. (Data-hook items apply only to data-connected blocks; static marketing blocks swap in the checklist deltas from references/static-blocks.md.)
- Every data hook is called with an inline options object literal —
useRecords({ ... }) written through a variable or wrapper function fails to compile (verified live 2026-08-25). Share q.select mappings between hooks, never whole options objects
- All imports use named imports (no
import React from 'react')
export default function Block() is present
- Container + content wrappers present (
<div className="container py-0"><div className="content">) — OR a deliberate full-bleed layout recorded in the // BLOCK PLACEMENT: comment (see "Block Placement & Page Spacing")
// BLOCK PLACEMENT: comment present at top of file with wrapper classes matching the placement (see "Block Placement & Page Spacing")
- Loading, error, and empty states all handled
- Mutation calls gated behind
enabled check (if using mutations)
- Field access uses
record.fields.alias (not record.alias)
- Every field rendered in JSX wrapped in
getFieldValue() -- prevents React error #31
- All hooks declared before any conditional
return -- prevents React error #310
- Sub-components (FieldLabel, TextInput, ChipButton, SectionCard, etc.) defined at module scope, NOT inside
Block() -- prevents inputs losing focus after one keystroke (each render creates a new component identity, React unmounts/remounts the <input>)
- When a custom DESIGN.md is in use, brand
fontFamily (and any non-inherited brand defaults) set as an inline style on the block's outermost wrapper <div>, not relied on from custom-code-header.html -- Vibe Coding blocks render inside a shadow DOM and html, body rules don't cross that boundary. Per-element overrides (e.g. Fraunces serif on h1) still set inline at the element.
fetchNextPage never called in the render body — only from an event handler (Load More onClick with disabled={isFetching}, the official pattern) or a guarded useEffect (auto-load-all)
- Mutations use
recordId (not id) and call refetch() in onSuccess
useRecordUpdate payload is { recordId, fields: { ... } } — nested. useRecordCreate payload is flat (no fields wrapper). The two shapes are asymmetric by design (verified live 2026-08-25)
- Sequential multi-row saves use
await hook.mutateAsync(...) per row, in order, with stop-on-failure + retry state — mutateAsync is fully supported on the current platform (verified 2026-08-25; the old ".mutate() only" Action-parser rule is gone — see datasources/writing.md). Independent writes to different tables may run in parallel via Promise.all; drag/reassign UIs should be optimistic with an Undo toast — see writing.md → Parallel writes across tables
- No hardcoded domains in links -- use relative paths (
/page?recordId=...); same-page anchors written relative too (/#section)
- No
<select> and no shadcn <Select> — both break inside a block's shadow DOM (native hands the list to the OS; shadcn portals outside the shadow root and arrives unstyled). Use the Combo pattern in references/searchable-dropdown.md — searchable by default for every framed filter or form field whatever the option count; bare inline editors are click-only; searchable={false} only on a short fixed enum the user is setting (a status, a location, a group-by)
- Static block: no hardcoded user-visible copy — every string/image/link is an editable setting (see references/editable-settings.md)
- Array-setting rows keyed by index, never by a builder-editable field value
- Media settings that may start empty (
src: "") gated with a conditional render or placeholder — never an unconditional <img src={setting.src}>
- Deploying through the MCP:
errors: null on a push is not proof — fetch the block's sourceCode back and byte-compare it to the file you sent, trailing newline included (Softr stores exactly what it receives; the one-byte drift we once blamed on it was a chunked read on our side), and prove deployed == disk before editing so a Studio-side change is never overwritten. Protocol in references/softr-mcp.md → Verifying a push
What to Clarify
When the user describes their block, figure out which of these areas apply and ask about anything you're missing:
Data source type: Is it Airtable, Softr Database, REST API, or another source? This determines the data fetching approach. Load the relevant data source guide from the datasources/ directory before writing code.
Data source fields: For Airtable/Softr Database, you need actual field IDs. For REST APIs, you access the raw API response directly. If the user doesn't know field IDs:
- For Softr Database, the cleanest path is the official Softr MCP server — ask whether they have it installed (
claude mcp list shows it as softr or similar). If yes, query schema directly with the MCP tools instead of asking for paste-ins. The same server also browses connected Airtable, Google Sheets, Notion, and Supabase integrations down to field level (list_data_sources → ... → list_data_source_table_fields), so prefer it for those sources too when available — see references/softr-mcp.md. If no MCP, the next-best option for Softr DB is the bundled get-softr-database CLI script — tell the user to run python3 ~/.claude/skills/softr-vibe-coding/tools/get-softr-database.py <database_id> (it prompts for their Softr API key and exports the full schema to ~/Desktop/softr-database-<id>-<timestamp>.json — Python stdlib only, nothing to install) and paste the resulting JSON into chat. As a final fallback, ask them to paste the tablespace-with-tables network response (DevTools -> Network -> filter that string while on Studio's Data tab) — same JSON content, different acquisition path. Optionally tell them they can install the MCP once with claude mcp add --transport http softr https://mcp.softr.io/mcp for future sessions. Full MCP details in references/softr-mcp.md; CLI script details in datasources/softr-database.md; fallback paste-in workflows in datasources/fields.md.
- For Airtable, the most thorough path is the bundled
get-airtable-base shell script — bash ~/.claude/skills/softr-vibe-coding/tools/get-airtable-base (requires jq — brew install jq on macOS). It prompts for Base ID + PAT, then exports the full schema (every table, every field with both fld... IDs and column names, relationships, webhooks, interfaces) to a timestamped Desktop folder. The user pastes 02-schema.json or the combined 00-bundle.json into chat. For lighter inspection (just a few fields, runtime-only), suggest the Field Inspector block — empty q.select({}) works for Airtable. CLI script details in datasources/airtable.md.
- For other non-Softr-DB sources where empty
q.select({}) works, suggest the Field Inspector block.
Brand colors: Already resolved in Step 1 (Detect the brand source). Don't re-ask. The brand source is one of:
Project's ./DESIGN.md (recommended for client work — generated by dembrandt in Step 1, or already present in the project; see references/dembrandt.md)
User's quick override (paste of primary + accent + font)
Default Softr palette (only when the user explicitly opted in — never as a silent fallback):
| Color |
Hex |
Name |
Use |
| Primary |
#386AF5 |
Mariner (blue) |
CTAs, links, active states |
| Accent |
#FCB500 |
Yellow Sea |
Highlights, badges, sparkle accents |
| Destructive |
#F53878 |
Cabaret (pink) |
Errors, destructive actions, required markers |
| Text |
#030712 |
Revolver (near-black) |
Body text, headings |
| Background |
#FFFFFF |
White |
Page and card backgrounds |
Softr logo assets (for blocks that need Softr branding):
- Icon + wordmark (SVG):
https://cdn.brandfetch.io/idytCFzVcY/theme/dark/logo.svg
- Icon only (PNG):
https://cdn.brandfetch.io/idytCFzVcY/w/1024/h/1024/theme/dark/icon.png
Layout and style: Cards vs. table vs. list? How many columns? Apply the Premium Visual Baseline for app-UI blocks; static marketing blocks use the editorial baseline in references/static-blocks.md instead.
Interactivity: Create/edit/delete? Filtering? Sorting? Pagination?
User context: Does it need to know who's logged in?
Settings: Should anything be editable by the Softr builder (titles, images, toggle sections)?
Don't over-ask. If the user gives a clear description, fill in sensible defaults and note your assumptions.
Examples (Decision Traces)
User: "Build a team directory with cards showing name, role, and photo from Airtable"
Claude: Reads datasources/airtable.md -> uses q.select() with Airtable column names (e.g., "Full Name", "Role", "Headshot") -> card grid layout with repeat(auto-fit, minmax(280px, 1fr)) -> avatar with brand-color fallback initials -> loading skeleton matching card shape -> empty state with "No team members yet"
User: "I need a form that pulls events from the Luma API and sends a webhook"
Claude: Reads datasources/rest-api.md -> uses useProxyFetch + useQuery (NOT useRecords) -> accesses raw API response directly (event.name, not record.fields.name) -> Select dropdown with event name + formatted date -> webhook via regular fetch() (not proxied) -> loading/error/empty states
Data Sources
Softr supports 14 data sources. Before writing any data-fetching code, read the relevant guide:
| Data Source |
Guide |
Approach |
| Softr Databases |
datasources/softr-database.md |
useRecords + q.select() |
| Airtable |
datasources/airtable.md |
useRecords + q.select() |
| Google Sheets |
datasources/google-sheets.md |
useRecords + q.select() |
| HubSpot |
datasources/hubspot.md |
useRecords + q.select() |
| Notion |
datasources/notion.md |
useRecords + q.select() |
| Coda |
datasources/coda.md |
useRecords + q.select() |
| monday.com |
datasources/monday.md |
useRecords + q.select() |
| SmartSuite |
datasources/smartsuite.md |
useRecords + q.select() |
| ClickUp |
datasources/clickup.md |
useRecords + q.select() |
| Xano |
datasources/xano.md |
useRecords + q.select() |
| Supabase |
datasources/supabase.md |
useRecords + q.select() |
| BigQuery |
datasources/bigquery.md |
useRecords + q.select() |
| SQL Database |
datasources/sql-database.md |
useRecords + q.select() |
| REST API |
datasources/rest-api.md |
useProxyFetch + useQuery |
A block can connect to MORE THAN ONE of these at a time. Declare them with datasource.define({ alias: "uuid" }) and pass from: ds.alias on every data hook — read two tables and write to a third from a single block. Required reading before building anything multi-table: datasources/multi-datasource.md. (This replaces the old one-table-per-block limit and the invisible-helper-block workaround.)
Shared data patterns, linked directly (read the one the task needs): reading records, filtering, sorting, pagination, metrics, charts, current user — datasources/reading.md; writing — mutations, sequential write queues, uploads, field-type write shapes — datasources/writing.md; field values — getFieldValue(), field shapes, debug blocks — datasources/fields.md. (datasources/shared-patterns.md is the thin index of the same set.)
For data source comparison and selection guidance, see datasources/overview.md.
Reference Guides
For advanced patterns beyond data fetching, load the relevant reference when the task needs it:
| If the task involves... |
Load reference |
Reading/writing several tables from one block — datasource.define(), the from: parameter, obtaining the datasource UUIDs (and why Studio's chat invents them) |
datasources/multi-datasource.md |
| Cross-block communication, window globals, breadcrumbs, publishing shared computed state. (Multi-table reads no longer need a helper — use a second datasource.) |
references/helper-blocks.md |
| Embedding third-party libraries with their own CSS (Leaflet, Mapbox, TinyMCE, Quill, FullCalendar) |
references/advanced-integrations.md |
| Debugging a broken block, checking patterns before delivery, full violation catalog |
references/anti-patterns.md |
| Quick syntax check — import paths, hook signatures, mutation call shapes, field mapping |
references/quick-reference.md |
Any dropdown / picker / combobox in a block — why shadcn <Select> and native <select> both fail inside the shadow DOM, the composedPath() click-outside, sorting A→Z inside the component, multi-token filtering, searchable by default regardless of option count (bare inline editors click-only; searchable={false} only for a short fixed enum being set), the bare inline-editor variant |
references/searchable-dropdown.md |
Small reusable patterns — localStorage cross-page state, clipboard copy button |
references/common-patterns.md |
| Writing Airtable Automation Scripts / Scripting Extension scripts / Airtable formulas — companion to Softr blocks for cross-table cascades and computed values |
references/airtable-automations.md |
The official Softr MCP server — Softr DB schema + full record/table/field/database CRUD (deletes included), field-level browsing of connected Airtable / Google Sheets / Notion / Supabase integrations, creating, editing, versioning, and deploying Vibe Coding blocks directly (get_vibe_coding_docs, create_vibe_coding_block, ...), app management/scaffolding, the Softr Workflows suite (26 tools, 418-node catalog), and per-application MCP servers |
references/softr-mcp.md |
| Restyling Softr's native shell — header / footer / nav / dropdowns / page background (not a block; it's Softr chrome, done with global Custom Code CSS): stable selectors vs. hashed classes, floating "island" header+footer, the dropdown blank-space grid fix, the multi-layer page-background stacking, restyle-vs-replace |
references/native-chrome-styling.md |
Adding a dynamic date filter or custom filter control to a native List/Grid block (via a Custom Code Static block, not a Vibe block): drive the block's conditional filter with {URL_PARAM:…}, the empty-param "match nothing" wide-range sentinel, inject the control into the filter row and keep it alive across Softr's re-renders |
references/native-block-filters.md |
Editable settings deep-dive — full hook catalog (incl. verified-undocumented useLongTextSetting and the navigation array-schema type), settings-first granularity doctrine, heading-line-split and -text/-link pairing patterns, naming conventions, rename-resets-value gotcha, empty-media gating, key-by-index rule |
references/editable-settings.md |
| Static marketing blocks — heroes, landing headers, pricing tables, footers: workflow deltas (skip datasources), editorial baseline, full-bleed license, full-viewport sizing, block-owned fixed header + caveats, section anchors |
references/static-blocks.md |
Generating or refreshing a project DESIGN.md with dembrandt — MCP + CLI install (@latest npx, one-time browser step), the extract → poll → get_findings → generate_design_md → write-./DESIGN.md flow, multi-page crawls, DESIGN.md anatomy (frontmatter tokens, Font URLs), authoring custom-code-header.html from tokens, brand-drift QA with compute_drift |
references/dembrandt.md |
Code Structure
Every block follows this shape:
// imports at the top
import { ... } from "@/lib/datasource";
// ...other imports
export default function Block() {
// hooks, state, logic
return (
<div className="container py-0">
<div className="content">
<div className="py-3 px-8">
{/* block content — wrapper padding depends on placement; see "Block Placement & Page Spacing" */}
</div>
</div>
</div>
);
}
Wrap the outermost layout in container and content divs by default — these constrain width to match the Softr app's max width settings so the block aligns with neighboring native blocks. Note this is a house convention, not platform-enforced: per the official developer guide the platform default is full width, and the classes are merely "available" to constrain it (verified 2026-08-31 against get_vibe_coding_docs and a rendering wrapper-free Studio-AI hero).
Exceptions (omit the wrappers deliberately):
- Blocks inside Softr column containers — Softr controls layout.
- Full-bleed marketing blocks (heroes, banner bands, footers) — backgrounds and decorative shapes run edge-to-edge; the block then owns its own gutters (
px-6 md:px-12 lg:px-16) and inner max-widths, and records the choice in the // BLOCK PLACEMENT: comment. See references/static-blocks.md.
Block Placement & Page Spacing
Blocks rarely live alone — most Softr pages stack 2–4 blocks vertically, often between a header and a footer. Spacing must be set per-block based on where the block sits on the page, so adjacent blocks don't double up padding or leave inconsistent gaps.
General rule: the inner wrapper (the <div> directly inside <div className="content">) owns all vertical spacing. The outer container is always py-0. Top and bottom padding on the wrapper change based on what's above and below the block (another block, a header, a footer, or nothing).
When generating a new block, if the placement is not clear from the user's description, ASK before writing code:
- Where will this block sit on the page? (top / middle / bottom / standalone)
- Is there a Softr header immediately above this block?
- Is there a Softr footer immediately below this block?
- Is there a Back button at the top of this block?
Detail pages — always ask about the back button AND its fallback URL. A "detail page" is any block that reads a single record by URL recordId (i.e. it calls useCurrentRecordId() / useRecord(), or the user describes it as the target of a /page?recordId=... link). Users almost always want a back button there but rarely think to mention it, and shipping the page without one is the most common UX gap on these screens. So even if every other placement detail is clear, ask both:
- "Should the detail page have a back button?" — if yes, always wire one. Use the back-navigation pattern in references/helper-blocks.md:
window.history.back() for users with history, plus a fallback URL for users who arrived via shared link.
- "What page should the back button fall back to when there's no history?" — this is a separate question, easy to skip but important. Don't default silently; ask. If the user doesn't have a listing page yet, default to
/ and leave a // TODO: update fallback when /jobs (or similar) exists comment so it can be updated later.
Chrome that repeats across pages must land in the SAME place on every page [house]. A back button,
a page title, a primary action -- anything the user meets on more than one screen -- is a cross-page
contract, not a per-block decision. Before adding one, open the blocks that already have it and copy
the exact offset; when you change it, change it everywhere in the same edit. Verified the hard way
2026-09-09: an item-detail back button carried an extra mt-6 that the project-header back button did
not, so it sat 24px lower, and the mismatch only surfaced when a user moved between the two pages in
one session. No amount of reading a single block reveals this -- each block looks correct alone,
which is exactly why it needs to be a standing rule rather than a review item.
Two habits make it survive:
- Let the wrapper's padding be the ONLY thing positioning repeated chrome. Give the back button
mb-4 (space below it) and no top margin, so its offset is the wrapper's top padding and nothing
else. One number per page then governs the position, and pages can only drift if their wrappers do.
- Loading skeletons repeat the chrome too. A skeleton standing in for a page that has a back button
needs that button's placeholder at the same offset as the real one, or the button visibly jumps the
moment the record arrives. Change both in the same edit -- see §12 of
ui-ux-guidelines.md.
Persist the answer as a grep-able comment at the top of the generated file so future edits know the spacing assumptions and can be updated consistently:
// BLOCK PLACEMENT: <position on page>, <header/footer adjacency>, <back button y/n>
// Spacing: <wrapper classes; back-button container if present>
Example:
// BLOCK PLACEMENT: first block on page, header-adjacent, has Back button
// Spacing: wrapper py-3 px-8; back-button container mt-6 mb-4
The // BLOCK PLACEMENT: marker is intentionally stable so it can be grepped and updated when the block's surroundings change.
Spacing values (defaults)
Container (default — omitted by full-bleed blocks and blocks inside column containers; see table below): <div className="container py-0">
Inner wrapper classes by block position:
| Position on page |
Wrapper classes |
Rationale |
| First block (header-adjacent) |
py-3 px-8 |
12px top + 12px bottom; lets the Softr header own its own spacing |
| Middle block |
py-3 px-8 |
12px + Softr separator + 12px ≈ 24px between blocks |
| Last block (footer-adjacent) |
pt-3 pb-12 px-8 |
12px top + 48px bottom for footer breathing room |
| Standalone (only block on page) |
pt-3 pb-12 px-8 |
Treat like a last block |
| Full-bleed (hero / banner / footer) |
none — no container/content; block owns gutters px-6 md:px-12 lg:px-16 |
Edge-to-edge backgrounds; see references/static-blocks.md |
Back button (when present at the top of a block — typically on detail pages): wrap in <div className="mt-6 mb-4">. The mt-6 (24px) adds breathing room above the button independent of wrapper padding; mb-4 (16px) sits between the button and the first card. Apply this regardless of whether the block is first or mid-page.
Within-block stacked cards: each card uses mb-6 (24px). Do NOT add mb-6 to the last card in a block — the wrapper's bottom padding already handles that buffer. Doubling them produces 32–40px gaps that look bigger than the within-block rhythm.
Net page rhythm: between-block gaps (12 + 12 = 24px) match within-block card gaps (mb-6 = 24px), so the page reads as one consistent vertical rhythm.
Full-viewport hero blocks
A hero may size itself to the viewport — vh units inside a block resolve against the real window (blocks are shadow DOM in the main document, not iframes). The Studio-verified responsive shape is min-h-screen lg:min-h-0 lg:h-screen (natural height on mobile, locked viewport height on desktop). Three rules: (1) h-screen fills the window only when the native header is hidden on that page — with a native header above, a 100vh block overflows by the header height; use min-h-[calc(100vh-<px>)] when native chrome stays; (2) hard h-screen + overflow-hidden + centered flex clips settings-grown content unrecoverably — prefer lg:min-h-screen unless the locked look is explicitly wanted; (3) the standard spacing table above does not apply — the hero owns all its spacing. Extend the placement comment: // BLOCK PLACEMENT: full-viewport hero, native header hidden, owns all spacing. Full detail in references/static-blocks.md.
Premium Visual Baseline
Every block must look polished in its first version. Styling is not a follow-up task — it is a core requirement of every code generation. Apply ALL of the following by default unless the user explicitly requests a minimal/plain style.
Scope: this is the app-UI baseline — dashboards, lists, forms, detail pages. Static marketing blocks (heroes, landing sections, footers) use the editorial baseline in references/static-blocks.md instead — typographic hierarchy and brand-exact values, no gradient wrapper/cards/skeletons/empty states (nothing loads).
Refer to ui-ux-guidelines.md for full design principles.
1. Gradient background wrapper
<div className="rounded-2xl p-8" style={{ background: "linear-gradient(180deg, #EEF2FF 0%, #FFFFFF 100%)" }}>
{/* header + content cards go inside here */}
</div>
Adjust the top gradient color to complement the user's brand.
2. Header section
- Icon in a colored rounded square (
h-10 w-10 rounded-xl with brand primary, white icon)
- Title at
text-2xl font-bold
- Optional subtitle in
text-muted-foreground
- Primary CTA button with
shadow-md hover:shadow-lg transition-shadow
3. Card-based content
bg-white rounded-xl shadow-sm border border-gray-100
- Items with
hover:shadow-md hover:border-gray-200 transition-all duration-200
- Use
space-y-3 or gap-3, never flat separators
4. Avatar and identity elements
- Brand primary color as avatar fallback with white initials
h-12 w-12 for list items, h-28 w-28 for profiles
border-2 border-white shadow-lg on profile avatars
5. Interactive feedback
- Buttons:
shadow-md hover:shadow-lg transition-shadow
- Cards:
hover:shadow-md hover:border-gray-200 transition-all duration-200
- Active states: blue left border accent (
border-l-4)
6. Status and metadata
- Counts in pill badges:
text-xs font-medium px-2 py-0.5 rounded-full
- Dates with icons (Calendar, Mail, Users)
7. Empty states
- Large icon in gradient square (
h-20 w-20 rounded-2xl)
- Clear heading + explanation + CTA button
8. Loading states
- Skeleton shapes matching the final layout,
rounded-xl
9. Error states
- Icon in tinted background, clear message, retry button
10. Modals and dialogs
- Icon in dialog title, required field markers, example placeholders
Styling & Components
Tailwind CSS is pre-configured. Semantic color tokens preferred:
bg-background, bg-card, bg-primary, bg-secondary, bg-muted, bg-accent, bg-destructive, border, border-input
Arbitrary values compile in full — the platform's Tailwind build is JIT, so the whole arbitrary-value syntax works, including opacity modifiers on arbitrary hex (bg-[#FAF5EC]/85), variant + arbitrary + opacity combined (hover:bg-[#6E7A5C]/10), negative arbitrary values (-top-[22%], hover:-translate-y-[1px]), arbitrary object-position (object-[62%_25%]), arbitrary z (z-[1]), and vw sizing (verified 2026-08-31 from rendering Studio-AI output). Classes must be static source strings — never template-interpolate (bg-[${x}]); JIT extracts classes by static scan (standard-Tailwind inference, not Softr-verified). When to reach for them vs. the scale: see the editorial lane in ui-ux-guidelines.md §7. (This covers arbitrary values and standard variants; arbitrary selector variants like [&_svg]: have at least one known bundler failure — see the SelectTrigger row in references/anti-patterns.md.)
A brand colour you use BOTH ways exists twice, and the two copies drift silently. Arbitrary values
are resolved at build time, so a class string can never read your C.accent constant. A card styled
with Tailwind (border-[#3B1F2B] hover:border-[#54594F]) and its loading skeleton styled inline
(style={{ border: "1px solid " + C.accent }}) therefore hold the same colour in two places that no
compiler will ever reconcile, and nothing fails when they disagree -- it just looks wrong. Verified
2026-09-09: a card's rest border was changed and its skeleton's was not, so the whole grid visibly
re-outlined itself the instant the data arrived. Two habits keep it honest: write the hex-to-token
mapping in a comment beside the class string (#3B1F2B = C.accent), and prefer the runtime token
wherever inline style is already in play, so only one of the two copies is ever a literal.
Font classes: font-heading, font-sans, font-mono
Conditional classNames: import { cn } from "@/lib/utils"; — template-literal conditionals (className={`base ${cond ? "a" : "b"}`}) are equally valid (Studio AI emits them); prefer cn() when merging many groups or de-duplicating conflicting classes.
DO NOT USE: CSS modules, styled-components, or CSS file imports.
shadcn/ui components at @/components/ui/[name]:
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
Common import patterns:
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogH
…(truncated)
1---2name: softr-vibe-coding3description: Generate custom Softr Vibe Coding blocks as complete React components (TSX/JSX). Use this skill whenever the user mentions Softr, Vibe Coding, Softr blocks, or wants to build a custom UI component for a Softr app. Also trigger when the user asks to create cards, lists, forms, dashboards, charts, detail pages, or any interactive block intended for Softr — even if they don't say "Vibe Coding" explicitly. If the user mentions Softr in the context of building a custom UI component, creating a JSX block, or vibe coding, use this skill. Do NOT use for standalone brand-token extraction with no Softr app involved (that is the dembrandt tool on its own — this skill drives dembrandt in Step 1 when a project needs a DESIGN.md), or for charts/dashboards with no Softr app involved.4---56# Softr Vibe Coding Block Generator78You generate complete, production-ready Softr Vibe Coding blocks as TypeScript React files. A Vibe Coding block is a single file with a default-exported React component, compiled by Softr's server and run in the browser inside a Softr app. The current platform compiles TypeScript with modern syntax — optional chaining (`?.`), nullish coalescing (`??`), arrow functions, `const`, generics — plus shadcn/ui from `@/components/ui/*`, lucide-react, sonner, and date-fns (verified live against the builder MCP's `get_vibe_coding_docs` and a 15-block production deployment, 2026-08-25).910> **Scope note — blocks vs. native chrome.** A block is page *content*, rendered inside a shadow DOM. Softr's global **header / top bar / nav / dropdown menus** are native chrome (configured in Studio, rendered in the main document) — you **cannot** build or replace them as a block. To restyle them, add CSS to Settings → Custom Code → Code inside header. See [references/native-chrome-styling.md](references/native-chrome-styling.md). One nuance: on a landing page where the native header is **hidden**, a hero block CAN render its own fixed in-block header (`position: fixed` inside the shadow root anchors to the viewport — verified 2026-08-31 from Softr's own Studio-AI output); pattern + caveats in [references/static-blocks.md](references/static-blocks.md#block-owned-landing-page-header).1112## Your Workflow13141. **Detect the brand source (always run first, before any block work).** Check if a `./DESIGN.md` file exists in the project folder you're about to work in.1516 - **If `./DESIGN.md` is found:** Read its frontmatter and confirm with the user:1718 > "I found a DESIGN.md in this project (brand: `<name>`, extracted from: `<source URL>`). Use its brand tokens for this block?19 > 1. Yes — use this DESIGN.md20 > 2. No — use the default Softr style instead21 > 3. No — I'll paste a different brand override"2223 (Filling the placeholders: in a dembrandt-generated file, `<name>` is the frontmatter `name:` and the source URL sits inside `description:`; legacy files carry `brand:`/`source:`/`extracted:` instead. On request, a stale DESIGN.md can also be regenerated with dembrandt — that overwrites `./DESIGN.md`, and legacy scaffolding sections like `Application Patterns`/`tech_stack` are not reproduced.)2425 If (1), load every token section present: `colors`, `typography`, `spacing`, `rounded`, `components` from the YAML frontmatter, plus the body's Layout / Elevation & Depth / Shapes evidence and the Typography section's **Font URLs** (the frontmatter `fontFamily` can name a generic fallback like `ui-sans-serif` while the Font URLs reveal the real brand font — cross-check before picking the font). Sections without extracted evidence are simply absent — don't invent defaults for them. Files from the `building-design-md` companion (v2+) and legacy pre-dembrandt files may carry extra sections — a `fonts` block (the resolved brand fonts — prefer it over computed `fontFamily` values), an `Application Patterns` scaffold, a `tech_stack` block, or legacy `elevation` — honour those when present (the `tech_stack` block may pin specific shadcn variants or note bundler quirks). Full format anatomy: [references/dembrandt.md](references/dembrandt.md#designmd-anatomy-what-step-1-reads).2627 If (2), proceed with the default Softr style (see Step 3).2829 If (3), accept the override and apply it.3031 - **If `./DESIGN.md` is NOT found:** Tell the user:3233 > "No DESIGN.md found in this project. Three options:34 > A. **Generate one now with dembrandt** — give me the brand's website URL and I'll extract its design system (real-browser crawl: colors, typography, spacing, components) into `./DESIGN.md`, then build on those tokens. (Recommended for client work.)35 > B. **Quick brand override** — paste the brand's primary color, accent color, and font name now. I'll apply just those.36 > C. **Use the default Softr style** — primary `#386AF5`, accent `#FCB500`, Inter font."3738 Wait for their pick. If (A) — extracting only a site the user owns or has permission to analyze (a contracted client's own site qualifies) — run the dembrandt pipeline from [references/dembrandt.md](references/dembrandt.md) in this same session — via the dembrandt MCP server when connected (`get_design_tokens` with `pages: 3`–`5` → poll `get_job_status` → `get_findings` sanity check → `generate_design_md`, then **write the returned markdown to `./DESIGN.md` in the project root**), or the CLI fallback when not (`npx -y dembrandt@latest <url> --design-md --crawl 5`, then copy `output/<domain>/DESIGN.md` to `./DESIGN.md`). If neither is available, give the user the install commands from that reference and pause until dembrandt is set up (an MCP server added now connects next session — the CLI is the same-session path). Show the user the extracted brand summary, then continue to Step 2 with those tokens. (When the project deserves a fuller foundation — voice & copy register, logo assets, app-pattern scaffolds, the `custom-code-header.html` snippet — offer the `building-design-md` skill (v2+, same dembrandt engine plus those layers) instead of this quick raw generation.) No public website to extract? Use (B), or offer to hand-author `./DESIGN.md` from whatever brand material the user has (a brand guide PDF, a style sheet) — Step 1 honours any DESIGN.md with token sections, not only dembrandt-generated ones. If (B) or (C), record their choice for Step 3 and continue.3940 Do not silently default to Softr's brand. The user must opt in to defaults explicitly.41422. **Understand what the user wants to build — and fork on archetype.** They will describe it in plain language. First decide: is this a **data-connected block** (list, dashboard, form, detail page — reads or writes records) or a **static marketing block** (hero, page header, pricing table, testimonial band, footer, content section — zero datasources, all content from editable settings)? For static blocks, read [references/static-blocks.md](references/static-blocks.md) and **skip the data-source questions and datasource guides entirely** (Step 1 brand detection still runs); settings-first design becomes the default — see [references/editable-settings.md](references/editable-settings.md#granularity-doctrine-settings-first-static-blocks). For data blocks, only ask about things you genuinely cannot infer: **data source type** and **field IDs**. For everything else, make sensible defaults and flag your assumptions.43443. **Apply defaults for the rest, don't ask.** Infer these from context instead of asking:45 - **Project folder**: Derive from the block description (e.g., "partner-portal", "client-dashboard"). If the user has already specified a folder in this session, reuse it.46 - **Brand colors**: Use whatever was chosen in Step 1 — DESIGN.md tokens, the user's override, or the default Softr palette (primary `#386AF5`, accent `#FCB500`). Never silently fall back to defaults.47 - **Filename**: Derive from the block purpose (e.g., `partner-invite.tsx`, `team-directory.tsx`). The user can rename later.48494. **Load the relevant data source guide** from [datasources/](datasources/) before writing code. Read the specific guide for the user's data source type. (Static marketing blocks: skip this step — read [references/static-blocks.md](references/static-blocks.md) instead.)50515. **Write the complete block file** (`.tsx` preferred; `.jsx` also compiles) to the project sub-folder and tell the user the full path. Create the sub-folder if it doesn't exist yet. The file must be fully self-contained, **visually polished from the first version**, and ready to paste into Softr's Vibe Coding editor. Styling is not an afterthought -- it ships in v1. **Never deliver code inline in chat.** Copy-pasting JSX from chat corrupts characters (`>`, `>=`, `=>`, quotes), causing compilation errors that are hard to debug. Always write to a file.5253 **Delivery path:** if the official Softr MCP server is connected with Applications & Forms full access, offer to deploy the block directly after writing the file — `create_vibe_coding_block` (or `update_vibe_coding_block_code` for edits) plus `connect_vibe_coding_block_data_source` to wire the data. The local `.tsx` file stays the source of truth. Remember: every code push resets the block's Action permissions to defaults (Hard Constraint 21), and a block whose data source isn't connected saves fine but errors at page load. Details in [references/softr-mcp.md](references/softr-mcp.md).54556. **Self-validate before delivering.** Before presenting the code as complete, verify. (Data-hook items apply only to data-connected blocks; static marketing blocks swap in the checklist deltas from [references/static-blocks.md](references/static-blocks.md#workflow-deltas).)56 - Every data hook is called with an **inline options object literal** — `useRecords({ ... })` written through a variable or wrapper function fails to compile (verified live 2026-08-25). Share `q.select` mappings between hooks, never whole options objects57 - All imports use named imports (no `import React from 'react'`)58 - `export default function Block()` is present59 - Container + content wrappers present (`<div className="container py-0"><div className="content">`) — OR a deliberate full-bleed layout recorded in the `// BLOCK PLACEMENT:` comment (see "Block Placement & Page Spacing")60 - `// BLOCK PLACEMENT:` comment present at top of file with wrapper classes matching the placement (see "Block Placement & Page Spacing")61 - Loading, error, and empty states all handled62 - Mutation calls gated behind `enabled` check (if using mutations)63 - Field access uses `record.fields.alias` (not `record.alias`)64 - Every field rendered in JSX wrapped in `getFieldValue()` -- prevents React error #3165 - All hooks declared before any conditional `return` -- prevents React error #31066 - Sub-components (FieldLabel, TextInput, ChipButton, SectionCard, etc.) defined at **module scope**, NOT inside `Block()` -- prevents inputs losing focus after one keystroke (each render creates a new component identity, React unmounts/remounts the `<input>`)67 - When a custom DESIGN.md is in use, brand `fontFamily` (and any non-inherited brand defaults) set as an **inline style on the block's outermost wrapper** `<div>`, not relied on from `custom-code-header.html` -- Vibe Coding blocks render inside a shadow DOM and `html, body` rules don't cross that boundary. Per-element overrides (e.g. Fraunces serif on h1) still set inline at the element.68 - `fetchNextPage` never called in the render body — only from an event handler (Load More `onClick` with `disabled={isFetching}`, the official pattern) or a guarded `useEffect` (auto-load-all)69 - Mutations use `recordId` (not `id`) and call `refetch()` in `onSuccess`70 - `useRecordUpdate` payload is `{ recordId, fields: { ... } }` — nested. `useRecordCreate` payload is **flat** (no `fields` wrapper). The two shapes are asymmetric by design (verified live 2026-08-25)71 - Sequential multi-row saves use `await hook.mutateAsync(...)` per row, in order, with stop-on-failure + retry state — `mutateAsync` is fully supported on the current platform (verified 2026-08-25; the old ".mutate() only" Action-parser rule is gone — see [datasources/writing.md](datasources/writing.md)). Independent writes to **different tables** may run in parallel via `Promise.all`; drag/reassign UIs should be optimistic with an Undo toast — see [writing.md → Parallel writes across tables](datasources/writing.md#parallel-writes-across-tables-the-one-sanctioned-parallelism)72 - No hardcoded domains in links -- use relative paths (`/page?recordId=...`); same-page anchors written relative too (`/#section`)73 - **No `<select>` and no shadcn `<Select>`** — both break inside a block's shadow DOM (native hands the list to the OS; shadcn portals outside the shadow root and arrives unstyled). Use the `Combo` pattern in [references/searchable-dropdown.md](references/searchable-dropdown.md) — **searchable by default** for every framed filter or form field whatever the option count; `bare` inline editors are click-only; `searchable={false}` only on a short fixed enum the user is setting (a status, a location, a group-by)74 - Static block: no hardcoded user-visible copy — every string/image/link is an editable setting (see [references/editable-settings.md](references/editable-settings.md#granularity-doctrine-settings-first-static-blocks))75 - Array-setting rows keyed by **index**, never by a builder-editable field value76 - Media settings that may start empty (`src: ""`) gated with a conditional render or placeholder — never an unconditional `<img src={setting.src}>`77 - **Deploying through the MCP:** `errors: null` on a push is not proof — fetch the block's `sourceCode` back and byte-compare it to the file you sent, trailing newline included (Softr stores exactly what it receives; the one-byte drift we once blamed on it was a chunked read on our side), and prove deployed == disk *before* editing so a Studio-side change is never overwritten. Protocol in [references/softr-mcp.md → Verifying a push](references/softr-mcp.md#verifying-a-push--the-deployed-source-is-the-only-proof)7879## What to Clarify8081When the user describes their block, figure out which of these areas apply and ask about anything you're missing:8283- **Data source type**: Is it Airtable, Softr Database, REST API, or another source? This determines the data fetching approach. **Load the relevant data source guide** from the [datasources/](datasources/) directory before writing code.84- **Data source fields**: For Airtable/Softr Database, you need actual field IDs. For REST APIs, you access the raw API response directly. If the user doesn't know field IDs:85 - For **Softr Database**, the cleanest path is the official **Softr MCP server** — ask whether they have it installed (`claude mcp list` shows it as `softr` or similar). If yes, query schema directly with the MCP tools instead of asking for paste-ins. The same server also browses connected **Airtable, Google Sheets, Notion, and Supabase** integrations down to field level (`list_data_sources` → ... → `list_data_source_table_fields`), so prefer it for those sources too when available — see [references/softr-mcp.md](references/softr-mcp.md). If no MCP, the next-best option for Softr DB is the bundled **`get-softr-database` CLI script** — tell the user to run `python3 ~/.claude/skills/softr-vibe-coding/tools/get-softr-database.py <database_id>` (it prompts for their Softr API key and exports the full schema to `~/Desktop/softr-database-<id>-<timestamp>.json` — Python stdlib only, nothing to install) and paste the resulting JSON into chat. As a final fallback, ask them to paste the `tablespace-with-tables` network response (DevTools -> Network -> filter that string while on Studio's Data tab) — same JSON content, different acquisition path. Optionally tell them they can install the MCP once with `claude mcp add --transport http softr https://mcp.softr.io/mcp` for future sessions. Full MCP details in [references/softr-mcp.md](references/softr-mcp.md); CLI script details in [datasources/softr-database.md](datasources/softr-database.md#bundled-cli-script-get-softr-database); fallback paste-in workflows in [datasources/fields.md](datasources/fields.md#field-inspector-block).86 - For **Airtable**, the most thorough path is the bundled **`get-airtable-base` shell script** — `bash ~/.claude/skills/softr-vibe-coding/tools/get-airtable-base` (requires `jq` — `brew install jq` on macOS). It prompts for Base ID + PAT, then exports the full schema (every table, every field with both `fld...` IDs and column names, relationships, webhooks, interfaces) to a timestamped Desktop folder. The user pastes `02-schema.json` or the combined `00-bundle.json` into chat. For lighter inspection (just a few fields, runtime-only), suggest the Field Inspector block — empty `q.select({})` works for Airtable. CLI script details in [datasources/airtable.md](datasources/airtable.md#bundled-cli-script-get-airtable-base).87 - For other non-Softr-DB sources where empty `q.select({})` works, suggest the Field Inspector block.88- **Brand colors**: Already resolved in Step 1 (Detect the brand source). Don't re-ask. The brand source is one of:89 - **Project's `./DESIGN.md`** (recommended for client work — generated by dembrandt in Step 1, or already present in the project; see [references/dembrandt.md](references/dembrandt.md))90 - **User's quick override** (paste of primary + accent + font)91 - **Default Softr palette** (only when the user explicitly opted in — never as a silent fallback):9293 | Color | Hex | Name | Use |94 |---|---|---|---|95 | Primary | `#386AF5` | Mariner (blue) | CTAs, links, active states |96 | Accent | `#FCB500` | Yellow Sea | Highlights, badges, sparkle accents |97 | Destructive | `#F53878` | Cabaret (pink) | Errors, destructive actions, required markers |98 | Text | `#030712` | Revolver (near-black) | Body text, headings |99 | Background | `#FFFFFF` | White | Page and card backgrounds |100101 Softr logo assets (for blocks that need Softr branding):102 - Icon + wordmark (SVG): `https://cdn.brandfetch.io/idytCFzVcY/theme/dark/logo.svg`103 - Icon only (PNG): `https://cdn.brandfetch.io/idytCFzVcY/w/1024/h/1024/theme/dark/icon.png`104- **Layout and style**: Cards vs. table vs. list? How many columns? Apply the Premium Visual Baseline for app-UI blocks; static marketing blocks use the editorial baseline in [references/static-blocks.md](references/static-blocks.md#editorial-baseline-replaces-the-premium-visual-baseline) instead.105- **Interactivity**: Create/edit/delete? Filtering? Sorting? Pagination?106- **User context**: Does it need to know who's logged in?107- **Settings**: Should anything be editable by the Softr builder (titles, images, toggle sections)?108109Don't over-ask. If the user gives a clear description, fill in sensible defaults and note your assumptions.110111## Examples (Decision Traces)112113**User:** "Build a team directory with cards showing name, role, and photo from Airtable"114**Claude:** Reads `datasources/airtable.md` -> uses `q.select()` with Airtable column names (e.g., `"Full Name"`, `"Role"`, `"Headshot"`) -> card grid layout with `repeat(auto-fit, minmax(280px, 1fr))` -> avatar with brand-color fallback initials -> loading skeleton matching card shape -> empty state with "No team members yet"115116**User:** "I need a form that pulls events from the Luma API and sends a webhook"117**Claude:** Reads `datasources/rest-api.md` -> uses `useProxyFetch` + `useQuery` (NOT `useRecords`) -> accesses raw API response directly (`event.name`, not `record.fields.name`) -> Select dropdown with event name + formatted date -> webhook via regular `fetch()` (not proxied) -> loading/error/empty states118119## Data Sources120121Softr supports 14 data sources. **Before writing any data-fetching code, read the relevant guide:**122123| Data Source | Guide | Approach |124|---|---|---|125| Softr Databases | [datasources/softr-database.md](datasources/softr-database.md) | `useRecords` + `q.select()` |126| Airtable | [datasources/airtable.md](datasources/airtable.md) | `useRecords` + `q.select()` |127| Google Sheets | [datasources/google-sheets.md](datasources/google-sheets.md) | `useRecords` + `q.select()` |128| HubSpot | [datasources/hubspot.md](datasources/hubspot.md) | `useRecords` + `q.select()` |129| Notion | [datasources/notion.md](datasources/notion.md) | `useRecords` + `q.select()` |130| Coda | [datasources/coda.md](datasources/coda.md) | `useRecords` + `q.select()` |131| monday.com | [datasources/monday.md](datasources/monday.md) | `useRecords` + `q.select()` |132| SmartSuite | [datasources/smartsuite.md](datasources/smartsuite.md) | `useRecords` + `q.select()` |133| ClickUp | [datasources/clickup.md](datasources/clickup.md) | `useRecords` + `q.select()` |134| Xano | [datasources/xano.md](datasources/xano.md) | `useRecords` + `q.select()` |135| Supabase | [datasources/supabase.md](datasources/supabase.md) | `useRecords` + `q.select()` |136| BigQuery | [datasources/bigquery.md](datasources/bigquery.md) | `useRecords` + `q.select()` |137| SQL Database | [datasources/sql-database.md](datasources/sql-database.md) | `useRecords` + `q.select()` |138| REST API | [datasources/rest-api.md](datasources/rest-api.md) | `useProxyFetch` + `useQuery` |139140**A block can connect to MORE THAN ONE of these at a time.** Declare them with `datasource.define({ alias: "uuid" })` and pass `from: ds.alias` on every data hook — read two tables and write to a third from a single block. Required reading before building anything multi-table: [datasources/multi-datasource.md](datasources/multi-datasource.md). (This replaces the old one-table-per-block limit and the invisible-helper-block workaround.)141142Shared data patterns, linked directly (read the one the task needs): **reading** records, filtering, sorting, pagination, metrics, charts, current user — [datasources/reading.md](datasources/reading.md); **writing** — mutations, sequential write queues, uploads, field-type write shapes — [datasources/writing.md](datasources/writing.md); **field values** — `getFieldValue()`, field shapes, debug blocks — [datasources/fields.md](datasources/fields.md). ([datasources/shared-patterns.md](datasources/shared-patterns.md) is the thin index of the same set.)143144For data source comparison and selection guidance, see [datasources/overview.md](datasources/overview.md).145146## Reference Guides147148For advanced patterns beyond data fetching, load the relevant reference when the task needs it:149150| If the task involves... | Load reference |151|---|---|152| Reading/writing **several tables from one block** — `datasource.define()`, the `from:` parameter, obtaining the datasource UUIDs (and why Studio's chat invents them) | [datasources/multi-datasource.md](datasources/multi-datasource.md) |153| Cross-*block* communication, window globals, breadcrumbs, publishing shared computed state. *(Multi-table reads no longer need a helper — use a second datasource.)* | [references/helper-blocks.md](references/helper-blocks.md) |154| Embedding third-party libraries with their own CSS (Leaflet, Mapbox, TinyMCE, Quill, FullCalendar) | [references/advanced-integrations.md](references/advanced-integrations.md) |155| Debugging a broken block, checking patterns before delivery, full violation catalog | [references/anti-patterns.md](references/anti-patterns.md) |156| Quick syntax check — import paths, hook signatures, mutation call shapes, field mapping | [references/quick-reference.md](references/quick-reference.md) |157| Any **dropdown / picker / combobox** in a block — why shadcn `<Select>` and native `<select>` both fail inside the shadow DOM, the `composedPath()` click-outside, sorting A→Z inside the component, multi-token filtering, **searchable by default** regardless of option count (`bare` inline editors click-only; `searchable={false}` only for a short fixed enum being set), the `bare` inline-editor variant | [references/searchable-dropdown.md](references/searchable-dropdown.md) |158| Small reusable patterns — `localStorage` cross-page state, clipboard copy button | [references/common-patterns.md](references/common-patterns.md) |159| Writing Airtable Automation Scripts / Scripting Extension scripts / Airtable formulas — companion to Softr blocks for cross-table cascades and computed values | [references/airtable-automations.md](references/airtable-automations.md) |160| The official **Softr MCP server** — Softr DB schema + full record/table/field/database CRUD (deletes included), field-level browsing of connected Airtable / Google Sheets / Notion / Supabase integrations, **creating, editing, versioning, and deploying Vibe Coding blocks directly** (`get_vibe_coding_docs`, `create_vibe_coding_block`, ...), app management/scaffolding, the **Softr Workflows** suite (26 tools, 418-node catalog), and **per-application MCP servers** | [references/softr-mcp.md](references/softr-mcp.md) |161| Restyling Softr's **native shell — header / footer / nav / dropdowns / page background** (not a block; it's Softr chrome, done with global Custom Code CSS): stable selectors vs. hashed classes, floating "island" header+footer, the dropdown blank-space grid fix, the multi-layer page-background stacking, restyle-vs-replace | [references/native-chrome-styling.md](references/native-chrome-styling.md) |162| Adding a **dynamic date filter or custom filter control to a native List/Grid block** (via a Custom Code Static block, not a Vibe block): drive the block's conditional filter with `{URL_PARAM:…}`, the empty-param "match nothing" wide-range sentinel, inject the control into the filter row and keep it alive across Softr's re-renders | [references/native-block-filters.md](references/native-block-filters.md) |163| **Editable settings deep-dive** — full hook catalog (incl. verified-undocumented `useLongTextSetting` and the `navigation` array-schema type), settings-first granularity doctrine, heading-line-split and `-text`/`-link` pairing patterns, naming conventions, rename-resets-value gotcha, empty-media gating, key-by-index rule | [references/editable-settings.md](references/editable-settings.md) |164| **Static marketing blocks** — heroes, landing headers, pricing tables, footers: workflow deltas (skip datasources), editorial baseline, full-bleed license, full-viewport sizing, block-owned fixed header + caveats, section anchors | [references/static-blocks.md](references/static-blocks.md) |165| Generating or refreshing a project **`DESIGN.md` with dembrandt** — MCP + CLI install (`@latest` npx, one-time browser step), the extract → poll → `get_findings` → `generate_design_md` → write-`./DESIGN.md` flow, multi-page crawls, DESIGN.md anatomy (frontmatter tokens, Font URLs), authoring `custom-code-header.html` from tokens, brand-drift QA with `compute_drift` | [references/dembrandt.md](references/dembrandt.md) |166167## Code Structure168169Every block follows this shape:170171```jsx172// imports at the top173import { ... } from "@/lib/datasource";174// ...other imports175176export default function Block() {177 // hooks, state, logic178 return (179 <div className="container py-0">180 <div className="content">181 <div className="py-3 px-8">182 {/* block content — wrapper padding depends on placement; see "Block Placement & Page Spacing" */}183 </div>184 </div>185 </div>186 );187}188```189190Wrap the outermost layout in `container` and `content` divs by default — these constrain width to match the Softr app's max width settings so the block aligns with neighboring native blocks. Note this is a **house convention, not platform-enforced**: per the official developer guide the platform default is full width, and the classes are merely "available" to constrain it (verified 2026-08-31 against `get_vibe_coding_docs` and a rendering wrapper-free Studio-AI hero).191192**Exceptions (omit the wrappers deliberately):**193- Blocks inside Softr column containers — Softr controls layout.194- Full-bleed marketing blocks (heroes, banner bands, footers) — backgrounds and decorative shapes run edge-to-edge; the block then owns its own gutters (`px-6 md:px-12 lg:px-16`) and inner max-widths, and records the choice in the `// BLOCK PLACEMENT:` comment. See [references/static-blocks.md](references/static-blocks.md#full-bleed-layout-license).195196## Block Placement & Page Spacing197198Blocks rarely live alone — most Softr pages stack 2–4 blocks vertically, often between a header and a footer. Spacing must be set per-block based on **where the block sits on the page**, so adjacent blocks don't double up padding or leave inconsistent gaps.199200**General rule:** the inner wrapper (the `<div>` directly inside `<div className="content">`) owns all vertical spacing. The outer `container` is always `py-0`. Top and bottom padding on the wrapper change based on what's above and below the block (another block, a header, a footer, or nothing).201202**When generating a new block, if the placement is not clear from the user's description, ASK before writing code:**203- Where will this block sit on the page? (top / middle / bottom / standalone)204- Is there a Softr header immediately above this block?205- Is there a Softr footer immediately below this block?206- Is there a Back button at the top of this block?207208**Detail pages — always ask about the back button AND its fallback URL.** A "detail page" is any block that reads a single record by URL recordId (i.e. it calls `useCurrentRecordId()` / `useRecord()`, or the user describes it as the target of a `/page?recordId=...` link). Users almost always want a back button there but rarely think to mention it, and shipping the page without one is the most common UX gap on these screens. So even if every other placement detail is clear, ask both:2092101. **"Should the detail page have a back button?"** — if yes, always wire one. Use the back-navigation pattern in [references/helper-blocks.md](references/helper-blocks.md#breadcrumb--back-navigation): `window.history.back()` for users with history, plus a fallback URL for users who arrived via shared link.2112. **"What page should the back button fall back to when there's no history?"** — this is a separate question, easy to skip but important. Don't default silently; ask. If the user doesn't have a listing page yet, default to `/` and leave a `// TODO: update fallback when /jobs (or similar) exists` comment so it can be updated later.212213**Chrome that repeats across pages must land in the SAME place on every page [house].** A back button,214a page title, a primary action -- anything the user meets on more than one screen -- is a cross-page215contract, not a per-block decision. Before adding one, open the blocks that already have it and copy216the exact offset; when you change it, change it everywhere in the same edit. Verified the hard way2172026-09-09: an item-detail back button carried an extra `mt-6` that the project-header back button did218not, so it sat 24px lower, and the mismatch only surfaced when a user moved between the two pages in219one session. **No amount of reading a single block reveals this** -- each block looks correct alone,220which is exactly why it needs to be a standing rule rather than a review item.221222Two habits make it survive:223224- **Let the wrapper's padding be the ONLY thing positioning repeated chrome.** Give the back button225 `mb-4` (space below it) and no top margin, so its offset is the wrapper's top padding and nothing226 else. One number per page then governs the position, and pages can only drift if their wrappers do.227- **Loading skeletons repeat the chrome too.** A skeleton standing in for a page that has a back button228 needs that button's placeholder at the same offset as the real one, or the button visibly jumps the229 moment the record arrives. Change both in the same edit -- see §12 of230 [ui-ux-guidelines.md](ui-ux-guidelines.md).231232**Persist the answer as a grep-able comment at the top of the generated file** so future edits know the spacing assumptions and can be updated consistently:233234```jsx235// BLOCK PLACEMENT: <position on page>, <header/footer adjacency>, <back button y/n>236// Spacing: <wrapper classes; back-button container if present>237```238239Example:240241```jsx242// BLOCK PLACEMENT: first block on page, header-adjacent, has Back button243// Spacing: wrapper py-3 px-8; back-button container mt-6 mb-4244```245246The `// BLOCK PLACEMENT:` marker is intentionally stable so it can be grepped and updated when the block's surroundings change.247248### Spacing values (defaults)249250**Container** (default — omitted by full-bleed blocks and blocks inside column containers; see table below): `<div className="container py-0">`251252**Inner wrapper** classes by block position:253254| Position on page | Wrapper classes | Rationale |255|---|---|---|256| First block (header-adjacent) | `py-3 px-8` | 12px top + 12px bottom; lets the Softr header own its own spacing |257| Middle block | `py-3 px-8` | 12px + Softr separator + 12px ≈ 24px between blocks |258| Last block (footer-adjacent) | `pt-3 pb-12 px-8` | 12px top + 48px bottom for footer breathing room |259| Standalone (only block on page) | `pt-3 pb-12 px-8` | Treat like a last block |260| Full-bleed (hero / banner / footer) | none — no container/content; block owns gutters `px-6 md:px-12 lg:px-16` | Edge-to-edge backgrounds; see [references/static-blocks.md](references/static-blocks.md#full-bleed-layout-license) |261262**Back button** (when present at the top of a block — typically on detail pages): wrap in `<div className="mt-6 mb-4">`. The `mt-6` (24px) adds breathing room above the button independent of wrapper padding; `mb-4` (16px) sits between the button and the first card. Apply this regardless of whether the block is first or mid-page.263264**Within-block stacked cards**: each card uses `mb-6` (24px). **Do NOT add `mb-6` to the last card** in a block — the wrapper's bottom padding already handles that buffer. Doubling them produces 32–40px gaps that look bigger than the within-block rhythm.265266**Net page rhythm**: between-block gaps (12 + 12 = 24px) match within-block card gaps (`mb-6` = 24px), so the page reads as one consistent vertical rhythm.267268### Full-viewport hero blocks269270A hero may size itself to the viewport — vh units inside a block resolve against the real window (blocks are shadow DOM in the main document, not iframes). The Studio-verified responsive shape is `min-h-screen lg:min-h-0 lg:h-screen` (natural height on mobile, locked viewport height on desktop). Three rules: (1) `h-screen` fills the window only when the native header is hidden on that page — with a native header above, a 100vh block overflows by the header height; use `min-h-[calc(100vh-<px>)]` when native chrome stays; (2) hard `h-screen` + `overflow-hidden` + centered flex **clips settings-grown content unrecoverably** — prefer `lg:min-h-screen` unless the locked look is explicitly wanted; (3) the standard spacing table above does not apply — the hero owns all its spacing. Extend the placement comment: `// BLOCK PLACEMENT: full-viewport hero, native header hidden, owns all spacing`. Full detail in [references/static-blocks.md](references/static-blocks.md#full-viewport-hero-sizing).271272## Premium Visual Baseline273274**Every block must look polished in its first version.** Styling is not a follow-up task — it is a core requirement of every code generation. Apply ALL of the following by default unless the user explicitly requests a minimal/plain style.275276**Scope: this is the app-UI baseline** — dashboards, lists, forms, detail pages. Static marketing blocks (heroes, landing sections, footers) use the **editorial baseline** in [references/static-blocks.md](references/static-blocks.md#editorial-baseline-replaces-the-premium-visual-baseline) instead — typographic hierarchy and brand-exact values, no gradient wrapper/cards/skeletons/empty states (nothing loads).277278Refer to [ui-ux-guidelines.md](ui-ux-guidelines.md) for full design principles.279280### 1. Gradient background wrapper281```jsx282<div className="rounded-2xl p-8" style={{ background: "linear-gradient(180deg, #EEF2FF 0%, #FFFFFF 100%)" }}>283 {/* header + content cards go inside here */}284</div>285```286Adjust the top gradient color to complement the user's brand.287288### 2. Header section289- Icon in a colored rounded square (`h-10 w-10 rounded-xl` with brand primary, white icon)290- Title at `text-2xl font-bold`291- Optional subtitle in `text-muted-foreground`292- Primary CTA button with `shadow-md hover:shadow-lg transition-shadow`293294### 3. Card-based content295- `bg-white rounded-xl shadow-sm border border-gray-100`296- Items with `hover:shadow-md hover:border-gray-200 transition-all duration-200`297- Use `space-y-3` or `gap-3`, never flat separators298299### 4. Avatar and identity elements300- Brand primary color as avatar fallback with white initials301- `h-12 w-12` for list items, `h-28 w-28` for profiles302- `border-2 border-white shadow-lg` on profile avatars303304### 5. Interactive feedback305- Buttons: `shadow-md hover:shadow-lg transition-shadow`306- Cards: `hover:shadow-md hover:border-gray-200 transition-all duration-200`307- Active states: blue left border accent (`border-l-4`)308309### 6. Status and metadata310- Counts in pill badges: `text-xs font-medium px-2 py-0.5 rounded-full`311- Dates with icons (Calendar, Mail, Users)312313### 7. Empty states314- Large icon in gradient square (`h-20 w-20 rounded-2xl`)315- Clear heading + explanation + CTA button316317### 8. Loading states318- Skeleton shapes matching the final layout, `rounded-xl`319320### 9. Error states321- Icon in tinted background, clear message, retry button322323### 10. Modals and dialogs324- Icon in dialog title, required field markers, example placeholders325326## Styling & Components327328**Tailwind CSS** is pre-configured. **Semantic color tokens** preferred:329`bg-background`, `bg-card`, `bg-primary`, `bg-secondary`, `bg-muted`, `bg-accent`, `bg-destructive`, `border`, `border-input`330331**Arbitrary values compile in full** — the platform's Tailwind build is JIT, so the whole arbitrary-value syntax works, including opacity modifiers on arbitrary hex (`bg-[#FAF5EC]/85`), variant + arbitrary + opacity combined (`hover:bg-[#6E7A5C]/10`), negative arbitrary values (`-top-[22%]`, `hover:-translate-y-[1px]`), arbitrary object-position (`object-[62%_25%]`), arbitrary z (`z-[1]`), and vw sizing (verified 2026-08-31 from rendering Studio-AI output). Classes must be **static source strings** — never template-interpolate (`` bg-[${x}] ``); JIT extracts classes by static scan (standard-Tailwind inference, not Softr-verified). When to reach for them vs. the scale: see the editorial lane in [ui-ux-guidelines.md](ui-ux-guidelines.md) §7. (This covers arbitrary *values* and standard variants; arbitrary *selector* variants like `[&_svg]:` have at least one known bundler failure — see the SelectTrigger row in [references/anti-patterns.md](references/anti-patterns.md#layout--styling).)332333**A brand colour you use BOTH ways exists twice, and the two copies drift silently.** Arbitrary values334are resolved at build time, so a class string can never read your `C.accent` constant. A card styled335with Tailwind (`border-[#3B1F2B] hover:border-[#54594F]`) and its loading skeleton styled inline336(`style={{ border: "1px solid " + C.accent }}`) therefore hold the same colour in two places that no337compiler will ever reconcile, and nothing fails when they disagree -- it just looks wrong. Verified3382026-09-09: a card's rest border was changed and its skeleton's was not, so the whole grid visibly339re-outlined itself the instant the data arrived. Two habits keep it honest: write the hex-to-token340mapping in a comment beside the class string (`#3B1F2B = C.accent`), and prefer the runtime token341wherever inline `style` is already in play, so only one of the two copies is ever a literal.342343**Font classes:** `font-heading`, `font-sans`, `font-mono`344345**Conditional classNames:** `import { cn } from "@/lib/utils";` — template-literal conditionals (`` className={`base ${cond ? "a" : "b"}`} ``) are equally valid (Studio AI emits them); prefer `cn()` when merging many groups or de-duplicating conflicting classes.346347**DO NOT USE:** CSS modules, styled-components, or CSS file imports.348349**shadcn/ui components** at `@/components/ui/[name]`:350accordion, 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, tooltip351352**Common import patterns:**353354```jsx355import { Button } from "@/components/ui/button";356import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";357import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogH358359…(truncated)