@mshafiqyajid/react-code-block
Headless code block hook and styled syntax-highlighted code component for React. Optional Shiki integration, diff view, line numbers, copy button, terminal bar, focus lines, collapsible, badge.
Playground #
import { useState } from "react";
interface CounterProps {
initialCount?: number;
}
export function Counter({ initialCount = 0 }: CounterProps) {
const [count, setCount] = useState(initialCount);
return (
<div className="counter">
<button onClick={() => setCount((c) => c - 1)}>β</button>
<span>{count}</span>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</div>
);
}
import { CodeBlockStyled } from "@mshafiqyajid/react-code-block/styled";
import "@mshafiqyajid/react-code-block/styles.css";
<CodeBlockStyled
language="tsx"
/>Install #
npm install @mshafiqyajid/react-code-block For syntax highlighting, install Shiki as an optional peer dep:
npm install shiki Quick start #
import { CodeBlockStyled } from "@mshafiqyajid/react-code-block/styled";
import "@mshafiqyajid/react-code-block/styles.css";
export function Example() {
return (
<CodeBlockStyled
code={`import { useState } from "react";
export function Counter({ initialCount = 0 }) {
const [count, setCount] = useState(initialCount);
return (
<div>
<button onClick={() => setCount(c => c - 1)}>β</button>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}`}
language="tsx"
showLanguageLabel
showCopy
/>
);
} Terminal mode #
Set terminal to render a macOS-style window chrome header. The existing title prop becomes the centered window title.
<CodeBlockStyled
code={shellCode}
language="bash"
terminal
title="~/projects/my-app"
showCopy
/> Collapsible #
Use maxLines to clamp visible lines. A gradient fade and an Expand / Collapse button appear automatically. Set expandable= to disable the button and keep the block permanently truncated.
<CodeBlockStyled
code={longCode}
language="tsx"
maxLines={10}
showLineNumbers
/> Focus lines #
Pass 1-based line numbers to focusLines to highlight those lines and dim all others to 30% opacity. Transitions are 200 ms.
<CodeBlockStyled
code={multiLineCode}
language="tsx"
focusLines={[3, 4, 5, 6]}
showLineNumbers
/> Badge #
The badge prop renders a small pill label in the header bar next to the language label. Useful for "Live", "New", "Beta", etc.
<CodeBlockStyled code={code} language="tsx" badge="Live" showLanguageLabel /> Diff mode #
Set diff to parse lines prefixed with + (add) or - (remove). Neutral lines begin with a space.
<CodeBlockStyled
code={`-const old = "removed";
+const next = "added";
const unchanged = true;`}
language="typescript"
diff
title="example.ts"
/> Line highlights #
Pass 1-based line numbers to highlightLines to draw attention to specific lines.
<CodeBlockStyled
code={multiLineCode}
language="tsx"
highlightLines={[3, 7]}
showLineNumbers
/> With title #
<CodeBlockStyled
code={code}
language="tsx"
title="src/components/Button.tsx"
showLanguageLabel
/> Headless usage #
import { useCodeBlock } from "@mshafiqyajid/react-code-block";
function MyCodeBlock({ code, language = "text" }) {
const { rootProps, copyProps, isCopied } = useCodeBlock({
code,
language,
copyLabel: "Copy",
copiedLabel: "Copied!",
onCopy: () => console.log("copied!"),
});
return (
<div {...rootProps}>
<pre><code>{code}</code></pre>
{isCopied ? (
<button {...copyProps}>Copied!</button>
) : (
<button {...copyProps}>Copy</button>
)}
</div>
);
} Theme #
theme="auto" (default) reads the data-theme attribute on
document.documentElement and maps "dark" β Shiki's
github-dark-default theme, and "light" (or no attribute) β
github-light. A MutationObserver watches for attribute changes
so the highlighted output updates when you toggle the site theme.
Pass any Shiki theme name as the theme prop to pin a specific theme:
<CodeBlockStyled code={code} language="ts" theme="dracula" /> Props #
| Prop | Type | Default | Description |
|---|---|---|---|
| code | string | β | The code string to display |
| language | string | "text" | Language identifier passed to Shiki |
| theme | "auto" | string | "auto" | Shiki theme; "auto" follows [data-theme] |
| showLineNumbers | boolean | false | Prepend line numbers to each line |
| highlightLines | number[] | [] | 1-based line numbers to highlight |
| diff | boolean | false | Parse +/- prefix as add/remove diff lines |
| showCopy | boolean | true | Show the copy button |
| copyLabel | string | "Copy" | Copy button label |
| copiedLabel | string | "Copied!" | Label after successful copy (2 s) |
| onCopy | () => void | β | Called on successful copy |
| title | string | β | Filename or label shown in the header bar |
| showLanguageLabel | boolean | true | Show the language identifier in the header |
| maxHeight | string | number | β | Max height of the scrollable code area |
| wrap | boolean | false | Wrap long lines instead of scrolling |
| size | "sm" | "md" | "lg" | "md" | Font size and padding scale |
| radius | "none" | "sm" | "md" | "lg" | "md" | Border radius |
| maxLines | number | β | Clamp visible lines; shows fade and expand button |
| expandable | boolean | true | Show expand/collapse button when maxLines is set |
| terminal | boolean | false | Render a macOS-style terminal header bar |
| focusLines | number[] | β | 1-based lines to focus; others are dimmed to 30% |
| badge | string | β | Pill badge shown in header (e.g. "Live", "Beta") |
| className | string | β | Extra class on the root element |
| style | CSSProperties | β | Inline style on the root element |
| ref | Ref<HTMLDivElement> | β | Forwarded to the root div |
Data attributes #
| Attribute | Values |
|---|---|
| data-language | The current language string |
| data-theme | "light" | "dark" |
| data-diff | "true" when diff is enabled |
| data-wrap | "true" when wrap is enabled |
| data-has-title | "true" when a title is set |
| data-size | "sm" | "md" | "lg" |
| data-radius | "none" | "sm" | "md" | "lg" |
| data-collapsed | "true" when maxLines is set and block is collapsed |
| data-terminal | "true" when terminal mode is active |
| data-has-focus | "true" when focusLines is non-empty |
| data-has-badge | "true" when a badge is set |
| data-focused | "true" on lines in focusLines |
| data-unfocused | "true" on lines not in focusLines |
CSS variables #
:root {
--rcblk-bg: #f6f8fa;
--rcblk-header-bg: #eaeef2;
--rcblk-border: #d0d7de;
--rcblk-fg: #24292e;
--rcblk-line-number-fg: #8b949e;
--rcblk-line-highlight-bg: rgba(255, 213, 0, 0.15);
--rcblk-line-highlight-border: #d4a017;
--rcblk-diff-add-bg: rgba(22, 163, 74, 0.12);
--rcblk-diff-add-border: #16a34a;
--rcblk-diff-remove-bg: rgba(220, 38, 38, 0.12);
--rcblk-diff-remove-border: #dc2626;
--rcblk-copy-fg: #57606a;
--rcblk-copy-bg-hover: rgba(0, 0, 0, 0.06);
--rcblk-radius: 8px;
--rcblk-font-size-sm: 0.75rem;
--rcblk-font-size-md: 0.8125rem;
--rcblk-font-size-lg: 0.9rem;
}