Skip to main content

airtable-custom-interface-extensions

Complete SDK reference for building Airtable Interface Extensions — models, hooks, components, field types, write patterns, and common pitfalls.

Source facts

Repository
victoriaplummer/airtable-interface-extension-toolkit
Last source activity
March 27, 2026 at 15:15
Detected SKILL.md language
English
Stars
51
Forks
2

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
Airtable Custom Interface Extensions
description
Complete SDK reference for building Airtable Interface Extensions — models, hooks, components, field types, write patterns, and common pitfalls.
whenToUse
When building, modifying, or debugging Airtable Custom Interface Extensions using the @airtable/blocks/interface SDK.
# Airtable Custom Interface Extensions — SDK Reference You are building Airtable Interface Extensions. These are React components that run inside Airtable Interfaces. They use a specific SDK (`@airtable/blocks/interface`) — NOT the older Blocks SDK or the REST API. ## Quick Start Scaffold ```tsx import {initializeBlock} from '@airtable/blocks/interface/ui'; import React from 'react'; function App() { return <div>Hello world</div>; } // Entry point — this is NOT ReactDOM.render initializeBlock({interface: () => <App />}); ``` **CLI setup**: `npm install -g @airtable/blocks-cli` → `block init NONE/blkXXX --template=<template-url> my-extension` → `cd my-extension` → `block run` **Requires**: Node 22+, PAT token with `block:manage` scope configured via `block set-api-key`. --- ## Import Map Everything comes from two paths: ```tsx // Models & types import {FieldType} from '@airtable/blocks/interface/models'; // Hooks, components, utils — everything UI import { useBase, useRecords, useCustomProperties, useGlobalConfig, useSynced, useSession, useRunInfo, useColorScheme, useWatchable, CellRenderer, expandRecord, initializeBlock, colors, colorUtils, loadCSSFromString, loadCSSFromURLAsync, loadScriptFromURLAsync, } from '@airtable/blocks/interface/ui'; ``` There is NO Airtable-provided UI component library beyond `<CellRenderer>`. For buttons, inputs, selects, dialogs, etc., use a third-party library like **MUI** (Material UI — used in Airtable's own sliding-bar-chart example) or plain HTML/React elements. See the Styling & External Libraries section for options. ### Old Blocks SDK vs New Interface Extensions SDK Claude's training data contains extensive examples from the **old** `@airtable/blocks` SDK. Do NOT use those patterns. Key differences: | Old Blocks SDK (`@airtable/blocks/ui`) | New Interface Extensions (`@airtable/blocks/interface/ui`) | |---|---| | `<Button>`, `<Input>`, `<Box>`, `<FormField>`, `<Select>`, `<Dialog>`, `<Tooltip>`, `<Icon>`, etc. | **None of these exist.** Only `<CellRenderer>`. Use plain HTML/React. | | `import {useBase} from '@airtable/blocks/ui'` | `import {useBase} from '@airtable/blocks/interface/ui'` | | `initializeBlock(() => <App />)` | `initializeBlock({interface: () => <App />})` | | `useRecords(queryResult)` with views/sorts/fields | `useRecords(table)` — table-level only, no view access | | `cursor`, `viewport`, `useViewport` | Not available | --- ## Styling & External Libraries ### npm packages work The blocks CLI uses webpack. Any npm package compatible with webpack works. Install via `npm install` in your extension directory. ### Recommended libraries | Need | Recommended | Notes | |------|-------------|-------| | **Charts** | `recharts` (React), `d3` + `d3-cloud` | Airtable's official word-cloud example uses D3. Recharts is easier for standard charts. | | **Date handling** | `date-fns` or `dayjs` | Lighter than moment.js. Date fields return ISO 8601 strings. | | **Component library** | `@mui/material` (MUI) | Airtable's own sliding-bar-chart example uses MUI v7 + Emotion. Full component set (buttons, inputs, dialogs, etc.). | | **Headless UI** | `@radix-ui/react-*` | Accessible primitives (dialogs, dropdowns, tooltips) without styling opinions. Lighter than MUI. | | **Icons** | `@phosphor-icons/react` | Append `Icon` suffix when importing: `import {ArrowRightIcon} from '@phosphor-icons/react'`. | | **Drag & drop** | `@dnd-kit/core` | Accessible drag-and-drop for kanban boards, sortable lists, etc. | | **Markdown** | `marked` | Parse markdown content from rich text or long text fields. | | **3D models** | `@google/model-viewer` | Render 3D models inline. | | **CSS framework** | Plain CSS or **Tailwind CSS** | Tailwind is officially supported — used in Airtable's own map extension. See Tailwind section below. | **React 19 note:** If a third-party library doesn't list React 19 as a peer dependency, use `npm install --legacy-peer-deps` to install it. ### CSS approach Import CSS files directly — the webpack bundler handles them: ```tsx import './style.css'; ``` For external CSS: ```tsx await loadCSSFromURLAsync('https://cdn.example.com/library.css'); ``` For dynamic CSS: ```tsx loadCSSFromString(` .my-card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; } @media (prefers-color-scheme: dark) { .my-card { border-color: #444; background: #2a2a2a; } } `); ``` ### Tailwind CSS — officially supported Tailwind works with the blocks CLI. Airtable's own [map extension](https://github.com/Airtable/interface-extensions-map) uses this exact setup. The webpack bundler auto-detects PostCSS when the loaders are installed. **Setup:** ```bash npm install -D tailwindcss postcss postcss-loader css-loader style-loader autoprefixer @airtable/blocks-webpack-bundler ``` **tailwind.config.js** (at project root): ```js module.exports = { // CRITICAL: must be 'media', not 'class'. Airtable controls dark mode via // prefers-color-scheme, not a CSS class. Using 'class' means dark: utilities // won't fire unless you manually add a 'dark' class wrapper — and even then // it won't work on first render before JS hydrates. darkMode: 'media', content: ['./frontend/**/*.{js,ts,jsx,tsx}'], theme: { extend: { colors: { // Map Airtable's design tokens to Tailwind utilities blue: { DEFAULT: 'rgb(22, 110, 225)', dark1: 'rgb(13, 82, 172)', light1: 'rgb(160, 198, 255)', light2: 'rgb(209, 226, 255)', }, // Add more Airtable colors as needed — see the full token set at: // github.com/nabong04/airtable-geocoded-locations-map/blob/main/tailwind.config.js }, }, }, }; ``` **frontend/style.css:** ```css @tailwind base; @tailwind components; @tailwind utilities; ``` Then `import './style.css'` in your component. Use classes like `bg-blue-light2`, `text-gray-900`, etc. **Raw CSS dark mode:** For any styles written in plain CSS (not Tailwind utilities), use `@media (prefers-color-scheme: dark)` to match Airtable's dark mode — not `.dark` parent selectors. **References:** - [Airtable/interface-extensions-map](https://github.com/Airtable/interface-extensions-map) — official Airtable example with Tailwind + TypeScript + Mapbox - [nabong04/airtable-geocoded-locations-map](https://github.com/nabong04/airtable-geocoded-locations-map) — community example with full Airtable design token mapping in `tailwind.config.js` ### Design tokens from Airtable Use the built-in `colors` and `colorUtils` to match Airtable's palette (works without Tailwind): ```tsx import {colors, colorUtils} from '@airtable/blocks/interface/ui'; // Airtable's blue: '#2d7ff9' const airtableBlue = colorUtils.getHexForColor(colors.BLUE); // Check contrast for text on colored backgrounds if (colorUtils.shouldUseLightTextOnColor(colors.BLUE_DARK_1)) { // use white text } ``` Available color families: BLUE, CYAN, GRAY, GREEN, ORANGE, PINK, PURPLE, RED, TEAL, YELLOW — each with base, BRIGHT, DARK_1, LIGHT_1, LIGHT_2 variants. --- ## Reading Data ### Access the base and tables ```tsx function App() { const base = useBase(); const table = base.getTableByName('Tasks'); // throws if not found const table2 = base.getTableByNameIfExists('X'); // returns null if not found // Also: base.getTableById(), base.getTableByIdIfExists(), base.getTable(idOrName) // base.tables — all tables (arbitrary order) // base.name, base.color, base.activeCollaborators, base.workspaceId } ``` ### Read records ```tsx function RecordList() { const base = useBase(); const table = base.getTableByName('Tasks'); const records = useRecords(table); // auto-refreshes on changes return ( <ul> {records.map(record => ( <li key={record.id}> {record.name} {/* primary field as string */} {record.getCellValue('Status')} {/* raw cell value */} {record.getCellValueAsString('Due Date')} {/* formatted string */} </li> ))} </ul> ); } ``` ### Read from multiple tables Use custom properties to let builders configure which tables to use: ```tsx function getCustomProperties(base) { return [ { key: 'projectsTable', label: 'Projects Table', type: 'table', defaultValue: base.tables.find(t => t.name.toLowerCase().includes('projects')), }, { key: 'tasksTable', label: 'Tasks Table', type: 'table', defaultValue: base.tables.find(t => t.name.toLowerCase().includes('tasks')), }, ]; } function MyExtension() { const {customPropertyValueByKey} = useCustomProperties(getCustomProperties); const projectsTable = customPropertyValueByKey.projectsTable; const tasksTable = customPropertyValueByKey.tasksTable; const projectRecords = useRecords(projectsTable); const taskRecords = useRecords(tasksTable); // ... } ``` ### Field access **Always use `getFieldIfExists`** — it returns `null` instead of throwing. The throwing variants (`getField`, `getFieldByName`, `getFieldById`) will crash your extension if a field was deleted or isn't visible. ```tsx const field = table.getFieldIfExists('Status'); // returns Field | null if (!field) return <div>Please configure the Status field</div>; field.type // FieldType enum value, e.g. 'singleSelect' field.name // string field.options // field-specific options (see FieldType reference) field.config // { type, options } — useful for type narrowing field.isComputed // true for formula, rollup, autoNumber, etc. field.isPrimaryField field.description // string | null ``` **Best practice:** Don't hardcode field names. Use custom properties to let builders select fields (see Custom Properties section). Use `record.getCellValueAsString(field)` when you just need to display a value without handling each field type individually. **Always use the FieldType enum for comparisons** — never compare against string literals: ```tsx import {FieldType} from '@airtable/blocks/interface/models'; // ✅ CORRECT if (field.type === FieldType.SINGLE_SELECT) { /* ... */ } // ❌ WRONG — don't use string literals if (field.type === 'singleSelect') { /* ... */ } ``` --- ## Writing Data ### CRITICAL: Always check permissions first Interface Designer can disable editing per-field. Users may have read-only access. Always check before writing: ```tsx // Simple boolean check if (table.hasPermissionToCreateRecord()) { /* ... */ } if (table.hasPermissionToUpdateRecord(record, {'Status': {name: 'Done'}})) { /* ... */ } if (table.hasPermissionToDeleteRecord(record)) { /* ... */ } // Detailed check with reason string (for showing error messages) const check = table.checkPermissionsForUpdateRecord(record, fields); if (!check.hasPermission) { alert(check.reasonDisplayString); // e.g. "You don't have permission to edit this field" } ``` Use `undefined` as placeholder for unknown values in permission checks: ```tsx // "Can user update this record at all?" (unknown fields) table.hasPermissionToUpdateRecord(record); // "Can user update this field on some record?" (unknown record) table.hasPermissionToUpdateRecord(undefined, {'Status': undefined}); ``` ### Create records ```tsx // Single record — returns Promise<RecordId> const newId = await table.createRecordAsync({ 'Project Name': 'New project', 'Budget': 100, }); // By field ID instead of name await table.createRecordAsync({ [nameField.id]: 'New project', [budgetField.id]: 100, }); // Batch — max 50 records per call const ids = await table.createRecordsAsync([ {fields: {'Name': 'Record 1'}}, {fields: {'Name': 'Record 2'}}, ]); ``` ### Update records
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub