| name | raycast-extensions |
| description | Build Raycast extensions using the Raycast API: commands, list views, forms, and preferences. Triggers on: Raycast, @raycast/api, raycast extension, raycast command, showToast, List.Item, Action.
|
Raycast Extensions
When to Use
Trigger when building, debugging, or publishing Raycast extensions. Covers List, Form, and Detail commands; preferences; LocalStorage; navigation; clipboard; and the submission process.
Core Rules
- Raycast extensions use React + TypeScript โ always use functional components
- Every command exports a default React component
- API imports always come from
@raycast/api
- Use
showToast for feedback โ never console.log in production paths
- Keep commands focused โ one command, one job
- Extensions live in
~/.config/raycast/extensions/ (dev) or the store
Extension Structure
my-extension/
โโโ package.json # Extension manifest
โโโ src/
โ โโโ index.tsx # Main command
โ โโโ second-command.tsx
โ โโโ utils.ts
โโโ assets/
โ โโโ extension-icon.png # 512ร512 PNG
โโโ tsconfig.json
package.json manifest
{
"name": "my-extension",
"title": "My Extension",
"description": "What it does in one sentence",
"icon": "extension-icon.png",
"author": "example-author",
"categories": ["Productivity"],
"license": "MIT",
"commands": [
{
"name": "index",
"title": "My Command",
"description": "Launch my command",
"mode": "view"
},
{
"name": "quick-action",
"title": "Quick Action",
"description": "No UI command",
"mode": "no-view"
}
],
"dependencies": {
"@raycast/api": "^1.70.0"
},
"devDependencies": {
"@raycast/eslint-config": "^1.0.8",
"typescript": "^5.4.0"
},
"scripts": {
"build": "ray build -e dist",
"dev": "ray develop",
"lint": "ray lint"
}
}
List Command
The most common command type โ searchable list of items.
import {
List,
Action,
ActionPanel,
showToast,
Toast,
Icon,
} from "@raycast/api";
import { useState } from "react";
interface Item {
id: string;
title: string;
subtitle?: string;
url?: string;
}
export default function Command() {
const [searchText, setSearchText] = useState("");
const [items] = useState<Item[]>([
{ id: "1", title: "First Item", subtitle: "Subtitle", url: "https://example.com" },
{ id: "2", title: "Second Item" },
]);
const filtered = items.filter((item) =>
item.title.toLowerCase().includes(searchText.toLowerCase())
);
return (
{filtered.map((item) => (
{
await showToast({
style: Toast.Style.Success,
title: "Done!",
message: `Acted on ${item.title}`,
});
}}
/>
}
/>
))}
);
}
List with Sections
<List>
<List.Section title="Recent" subtitle="Last 7 days">
{recentItems.map((item) => <List.Item key={item.id} title={item.title} />)}
</List.Section>
<List.Section title="All">
{allItems.map((item) => <List.Item key={item.id} title={item.title} />)}
</List.Section>
</List>
List with Metadata (Detail side panel)
<List isShowingDetail>
<List.Item
title="Product"
detail={
<List.Item.Detail
markdown={`# Product\n\nDescription here.`}
metadata={
<List.Item.Detail.Metadata>
<List.Item.Detail.Metadata.Label title="Price" text="$29.99" />
<List.Item.Detail.Metadata.Separator />
<List.Item.Detail.Metadata.TagList title="Tags">
<List.Item.Detail.Metadata.TagList.Item text="new" color="#00ff00" />
</List.Item.Detail.Metadata.TagList>
</List.Item.Detail.Metadata>
}
/>
}
/>
</List>
Form Command
import { Form, ActionPanel, Action, showToast, Toast, popToRoot } from "@raycast/api";
import { useState } from "react";
interface FormValues {
name: string;
email: string;
category: string;
notify: boolean;
date: Date;
}
export default function CreateForm() {
const [nameError, setNameError] = useState<string | undefined>();
async function handleSubmit(values: FormValues) {
if (!values.name) {
setNameError("Name is required");
return;
}
await showToast({ style: Toast.Style.Animated, title: "Submitting..." });
try {
await showToast({ : .., : , : values. });
();
} (err) {
({ : .., : , : (err) });
}
}
(
);
}
Detail Command
import { Detail, ActionPanel, Action } from "@raycast/api";
export default function ShowDetail() {
const markdown = `
# Report
**Generated:** ${new Date().toLocaleDateString()}
## Summary
Some content with **bold** and \`code\`.
\`\`\`json
{ "status": "ok" }
\`\`\`
`;
return (
<Detail
markdown={markdown}
navigationTitle="Report"
actions={
<ActionPanel>
<Action.CopyToClipboard content={markdown} />
<Action.OpenInBrowser url="https://example.com" />
</ActionPanel>
}
/>
);
}
No-View Command
Quick actions with no UI โ runs instantly and shows a toast.
import { showToast, Toast, Clipboard } from "@raycast/api";
export default async function Command() {
const text = await Clipboard.readText();
if (!text) {
await showToast({ style: Toast.Style.Failure, title: "Clipboard is empty" });
return;
}
const transformed = text.toUpperCase();
await Clipboard.copy(transformed);
await showToast({
style: Toast.Style.Success,
title: "Transformed!",
message: `${text.length} chars uppercased`,
});
}
Preferences API
Declare in package.json:
"preferences": [
{
"name": "apiKey",
"title": "API Key",
"description": "Your API key",
"type": "password",
"required": true
},
{
"name": "defaultFolder",
"title": "Default Folder",
"type": "directory",
"required": false,
"default": "~/Documents"
},
{
"name": "theme",
"title": "Theme",
"type"
Read in code:
import { getPreferenceValues } from "@raycast/api";
interface Preferences {
apiKey: string;
defaultFolder: string;
theme: "auto" | "dark";
}
const prefs = getPreferenceValues<Preferences>();
console.log(prefs.apiKey);
LocalStorage (per-extension persistence)
import { LocalStorage } from "@raycast/api";
await LocalStorage.setItem("last-query", "search text");
await LocalStorage.setItem("config", JSON.stringify({ count: 5 }));
const lastQuery = await LocalStorage.getItem<string>("last-query");
const config = JSON.parse((await LocalStorage.getItem<string>("config")) ?? "{}");
await LocalStorage.removeItem("last-query");
await LocalStorage.clear();
const all = await LocalStorage.allItems();
Navigation
import { useNavigation, List, Detail, Action, ActionPanel } from "@raycast/api";
function DetailView({ title }: { title: string }) {
const { pop } = useNavigation();
return (
<Detail
markdown={`# ${title}`}
actions={
<ActionPanel>
<Action title="Go Back" onAction={pop} />
</ActionPanel>
}
/>
);
}
export default function RootCommand() {
const { push } = useNavigation();
return (
<List>
<List.Item
title="Open Detail"
actions={
<ActionPanel>
<Action title="Open" onAction={() => push(<DetailView = />)} />
}
/>
);
}
Useful Actions
<Action.OpenInBrowser url="https://example.com" />
<Action.CopyToClipboard content="text to copy" />
<Action.Paste content="text to paste" />
<Action.Open title="Open File" target="/path/to/file" />
<Action
title="Run Script"
onAction={() => {
import { exec } from "child_process";
exec("open -a 'Obsidian'");
}}
/>
<Action.OpenExtensionPreferences />
Environment Info
import { environment } from "@raycast/api";
environment.extensionName;
environment.commandName;
environment.raycastVersion;
environment.isDevelopment;
environment.assetsPath;
environment.supportPath;
Development Workflow
npm install -g @raycast/api
ray create my-extension
cd my-extension && npm run dev
npm run lint
npm run build
ray publish
Common Patterns
Fetch data with loading state
import { List, showToast, Toast } from "@raycast/api";
import { useEffect, useState } from "react";
export default function Command() {
const [items, setItems] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch("https://api.example.com/items")
.then((r) => r.json())
.then((data) => setItems(data))
.catch(async (err) => {
await showToast({ style: Toast.Style.Failure, title: "Failed", message: String(err) });
})
.finally(() => setIsLoading(false));
}, []);
return (
<List isLoading={isLoading}>
{items.map((item) => )}
);
}
Open URL shortcut
import { open } from "@raycast/api";
await open("https://example.com");
Related Skills
applescript-jxa โ Mac automation
keyboard-maestro โ workflow automation
shortcuts-skill โ Apple Shortcuts
GitNexus Index
This skill is indexed by GitNexus for knowledge graph traversal.
Index path: /Users/localuser/.claude/skills/raycast-extensions/.gitnexus
Last indexed: 2026-05-23