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.

Quellinformationen

Repository
victoriaplummer/airtable-interface-extension-toolkit
Letzte Quellaktivität
27. März 2026 um 15:15
Erkannte Sprache von SKILL.md
Englisch
Sterne
51
Forks
2

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen