airtable-custom-interface-extensions
Complete SDK reference for building Airtable Interface Extensions — models, hooks, components, field types, write patterns, and common pitfalls.
معلومات المصدر
- المستودع
- victoriaplummer/airtable-interface-extension-toolkit
- آخر نشاط في المصدر
- ٢٧ مارس ٢٠٢٦ في ١٥:١٥
- لغة SKILL.md المكتشفة
- الإنجليزية
- النجوم
- ٥١
- التفرعات
- ٢
خيارات التثبيت
يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.
مراجعة ملفات المصدر
اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.
عرض SKILL.md
SKILL.md
تعليمات المصدر · معاينة للقراءة فقط- 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
عرض على GitHubملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub