R-005 Video Template complex

How to create a typing code reveal タイピングコードリビールの作り方

Remotion video template: code types out character by character in a VS Code-style editor with syntax highlighting and a blinking cursor. 1920×1080 / 30fps / 12s. コードが1文字ずつタイプされながら現れるコードエディター風シーンの Remotion 動画テンプレ。シンタックスハイライト・ブリンクカーソル付き。1920×1080 / 30fps / 12秒。

ライブデモ Live Demo

StatCard.tsx

概要・用途・特徴Overview, Usage & Features

何ができるかWhat it does

Remotion video template: code types out character by character in a VS Code-style editor with syntax highlighting and a blinking cursor. 1920×1080 / 30fps / 12s.

コードが1文字ずつタイプされながら現れるコードエディター風シーンの Remotion 動画テンプレ。シンタックスハイライト・ブリンクカーソル付き。1920×1080 / 30fps / 12秒。

どこで使うかWhere to use

social media content, product demo, marketing video

コードチュートリアル動画、開発者向けコンテンツ、技術プレゼンテーション、LinkedIn投稿

特徴Key features

Remotion video template that types out code character by character with a blinking cursor. Syntax highlighting preserved during animation. Configurable typing speed. Dark editor theme. 1920×1080 / 30fps.

点滅カーソルで文字ごとにコードをタイプするRemotionビデオテンプレート。アニメーション中にシンタックスハイライトを保持。設定可能なタイピング速度。ダークエディタテーマ。1920×1080・30fps。

調整可能パラメータ Adjustable Parameters

Parameter Default Description

実装コード Implementation Code

import React from 'react';
import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from 'remotion';
import { z } from 'zod';
import '../styles.css';

// ─── Default code ────────────────────────────────────────────────────────────
const DEFAULT_CODE = `import React, { useState } from 'react';

interface CardProps {
  title: string;
  count: number;
}

export const StatCard: React.FC<CardProps> = ({
  title,
  count,
}) => {
  const [active, setActive] = useState(false);
  const cls = active ? 'card card--on' : 'card';

  return (
    <div className={cls} onClick={() => setActive(!active)}>
      <h2>{title}</h2>
      <span className="count">{count}</span>
    </div>
  );
};`;

// ─── Schema ──────────────────────────────────────────────────────────────────
const themeSchema = z.object({
  bg:       z.string().default('#0d1117'),
  editorBg: z.string().default('#161b22'),
  tabBg:    z.string().default('#1c2128'),
  keyword:  z.string().default('#ff7b72'),
  string:   z.string().default('#a5d6ff'),
  comment:  z.string().default('#8b949e'),
  type:     z.string().default('#ffa657'),
  jsx:      z.string().default('#7ee787'),
  plain:    z.string().default('#e6edf3'),
  lineNum:  z.string().default('#30363d'),
  cursor:   z.string().default('#f78166'),
});

export const typingCodeRevealSchema = z.object({
  filename:    z.string().default('StatCard.tsx'),
  code:        z.string().default(DEFAULT_CODE),
  typingStart: z.number().int().default(30),
  typingEnd:   z.number().int().default(330),
  theme:       themeSchema.default({}),
});
type Props = z.infer<typeof typingCodeRevealSchema>;

// ─── Tokenizer ───────────────────────────────────────────────────────────────
type TType = 'keyword' | 'string' | 'comment' | 'type' | 'jsx' | 'plain';
interface Token { type: TType; text: string }

const KEYWORDS = new Set([
  'import', 'export', 'const', 'let', 'var', 'from',
  'interface', 'return', 'type', 'default', 'function',
  'async', 'await', 'if', 'else', 'true', 'false',
  'null', 'undefined', 'React', 'FC', 'useState',
]);

function tokenize(code: string): Token[] {
  const out: Token[] = [];
  let i = 0;
  while (i < code.length) {
    // Line comment
    if (code[i] === '/' && code[i + 1] === '/') {
      let j = i + 2;
      while (j < code.length && code[j] !== '\n') j++;
      out.push({ type: 'comment', text: code.slice(i, j) });
      i = j; continue;
    }
    // String (single / double quote)
    if (code[i] === "'" || code[i] === '"' || code[i] === '`') {
      const q = code[i];
      let j = i + 1;
      while (j < code.length && code[j] !== q && code[j] !== '\n') {
        if (code[j] === '\\') j++;
        j++;
      }
      if (j < code.length && code[j] === q) j++;
      out.push({ type: 'string', text: code.slice(i, j) });
      i = j; continue;
    }
    // JSX tag
    if (code[i] === '<' && i + 1 < code.length && /[a-zA-Z/]/.test(code[i + 1])) {
      let j = i + 1;
      while (j < code.length && code[j] !== '>' && code[j] !== '\n') j++;
      if (j < code.length && code[j] === '>') j++;
      out.push({ type: 'jsx', text: code.slice(i, j) });
      i = j; continue;
    }
    // Identifier / keyword / type
    if (/[a-zA-Z_$]/.test(code[i])) {
      let j = i + 1;
      while (j < code.length && /[a-zA-Z0-9_$]/.test(code[j])) j++;
      const word = code.slice(i, j);
      out.push({
        type: KEYWORDS.has(word) ? 'keyword' : /^[A-Z]/.test(word) ? 'type' : 'plain',
        text: word,
      });
      i = j; continue;
    }
    out.push({ type: 'plain', text: code[i] });
    i++;
  }
  return out;
}

function revealTokens(tokens: Token[], n: number): Token[] {
  let rem = n;
  const out: Token[] = [];
  for (const tok of tokens) {
    if (rem <= 0) break;
    if (rem >= tok.text.length) { out.push(tok); rem -= tok.text.length; }
    else { out.push({ type: tok.type, text: tok.text.slice(0, rem) }); rem = 0; }
  }
  return out;
}

// ─── Component ───────────────────────────────────────────────────────────────
export const TypingCodeReveal: React.FC<Props> = (props) => {
  const {
    filename    = 'StatCard.tsx',
    code        = DEFAULT_CODE,
    typingStart = 30,
    typingEnd   = 330,
    theme,
  } = props;

  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Editor entrance spring
  const entranceSpring = spring({ frame, fps, config: { damping: 20, stiffness: 80 } });
  const editorY        = interpolate(entranceSpring, [0, 1], [80, 0]);
  const editorOpacity  = interpolate(entranceSpring, [0, 1], [0, 1]);

  // Typing progress
  const progress    = interpolate(frame, [typingStart, typingEnd], [0, 1], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
  });
  const charsToShow = Math.floor(progress * code.length);

  // Cursor blink after typing
  const typingDone    = frame >= typingEnd;
  const cursorVisible = typingDone
    ? Math.floor(frame / (fps / 2)) % 2 === 0
    : true;

  // Tokenize and reveal
  const allTokens     = tokenize(code);
  const visibleTokens = revealTokens(allTokens, charsToShow);
  const visibleText   = visibleTokens.map(t => t.text).join('');
  const lines         = visibleText.split('\n');

  const C: Record<TType, string> = {
    keyword: theme?.keyword ?? '#ff7b72',
    string:  theme?.string  ?? '#a5d6ff',
    comment: theme?.comment ?? '#8b949e',
    type:    theme?.type    ?? '#ffa657',
    jsx:     theme?.jsx     ?? '#7ee787',
    plain:   theme?.plain   ?? '#e6edf3',
  };

  const renderedLines = lines.map((lineText, li) => {
    const lineToks = tokenize(lineText);
    const isLast   = li === lines.length - 1;
    return (
      <div key={li} style={{ display: 'flex', minHeight: 34, lineHeight: '34px' }}>
        <span style={{
          width: 64, textAlign: 'right', paddingRight: 20,
          color: theme?.lineNum ?? '#30363d',
          flexShrink: 0, userSelect: 'none',
        }}>
          {li + 1}
        </span>
        <span style={{ flex: 1, whiteSpace: 'pre' }}>
          {lineToks.map((t, ti) => (
            <span key={ti} style={{ color: C[t.type] }}>{t.text}</span>
          ))}
          {isLast && cursorVisible && (
            <span style={{
              display: 'inline-block', width: 3, height: 26,
              background: theme?.cursor ?? '#f78166',
              marginLeft: 1, verticalAlign: 'text-bottom',
            }} />
          )}
        </span>
      </div>
    );
  });

  // Post-typing glow
  const glowOpacity = interpolate(frame, [typingEnd + 10, typingEnd + 30], [0, 0.5], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
  });

  return (
    <AbsoluteFill style={{
      background: theme?.bg ?? '#0d1117',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      {/* Editor window */}
      <div style={{
        transform: `translateY(${editorY}px)`,
        opacity: editorOpacity,
        width: 1200,
        borderRadius: 14, overflow: 'hidden',
        boxShadow: '0 40px 100px rgba(0,0,0,0.7)',
        border: '1px solid #30363d',
      }}>
        {/* Title bar */}
        <div style={{
          background: theme?.tabBg ?? '#1c2128',
          padding: '14px 20px',
          display: 'flex', alignItems: 'center', gap: 10,
          borderBottom: `1px solid ${theme?.lineNum ?? '#30363d'}`,
        }}>
          {(['#ff5f57', '#febc2e', '#28c840'] as const).map((c, i) => (
            <div key={i} style={{ width: 14, height: 14, borderRadius: '50%', background: c }} />
          ))}
          <div style={{
            marginLeft: 16, background: theme?.editorBg ?? '#161b22',
            borderRadius: 6, padding: '5px 18px',
            fontSize: 22, color: theme?.plain ?? '#e6edf3',
            fontFamily: '"Fira Code", Consolas, monospace',
          }}>
            {filename}
          </div>
        </div>
        {/* Code body */}
        <div style={{
          background: theme?.editorBg ?? '#161b22',
          padding: '20px 20px 20px 0',
          fontSize: 22,
          fontFamily: '"Fira Code", Consolas, monospace',
          minHeight: 500,
        }}>
          {renderedLines}
        </div>
      </div>

      {/* Post-typing glow */}
      {glowOpacity > 0 && (
        <div style={{
          position: 'absolute', inset: 0, pointerEvents: 'none',
          background: 'radial-gradient(ellipse at center, rgba(127,86,217,0.2) 0%, transparent 70%)',
          opacity: glowOpacity,
        }} />
      )}
    </AbsoluteFill>
  );
};
/* Global reset for Remotion canvas */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/*
 * Font strategy
 * ──────────────────────────────────────────────────────────────────────────
 * Default: system-ui — no network dependency, safe for Lambda / CI rendering.
 *
 * To use a monospace font in Remotion Studio, uncomment the @import below.
 * For Lambda / serverless rendering, use @remotion/google-fonts or place
 * the font file in public/ and reference it via staticFile().
 *
 * @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;600&display=swap');
 */

仕組みとカスタマイズHow It Works & Customization

仕組みHow it works

The full code string is pre-processed through a syntax tokenizer. Remotion's interpolate() maps the current frame to a character index, and the visible portion of the token array is rendered up to that index. The blinking cursor is an absolutely-positioned span with a CSS opacity animation. Typing speed is controlled by the frames-per-character ratio.

完全なコード文字列がシンタックストークナイザーで前処理されます。Remotionのinterpolate()が現在のフレームを文字インデックスにマッピングし、そのインデックスまでのトークン配列の可視部分がレンダリングされます。点滅カーソルはCSS opacityアニメーションを持つ絶対配置されたspan。タイピング速度はフレーム/文字比率で制御されます。

カスタマイズ方法Customization

Change the code string to any language snippet. Adjust framesPerChar for faster or slower typing. Swap the syntax theme colors. Add a window chrome (macOS-style title bar) around the code block.

コード文字列を任意の言語スニペットに変更。framesPerCharを速いまたは遅いタイピングに調整。シンタックステーマカラーを交換。コードブロックの周りにウィンドウクロム(macOSスタイルのタイトルバー)を追加。

注意点Caveats

Long code snippets result in long videos — keep the snippet focused on the key concept to maintain viewer attention. Use a monospace font (JetBrains Mono, Fira Code) for authentic code rendering. Ensure font is bundled with the Remotion project to avoid fallback font rendering.

長いコードスニペットは長い動画になります — 視聴者の注意を維持するためにスニペットを重要なコンセプトに集中させてください。本格的なコードレンダリングのためにモノスペースフォント(JetBrains Mono、Fira Code)を使用してください。フォールバックフォントのレンダリングを避けるためにフォントをRemotionプロジェクトにバンドルしてください。

よくある質問 Frequently Asked Questions

How to customize the typing code reveal? Typing Code Revealをカスタマイズするには?

Modify the CSS custom properties and class styles defined in the code section. Key adjustable values include colors, sizes, durations, and spacing. See the Adjustable Parameters section for specific variables.

コードセクションで定義されているCSSカスタムプロパティとクラススタイルを変更してください。色、サイズ、時間、間隔が主な調整可能値です。具体的な変数は調整可能パラメータセクションを参照してください。

How to use the typing code reveal in React? ReactでTyping Code Revealを使うには?

Import the provided React component and its CSS file. The component accepts props for customization. Check the React code section for the full implementation and available props.

提供されているReactコンポーネントとCSSファイルをインポートしてください。コンポーネントのpropsでカスタマイズできます。完全な実装と利用可能なpropsはReactコードセクションを参照してください。

What are the performance implications of typing code reveal? Typing Code Revealのパフォーマンスへの影響は?

This implementation uses CSS transforms and opacity for animations, which are GPU-accelerated. It's lightweight and doesn't cause layout thrashing. Consider using prefers-reduced-motion for accessibility.

この実装はCSSのtransformとopacityを使用しており、GPUアクセラレーションされます。軽量でレイアウトスラッシングを引き起こしません。アクセシビリティのためにprefers-reduced-motionの使用を検討してください。

AIへの指示テンプレート AI Prompt Template

以下をAIに貼り付けると実装を依頼できます。 Paste the following into your AI assistant to request implementation.