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.

Informations de source

Dépôt
victoriaplummer/airtable-interface-extension-toolkit
Dernière activité de la source
27 mars 2026 à 15:15
Langue détectée de SKILL.md
anglais
Étoiles
51
Forks
2

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub