ComponentsData display

Code Block

Shows code from a reply, a canvas, a workflow step, or a file. It colors every language Shiki knows, over 240, picks the language from the file name, and copies the exact source with one button.

Code Block playgroundCopy uses your clipboard. Choose Blocked to see the error.
lead-score.ts
type Lead = { company: string; seats: number; replied: boolean };

export function scoreLead(lead: Lead): number {
  // Replies count more than company size.
  const base = Math.min(lead.seats, 50);
  return lead.replied ? base + 40 : base;
}

Usage

A code step in a workflow reviewTSX
import { CodeBlock } from "@tulipfarm/sprout/code-block";

<CodeBlock
  code={step.source}
  language="python"
  filename="score_lead.py"
/>

language takes a name, an alias, or an extension, such astypescript, ts, or py, so a Markdown fence name can go straight in. Leave it out and the language comes from filename: main.rs,Dockerfile, and .env.local all work.text, txt, and unknown names show plain text. The header shows filename, or the language’s name.lineNumbers is on by default. wrap wraps long lines for prose such as notes, and hides line numbers.

Syntax colors

Shiki tokenizes the code with its JavaScript regex engine, so there is no WebAssembly and no inline styles. Each grammar loads the first time a block needs it, and the block shows plain text until then. Markdown also loads the languages of its fences. Code over 200,000 characters stays plain so a huge file can’t freeze the page.

Recolor tokens for one themeCSS
[data-theme="sunset"] {
  --sprout-syntax-keyword: #c2410c;
  --sprout-syntax-string: #15803d;
  --sprout-syntax-comment: #78716c;
}

Tokens get the classes sprout-syntax-keyword,-string, -constant, -function,-parameter, -comment, -punctuation,-link, and, for Markdown and diffs, -heading,-inserted, and -deleted. Each reads a matching--sprout-syntax-* variable and falls back to the theme’s info, success, warning, danger, and muted colors, so every theme works without setup.

Highlighted on the serverTSX
import { preloadCodeLanguages } from "@tulipfarm/sprout/code-block";

await preloadCodeLanguages("tsx", "python");

Await preloadCodeLanguages before server rendering to send colored HTML. languageForFilename,codeLanguageName, and isHighlightedLanguageexpose the same lookup the block uses.

Variants

inline is a bordered card that scrolls after 24rem.file has no frame and fills its parent, so give the parent a height.preview drops the header and copy button and fades out at its parent’s height, for hover cards and thumbnails.

InlineBordered card, scrolls after 24rem.
fictional-garden.json
{
  "project": "Meadow",
  "tasks": 3,
  "ready": true
}
FileFills its parent. No frame.
memory/preferences.md
# Preferences

- Reply in short paragraphs.
- Use metric units.
- Ask before sending email.

In a chat reply

Render fenced code from Markdown with the fence language and no filename. A fence without a language shows as text.

A Markdown pre overrideTSX
pre({ node, children }) {
  const code = node?.children[0];
  if (code?.type !== "element" || code.tagName !== "code") return <pre>{children}</pre>;
  const language = languageClass(code)?.slice("language-".length) ?? "text";
  return <CodeBlock code={textOf(code).replace(/\n$/, "")} language={language} />;
}

Chat responseFenced code in a reply. The fence names the language; no filename.

Here is a query that counts signups per week for the last two months:

SQL
SELECT date_trunc('week', created_at) AS week, count(*) AS signups
FROM accounts
WHERE created_at >= now() - interval '8 weeks'
GROUP BY 1 ORDER BY 1 DESC;

Run it in your warehouse, then paste the rows back. To chart them in a script:

TypeScript
const weeks = rows.map((row) => ({ week: new Date(row.week), signups: Number(row.signups) }));

Expected output:

Plain text
week        signups
2026-10-12  184
2026-10-05  171

Canvas and workflow steps

Canvas code resultInside CanvasSection; filename falls back to the canvas tab title.
Funnel stages
WITH
  stage_01 AS (SELECT * FROM raw_events WHERE step = 1),
  stage_02 AS (SELECT * FROM stage_01 WHERE step = 2),
  stage_03 AS (SELECT * FROM stage_02 WHERE step = 3),
  stage_04 AS (SELECT * FROM stage_03 WHERE step = 4),
  stage_05 AS (SELECT * FROM stage_04 WHERE step = 5),
  stage_06 AS (SELECT * FROM stage_05 WHERE step = 6),
  stage_07 AS (SELECT * FROM stage_06 WHERE step = 7),
  stage_08 AS (SELECT * FROM stage_07 WHERE step = 8),
  stage_09 AS (SELECT * FROM stage_08 WHERE step = 9),
  stage_10 AS (SELECT * FROM stage_09 WHERE step = 10),
  stage_11 AS (SELECT * FROM stage_10 WHERE step = 11),
  stage_12 AS (SELECT * FROM stage_11 WHERE step = 12),
  stage_13 AS (SELECT * FROM stage_12 WHERE step = 13),
  stage_14 AS (SELECT * FROM stage_13 WHERE step = 14),
  stage_15 AS (SELECT * FROM stage_14 WHERE step = 15),
  stage_16 AS (SELECT * FROM stage_15 WHERE step = 16),
  stage_17 AS (SELECT * FROM stage_16 WHERE step = 17),
  stage_18 AS (SELECT * FROM stage_17 WHERE step = 18),
  stage_19 AS (SELECT * FROM stage_18 WHERE step = 19),
  stage_20 AS (SELECT * FROM stage_19 WHERE step = 20),
  stage_21 AS (SELECT * FROM stage_20 WHERE step = 21),
  stage_22 AS (SELECT * FROM stage_21 WHERE step = 22),
  stage_23 AS (SELECT * FROM stage_22 WHERE step = 23),
  stage_24 AS (SELECT * FROM stage_23 WHERE step = 24),
  stage_25 AS (SELECT * FROM stage_24 WHERE step = 25),
  stage_26 AS (SELECT * FROM stage_25 WHERE step = 26),
  stage_27 AS (SELECT * FROM stage_26 WHERE step = 27),
  stage_28 AS (SELECT * FROM stage_27 WHERE step = 28),
  final AS (SELECT * FROM stage_28)
SELECT count(*) FROM final;
Workflow code stepDraft review detail. Filename is the step id plus .js or .py.
  1. Step 1: run JavaScript code with the trigger input

    calculate.js
    return { total: input.amount };
  2. Step 2: run Python code with Lead from Step 1

    score_lead.py
    score = 0
    if lead["replied"]:
        score += 40
    score += min(lead["seats"], 50)
    return {"score": score}
Plain text without line numberslanguage defaults to text: no colors, and the header shows “text”.
Plain text
Plant bulbs
Water gently
Enjoy the flowers

File pane

A file that changesTSX
<div className="flex h-full min-h-0 flex-col">
  <CodeBlock
    key={`${file.revision}:${file.path}`}
    code={file.content}
    filename={file.path}
    variant="file"
  />
  {file.content === "" && <p>This file is empty.</p>}
</div>

A new key per file and revision resets the copy state and scroll position. Long paths truncate in the header; the region and the copy button still use the full path.

File explorervariant="file" fills the pane. Each file remounts the block.
SOUL.md
# Juniper

You help the growth team plan launches.
Keep answers short. Cite the source table for every number.

Shell examples

Turn off line numbers for shell commands so a copied command runs as is.

Webhook setupA shell example: plain text, no line numbers, long lines scroll.

POST https://hooks.tulipfarm.app/w/wf_8k2m4qz7/run

curl example
body='{"requestId":"'"$(uuidgen)"'","input":{"example":true}}'
timestamp=$(date +%s)
signature=$(printf '%s.%s' "$timestamp" "$body" | openssl dgst -sha256 -hmac "$TULIPFARM_WEBHOOK_KEY" | sed 's/^.*= //')
curl -X POST 'https://hooks.tulipfarm.app/w/wf_8k2m4qz7/run' \
  -H "X-TulipFarm-Timestamp: $timestamp" \
  -H "X-TulipFarm-Signature: sha256=$signature" \
  -H 'Content-Type: application/json' \
  --data-binary "$body"

Set TULIPFARM_WEBHOOK_KEY to your key before you run the example. The example never contains the key.

Accessibility

The code scrolls in a focusable region named after the file, or “Language code”, such as “TypeScript code”. The preview variant is not focusable. Line numbers are hidden from screen readers. The copy button is named “Copy filename” or “Copy code”, stays focusable while it copies, shows Copied for 2 seconds, and announces “Code copied to clipboard.” When the clipboard is blocked an alert says to select the code instead, and Copy tries again. On touch screens the button is 44px tall. The error fades in over 180ms, or appears at once with reduced motion.