Fabric.js Document Engine

Search documentation

Find a page or section

中文

Vertical text

View as Markdown

Editable Fabric.js vertical text, written top to bottom in columns from right to left.

Fabric.js draws text in rows only. Fabric.js vertical text has been an open request since 2013 (fabric.js #511), and HTML canvas vertical text has no built-in writing mode either. VerticalText adds editable vertical text for Chinese, Japanese and Korean.

Try it

Loading the live demo. Its code is below.

vertical-text-demo.tsx
"use client";
 
import {
  type CombineUpright,
  type TextOrientation,
  VerticalText,
} from "fabricjs-document-engine/text";
import {
  DownloadIcon,
  PencilIcon,
  RotateCwIcon,
  SigmaIcon,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
 
import { DemoCanvas, DemoFrame, Status, Toolbar, ToolButton } from "./demo-ui";
import { PAGE, useFabricCanvas } from "./use-fabric-canvas";
 
const TEXT = "縦書きの文章、ABCも読める。\n令和8年10月2日\n한국어 세로쓰기";
 
export default function VerticalTextDemo() {
  const t = useTranslations("demos");
  const { elementRef, canvas } = useFabricCanvas();
  const textRef = useRef<VerticalText | null>(null);
  const [orientation, setOrientation] = useState<TextOrientation>("mixed");
  const [combine, setCombine] = useState<CombineUpright>("digits2");
  const [message, setMessage] = useState("");
 
  useEffect(() => {
    if (!canvas) return;
    const text = new VerticalText(TEXT, {
      left: PAGE.width - 40,
      top: 20,
      originX: "right",
      originY: "top",
      fontSize: 24,
      lineHeight: 1.3,
      fill: "#0d0d0d",
      combineUpright: "digits2",
      styles: {
        0: {
          0: { fill: "#6f00ff" },
          1: { fill: "#6f00ff" },
          2: { fill: "#6f00ff" },
        },
      },
    });
    textRef.current = text;
    canvas.add(text);
    canvas.setActiveObject(text);
    canvas.requestRenderAll();
    return () => {
      canvas.remove(text);
    };
  }, [canvas]);
 
  useEffect(() => {
    const text = textRef.current;
    if (!canvas || !text) return;
    text.set({ textOrientation: orientation, combineUpright: combine });
    text.setCoords();
    canvas.requestRenderAll();
  }, [canvas, orientation, combine]);
 
  function edit() {
    const text = textRef.current;
    if (!canvas || !text) return;
    canvas.setActiveObject(text);
    text.enterEditing();
    text.selectAll();
    setMessage(t("vertical.editing"));
  }
 
  function downloadSvg() {
    if (!canvas) return;
    const url = URL.createObjectURL(
      new Blob([canvas.toSVG()], { type: "image/svg+xml" })
    );
    const link = document.createElement("a");
    link.href = url;
    link.download = "vertical-text.svg";
    link.click();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
    setMessage(t("vertical.downloaded"));
  }
 
  return (
    <DemoFrame>
      <DemoCanvas elementRef={elementRef} label={t("canvas")} />
 
      <Toolbar>
        <ToolButton
          icon={PencilIcon}
          label={t("vertical.edit")}
          primary
          disabled={!canvas}
          onClick={edit}
        />
        <ToolButton
          icon={RotateCwIcon}
          label={
            orientation === "mixed"
              ? t("vertical.mixed")
              : t("vertical.upright")
          }
          onClick={() =>
            setOrientation((value) => (value === "mixed" ? "upright" : "mixed"))
          }
        />
        <ToolButton
          icon={SigmaIcon}
          label={
            combine === "digits2"
              ? t("vertical.combined")
              : t("vertical.separate")
          }
          onClick={() =>
            setCombine((value) => (value === "digits2" ? "none" : "digits2"))
          }
        />
        <ToolButton
          icon={DownloadIcon}
          label={t("vertical.svg")}
          disabled={!canvas}
          onClick={downloadSvg}
        />
      </Toolbar>
 
      <Status tone={message ? "ok" : "idle"}>
        {message || t("vertical.hint")}
      </Status>
    </DemoFrame>
  );
}

Click Edit and type, or use an input method editor. The arrow keys move along the column and to the next one.

Add vertical text

import { VerticalText, textObjects } from "fabricjs-document-engine/text";
 
const engine = createDocumentEngine({ canvas, customObjects: textObjects });
 
canvas.add(
  new VerticalText("縦書きの文章、ABCも読める。\n令和8年10月2日", {
    left: 400,
    top: 20,
    fontSize: 28,
    combineUpright: "digits2",
  })
);
  • Each line becomes a column. Columns read top to bottom and are placed from right to left.
  • Chinese, Japanese and Korean characters stand upright, each in its own square.
  • Latin words, digits and brackets turn 90° and keep their own spacing, as in printed Japanese books.
  • The ideographic comma and full stop sit in the top-right corner of their square.
  • combineUpright: "digits2" sets one or two digits side by side in one square, the tate-chū-yoko style used for dates.
  • textOrientation: "upright" stands every character up, Latin letters too.

Which characters stand and which turn follows Unicode's table for vertical text (UAX #50). Styles, colors, underlines and text backgrounds work as in any IText. An underline runs on the right of the column.

Edit vertical text

KeyMoves the cursor
↓ and ↑To the next and previous character in the column
← and →To the next and previous column

The cursor is a horizontal bar, selections cover whole column segments, and clicks find the character under the pointer. Input methods for Chinese and Japanese work as in a normal text box.

Export

const { blob } = await engine.export({ format: "svg" });

SVG export draws each upright character and each turned run with its own position, instead of writing-mode, so the file looks the same in browsers, Illustrator and PDF. PDF export uses the same drawing; pass a font file in fonts for Chinese or Japanese text in PDF.

The options are listed in the Text API.