npm package
tate-chu-yoko
A small library that automatically wraps half-width alphanumerics inside vertical Japanese text so CSS `text-combine-upright` can compose them as tate-chu-yoko (縦中横). Authors write plain text; typesetting is handled for them.
第1章 2026年4月、Webの縦書きは進化した。
What it solves
Japanese vertical writing (writing-mode: vertical-rl) leaves half-width Latin letters and digits lying on their side. The fix — text-combine-upright: all — exists, but the developer still has to decide *which* runs to apply it to, usually by wrapping them in <span class="tcy"> by hand. That chore leaks into manuscripts, CMSes, and Markdown. tate-chu-yoko does the wrapping for you, so authors can keep writing plain text.
Three packages
Install only the adapter you need. The core tokenizer is pulled in automatically as a dependency.
@love-rox/tcy-core
Framework-agnostic tokenizer
Scans a string and returns a structured token stream identifying the runs that should be uprighted. Use it directly in a custom pipeline or build-time transform.
@love-rox/tcy-react
React component
A <Tcy> component that walks children recursively and wraps eligible runs in <span class="tcy">. Works with React 17+ and produces deterministic output on the server and the client.
@love-rox/tcy-vue
Vue 3 component
The same <Tcy> API for Vue 3 apps. Identical options and behavior as the React version.
Install
Pick the adapter (React or Vue) for your stack — @love-rox/tcy-core will be installed as a dependency.
bun
bun add @love-rox/tcy-reactpnpm
pnpm add @love-rox/tcy-reactnpm
npm i @love-rox/tcy-reactyarn
yarn add @love-rox/tcy-reactUsage
Wrap the vertical content with <Tcy> inside an element that has writing-mode: vertical-rl.
import { Tcy } from "@love-rox/tcy-react";
export function Chapter() {
return (
<p style={{ writingMode: "vertical-rl" }}>
<Tcy>第1章 2026年4月、Webの縦書きは進化した。</Tcy>
</p>
);
}<script setup lang="ts">
import { Tcy } from "@love-rox/tcy-vue";
</script>
<template>
<p style="writing-mode: vertical-rl">
<Tcy>第1章 2026年4月、Webの縦書きは進化した。</Tcy>
</p>
</template>import { tokenize } from "@love-rox/tcy-core";
tokenize("第1章 2026年4月");
// [
// { type: "text", value: "第" },
// { type: "tcy", value: "1" },
// { type: "text", value: "章 " },
// { type: "tcy", value: "2026" },
// { type: "text", value: "年" },
// { type: "tcy", value: "4" },
// { type: "text", value: "月" },
// ]Recommended CSS
Apply the following to the generated <span class="tcy"> elements.
.tcy {
-webkit-text-combine: horizontal;
text-combine-upright: all;
}Options
These options are shared by the React / Vue <Tcy> components and by tokenize().
Shared options
| Option | Type | Default | Description |
|---|---|---|---|
| target | 'alphanumeric' | 'alpha' | 'digit' | 'ascii' | RegExp | 'alphanumeric' | What counts as a tate-chu-yoko target. alphanumeric matches [0-9A-Za-z]; ascii covers printable ASCII. A custom RegExp is accepted. |
| combine | boolean | true | Merge consecutive target characters into a single span. Set to false to wrap each character individually. |
| include | string | string[] | undefined | Extra characters to treat as targets regardless of target. |
| exclude | string | string[] | undefined | Characters to exclude. Takes precedence over include. |
Component-only props
| Option | Type | Default | Description |
|---|---|---|---|
| className | string | 'tcy' | Class applied to each generated span. |
| as | keyof JSX.IntrinsicElements | 'span' | Tag name used for wrapping. |
Behavior notes
- Runs are not joined across element boundaries —
<em>12</em>34produces two separate spans. - Full-width alphanumerics (
A–Z/0–9) are left alone because they already render upright in vertical text. - Both React and Vue adapters are SSR-safe: the DOM emitted on the server matches the client.
Browser support
writing-mode: vertical-rl and text-combine-upright: all are supported in all modern browsers. For Safari it's still worth including -webkit-text-combine: horizontal alongside the standard property.
License
MIT License. Free to use in personal and commercial projects.
See it in action
The interactive demo lets you tweak options and compare the raw text with the <Tcy> output side by side.