ドキュメント
Tailwind
NSH のスタイルは Tailwind CSS 4 に従います。Rust では、Tailwind のユーティリティは同じ名前・同じ値を持つ型付きスタイルメソッドとして提供されます(.p_4() は p-4、.rounded_2xl() は rounded-2xl、ZINC.shade(900) は zinc-900)。TSX では、すべての要素が class でクラスをそのまま受け取り、Tailwind 4 のデフォルトテーマに対してネイティブに解決されます。以下の各例では、型付きメソッドを先に、クラスを後に示します。
import { mount, Scroll, signal, Text, View } from "nsh";
function App() {
const count = signal(0);
const busy = signal(false);
const tab = signal("Day");
const tabs = ["Day", "Week", "Month"];
const buttons = () => (
<View class="flex-row flex-wrap gap-2">
<View
onPress={() => count.update((n) => n + 1)}
class="rounded-lg bg-lime-300 px-4 py-2 transition-colors duration-150 motion-reduce:transition-none hover:bg-lime-200 active:bg-lime-400 focus-visible:outline-2 focus-visible:outline-sky-500"
>
<Text class="font-medium text-lime-950">Pressed {count()}×</Text>
</View>
<View
onPress={() => busy.set(!busy())}
class="rounded-lg border border-black/10 bg-white px-4 py-2 shadow-sm transition motion-reduce:transition-none hover:bg-zinc-50 active:scale-95 active:bg-zinc-100 dark:border-[#2c2e33] dark:bg-[#24262a] dark:hover:bg-[#2a2c31] dark:active:bg-[#2d2f34] focus-visible:outline-2 focus-visible:outline-sky-500"
>
<Text class="font-medium text-[#15161a] dark:text-[#eceef1]">{busy() ? "Enable" : "Disable"}</Text>
</View>
<View disabled={busy()} onPress={() => count.set(0)} class="rounded-lg bg-emerald-400 px-4 py-2 enabled:hover:bg-emerald-300 active:bg-emerald-500 disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-sky-500">
<Text class="font-medium text-emerald-950">Reset</Text>
</View>
</View>
);
const tabsRow = () => (
<View class="flex-row rounded-xl border border-black/5 bg-[#ecece7] p-1 dark:border-[#1f2124] dark:bg-[#0c0d0f]">
<Each of={tabs}>
{(t) => (
<View
onPress={() => tab.set(t())}
class="flex-1 items-center rounded-lg py-2 transition-colors motion-reduce:transition-none active:opacity-70 focus-visible:outline-2 focus-visible:outline-sky-500"
classList={{ "bg-white shadow-sm dark:bg-[#2a2c31]": tab() === t(), "hover:bg-black/5 dark:hover:bg-white/5": tab() !== t() }}
>
<Text class="text-sm font-medium" classList={{ "text-[#15161a] dark:text-[#eceef1]": tab() === t(), "text-[#565a61] dark:text-[#8d9198]": tab() !== t() }}>
{t()}
</Text>
</View>
)}
</Each>
</View>
);
const gradient = () => (
<View class="overflow-hidden rounded-2xl bg-linear-to-br from-lime-300 via-emerald-300 to-sky-400 p-5 shadow-xl shadow-lime-300/20">
<Text class="text-xl font-bold text-zinc-950">{tab()} view</Text>
<Text class="mt-1 text-sm text-zinc-950/70">bg-linear-to-br from-lime-300 via-emerald-300 to-sky-400</Text>
</View>
);
const grid = () => (
<View class="grid grid-cols-3 gap-2">
<View class="col-span-2 h-14 items-center justify-center rounded-lg bg-sky-950">
<Text class="text-xs text-sky-200">col-span-2</Text>
</View>
<View class="h-14 rounded-lg bg-sky-900" />
<View class="h-14 rounded-lg bg-sky-800" />
<View class="row-span-2 rounded-lg bg-sky-400" />
<View class="h-14 rounded-lg bg-sky-700" />
<View class="h-14 rounded-lg bg-sky-600" />
<View class="h-14 rounded-lg bg-sky-500" />
</View>
);
const list = () => (
<View class="overflow-hidden rounded-xl border border-black/10 bg-white shadow-sm dark:border-[#2c2e33] dark:bg-[#18191c]">
<Each of={["Inbox", "Drafts", "Sent", "Archive"]}>
{(item) => (
<View class="flex-row items-center justify-between border-t border-black/5 px-4 py-3 first:border-t-0 even:bg-[#fafaf7] active:bg-lime-50 dark:border-[#1f2124] dark:even:bg-[#141518] dark:active:bg-[#24262a]">
<Text class="text-[#15161a] dark:text-[#eceef1]">{item()}</Text>
<Text class="text-[#8c9097] dark:text-[#5a5e65]">›</Text>
</View>
)}
</Each>
</View>
);
const motion = () => (
<View class="flex-row items-center justify-around py-2">
<View class="size-8 animate-spin motion-reduce:animate-none rounded-md bg-lime-300" />
<View class="size-8 animate-bounce motion-reduce:animate-none rounded-full bg-sky-400" />
<View class="h-8 w-14 animate-pulse motion-reduce:animate-none rounded-md bg-[#e1e1db] dark:bg-[#2c2e33]" />
</View>
);
return (
<Scroll class="flex-1 bg-[#f3f3ef] dark:bg-[#101113]">
<View class="gap-5 p-5 pt-6">
<Text class="text-2xl font-bold tracking-tight text-[#15161a] dark:text-[#eceef1]">Tailwind classes</Text>
{/* Two columns from `md:` on (768 points): breakpoints follow the window. */}
<View class="gap-5 md:flex-row md:items-start">
<View class="gap-5 md:min-w-0 md:flex-1">
{buttons()}
{tabsRow()}
{gradient()}
</View>
<View class="gap-5 md:min-w-0 md:flex-1">
{grid()}
{list()}
{motion()}
</View>
</View>
</View>
</Scroll>
);
}
mount(App);use nsh_core::element::palette::ZINC;
view()
.flex_row()
.items_center()
.gap_3()
.rounded_2xl()
.bg(AdaptiveColor::new(white(), ZINC.shade(800)))
.p_4()
.shadow_lg()
.active(|s| s.scale(0.98))
.child(
text("Tailwind on native views")
.text_base()
.font_weight(FontWeight::SEMIBOLD)
.text_color(AdaptiveColor::new(ZINC.shade(900), white())),
)<View class="flex-row items-center gap-3 rounded-2xl bg-white p-4 shadow-lg active:scale-[0.98] dark:bg-zinc-800">
<Text class="text-base font-semibold text-zinc-900 dark:text-white">Tailwind on native views</Text>
</View>型付きメソッドは、クラスがコンパイルされるのと同じスタイルルールに変換され、パースすべき文字列はありません。クラスの場合、CSS は生成されず、JavaScript では何も実行されません。文字列はコアに渡され、コアは異なるクラス文字列ごとに一度だけパースし(結果はキャッシュされます)、スタイルルールに変換して適用します。状態(押下、ダークモード、ウィンドウ幅)に依存するバリアントは、その状態が変化したときにコアが再適用します。押下状態とフォーカス状態はネイティブに追跡されるため、active: のフィードバック(Rust では .active(..))はコードへの往復を必要としません。
クラスと style prop は同じネイティブスタイルを生成し、組み合わせて使用できます。このページでは、クラスとそれに対応する型付きメソッドに固有の内容を扱います。Tailwind リファレンスには、サポートされているすべてのユーティリティが載っています。
#class and classList
view()
.rounded_lg()
.px_4()
.py_2()
.transition_colors()
.bg(move || if selected.get() { INDIGO.shade(600) } else { ZINC.shade(200) })
.style_when(busy, |s| s.opacity(0.5))<View
class="rounded-lg px-4 py-2 transition-colors"
classList={{ "bg-indigo-600": selected(), "bg-zinc-200": !selected(), "opacity-50": busy() }}
/>classはクラスの文字列です。動的にもできます。クラスは実行時に解決されるため、class={`h-6 rounded bg-${color()}-500`}のような指定も動作します。classListはクラスを条件に対応付けます。値が truthy の間、そのクラスが適用されます。適用はclassの後に行われます。- Rust では、クロージャまたはシグナルを渡されたメソッドがそれに追従します(
.bg(move || ..))。.style_when(condition, |s| ..)は条件が成り立つ間スタイルを適用し、.when(condition, |v| ..)は要素の生成時に一度だけ決定します。
競合の解決方法:
- 後のクラスが優先されます。 同じ文字列内では、前のクラスより後のクラスが勝ちます。
"p-2 p-4"はp-4になります。tailwind-mergeは不要です。コンポーネントは、呼び出し側から渡されたclassを末尾に追加できます。Rust でも後のメソッドが優先されます。 - バリアント付きクラスは、通常のクラスの後に適用されます。 Tailwind のバリアント順序(
hover:はactive:より前、max-*:は広いものから、ブレークポイントは昇順、dark:は最後)に従い、文字列内での位置は関係ありません。Tailwind と同様に、クラスの最上位のバリアントが先に判定され、次にその次の位のバリアントが判定され、バリアントが 1 つ多いクラスが後になります。dark:hover:bg-a dark:bg-bはダークモードでホバーされている間はbg-aになり、max-md:p-2 max-lg:p-4はmd未満ではp-2になります。CSS と同様に、odd:bg-a last:bg-bは奇数番目の最後の行にbg-aを与えます。Tailwind がoddをlastの後にソートするためです。 styleはクラスより優先されます。 スタイルキーは、その名前の CSS プロパティ(クラスが設定するもの)に対応するため、そのプロパティだけを置き換えます。style={{ paddingLeft: 8 }}はpl-4を置き換え、pr-4は維持します。!はクラスを important にします。!p-0。
Rust コンポーネント(nsh/base のものと、アプリ自身のクレートのもの)も、TSX から class と classList を受け取れます。渡されたクラスはコンポーネント自身のスタイルの後にマージされ、同じ優先度では渡されたクラスが優先されます。
#バリアント
| バリアント | 適用される条件 | Rust |
|---|---|---|
active: | ビューが押されている | .active(|s| ..) |
hover: | ポインターがビューの上にある(iPad と Mac のポインター) | .hover(|s| ..) |
focus:, focus-visible: | 入力欄がフォーカスを持っている | .focus(|s| ..) |
focus-within: | ビューまたはその子孫がフォーカスを持っている(Overlay の内容は、それが置かれているビューの子孫です) | .focus_within(..) |
disabled:, enabled: | 要素の disabled prop が true / false | .disabled_style(|s| ..) |
selected: | 要素が選択されている(Button の selected, accessibilityState.selected) | .selected_style(..) |
checked: | 要素がチェックされている(accessibilityState.checked は true、"mixed" はチェックされていない) | .checked_style(..) |
indeterminate: | 要素が不確定状態である(accessibilityState.checked は "mixed") | .indeterminate_style(..) |
aria-expanded: | 要素が展開されている(accessibilityState.expanded は true)。Popover または Menu のトリガーで、そのポップアップが開いている間 | .expanded_style(..) |
aria-pressed: | 要素がオン状態のトグルボタンである(accessibilityState.pressed は true、"mixed" は押されていない)。active:(進行中の押下)ではありません | .aria_pressed(..), .pressed_style(..) |
aria-busy:, aria-checked:, aria-selected: | accessibilityState.busy、checked(true、"mixed" ではない)または selected が true。Tailwind の aria-* バリアントと同じ扱いで、疑似クラスの後に、名前順で適用されます | |
data-<name>: | 要素が data-* 属性 name を持っている。Tailwind の真偽値 data-* バリアント(&[data-range-start])と同じ扱いで、コンポーネントのパーツが状態以外に示すもの、たとえばカレンダーの範囲開始日(data-range-start:)に使います。aria-* バリアントの後に、名前順で適用されます | Interactive::project_data(name, ..) でコンポーネントのパーツに設定します |
group-active:, group-hover:, group-focus:, group-focus-within:, group-disabled:, group-selected:, group-checked:, group-indeterminate:, group-aria-expanded:, group-aria-pressed:, group-aria-busy:, group-aria-checked:, group-aria-selected:, group-data-<name>: | group クラスを持つ最も近い祖先がその状態である、またはその data-* 属性を持っている | 祖先側は .group()、内側は .group_active(..), .group_hover(..), .group_focus_within(..) |
group-active/name: … | 同じ内容で、group/name が付いた祖先が対象 | .group_named("name")、.group_active_named("name", ..), … |
first:, last:, odd:, even:, only: | 兄弟要素の中での要素自身の位置。行の位置に応じてラベルの色を変えるには、行に last:text-* を指定します。ラベルはそれを継承します | 行のインデックスからの通常の Rust コード(at.is_first(), at.is_last(), at.index() % 2) |
placeholder: | 入力欄のプレースホルダーをスタイル指定する(placeholder:text-zinc-400) | .placeholder_color(..) |
selection: | 入力欄や選択可能なテキスト、およびその要素内にあるものの選択ハイライトの色を指定する(selection:bg-violet-200) | .selection_color(..) |
dark: | システムの外観がダークである | AdaptiveColor、または .dark(|s| ..) |
sm:, md:, lg:, xl:, 2xl: | ウィンドウの幅が 640、768、1024、1280、1536 ポイント以上 | .style_when(|| window().width >= px(768.0), |s| ..) |
max-sm:, max-md:, … | ウィンドウの幅がブレークポイント未満 | 同じ指定で、< を使用 |
min-[600px]:, max-[600px]: | 任意のブレークポイント | 同じ |
portrait:, landscape: | ウィンドウの向き | window().width > window().height |
ios:, android:, web: | プラットフォーム | .when(os() == Os::Ios, |v| ..) |
rtl:, ltr: | レイアウト方向 | ビューの .direction(LayoutDirection::..) |
motion-safe:, motion-reduce: | ユーザーの視差効果を減らす設定 | reduce_motion() |
バリアントは組み合わせられます。dark:active:bg-zinc-700、md:landscape:flex-row。Rust では、.active(..) スタイル内の AdaptiveColor は dark:active: になります。
view()
.group()
.flex_row()
.items_center()
.gap_3()
.rounded_xl()
.bg(white())
.p_3()
.active(|s| s.bg(ZINC.shade(50)))
.child(text("Settings").flex_1().text_color(ZINC.shade(900)))
.child(
text("›")
.text_color(ZINC.shade(300))
.transition_transform()
.group_active(|s| s.translate_x(px(4.0))),
)<View class="group flex-row items-center gap-3 rounded-xl bg-white p-3 active:bg-zinc-50">
<Text class="flex-1 text-zinc-900">Settings</Text>
<Text class="text-zinc-300 transition-transform group-active:translate-x-1">›</Text>
</View>任意の要素の disabled(Rust では .disabled(..))は、disabled: クラス(.disabled_style(..))を適用し、マウントされたものから押下ハンドラーを取り除くため、押下フィードバックも表示されません。
view()
.disabled(saving)
.on_press(save)
.rounded_lg()
.bg(EMERALD.shade(500))
.px_4()
.py_2()
.active(|s| s.bg(EMERALD.shade(600)))
.disabled_style(|s| s.opacity(0.4))
.child(text("Save").font_weight(FontWeight::MEDIUM).text_color(white()))<View disabled={saving()} onPress={save} class="rounded-lg bg-emerald-500 px-4 py-2 active:bg-emerald-600 disabled:opacity-40">
<Text class="font-medium text-white">Save</Text>
</View>ブレークポイントはデバイスではなく、ウィンドウ(NSH が描画する面)と比較されます。iPad のスプリットビューやサイズ変更可能なウィンドウでは、window() と同様に、ウィンドウの幅に従います。
#Web 上の Tailwind との違い
- 単位はポイントです。
p-4(.p_4())は 16 ポイントです(スペーシング 1 ステップ = 4pt、1rem = 16pt)。pxの任意の値もポイントです。p-[13px](.p(px(13.0)))は 13pt です。 flexは行方向です。 CSS と同じく、class="flex"は方向クラスで別の指定がない限り子要素を行に並べます(方向クラスは文字列内のどこにあっても有効です。flex-col flexは列方向です)。スタイルのdisplay: "flex"と Rust の.flex()も同様に行方向で、display を設定しないビューは NSH のデフォルトである列方向のままです。blockとflow-rootはブロックレイアウトを使い、inlineとinline-blockも、ブラウザーが flex または grid の親の中でレイアウトするのと同様にブロックレイアウトを使います。Rust では、.flex_row()と.flex_col()が方向を設定します。- ビューに指定したテキストクラスは継承されます。
text-*、font-*、leading-*、tracking-*およびビューに指定したその他のテキストユーティリティ(Rust では.text_color(..)、.font_weight(..)など)は、CSS の継承と同様に、内側のテキスト要素と入力要素に引き継がれます。<View class="text-white">はラベルを白にします。 - すべてのビューは独立したボックスです。
overflow-x-hiddenとoverflow-y-hiddenは、overflow-hiddenと同様に両方の軸でクリップします(スクロールは scroll 要素の役割です)。invisibleはビューとその中のすべてを非表示にします。mix-blend-*はビューを背面に描画されているすべてのものとブレンドします。blockのボックスは、自身のマージンと子要素のマージンを分離したままにします。absoluteのビューは、CSS と同様に、最も近い位置指定された祖先の中に配置されます。fixedはビューをそのルートビューの中にレイアウトします(他の要素はabsoluteとして配置されます)。 - テキストは空白を保持します。 文字列内のスペースと改行は書かれたとおりに描画され、
whitespace-*が折り返しを設定します。JSX コンパイラーは、JSX と同様に、リテラルの JSX テキストの空白を畳み込みます。 - トランジションはプロパティグループをアニメーションします。
transitionは色、不透明度、シャドウ、変形をアニメーションし、transition-allはそれに加えて角の半径とボーダー幅もアニメーションします。フィルター、背景フィルター、グラデーション、レイアウトは新しい値に即座に切り替わります。 focus-visible:はfocus:です。 要素がフォーカスを持っている間は常に適用されます。- 状態の背景はグラデーションの上に表示されます。 CSS ではグラデーションが
background-colorを覆うため、bg-linear-*の面にactive:bg-*を指定しても表示されませんが、NSH ではactive:、hover:、focus:、group-*:による背景色は、その状態が続く間グラデーションを置き換えます(その状態がactive:from-*のように独自のグラデーションを設定している場合を除く)。activeStyleやその他の状態スタイル、アニメーションする背景色も同様です。 - テキストとタッチの設定はスタイルキーです。
line-clamp-*はnumberOfLinesを設定し、text-ellipsisとtext-clipはtextOverflowを、truncateは両方を、select-*はuserSelectを、pointer-events-*はpointerEventsを設定します。そのため、状態バリアントで機能し、すべてのクラスと同様にインラインのstyleに負けます。caret-*、placeholder:text-*、selection:bg-*は、同じ方法でcaretColor、placeholderColor、selectionColorスタイルを設定します。Rust ではこれらはStyledメソッドです(.number_of_lines(..)、.truncate()、.text_overflow(..)、.select_text()、.pointer_events(..)など)。whitespace-nowrapはクラスとしてテキストを 1 行に保ちます。object-*は画像と動画のobjectFitとobjectPositionスタイルを設定し、スタイルのキーは他のものと同様にこれらを置き換えます。 - サポート範囲: Tailwind 4 のユーティリティとバリアントのうち、ネイティブでの意味を持つもの。
peer-*バリアント、疑似要素(before:、after:)、任意のプロパティ([mask-type:luminance])は未知のクラスとして報告され、NSH にネイティブでの意味がないユーティリティもすべて同様です。これにより、クラスがブラウザーでの意味と異なる意味になることはありません。レンダリングヒント(antialiased、will-change-transform、appearance-none、box-borderなど)は受け付けられますが、何も変更しません。
それ以外はすべて Tailwind 4 のドキュメントどおりに動作します。カラーパレット、不透明度修飾子(bg-black/40)、任意の値(bg-[#34c759]、w-[72px]、grid-cols-[1fr_2fr]。p、px、py、pt、pr、pb、pl および m ユーティリティでは環境変数 pt-[env(safe-area-inset-top)]、pb-[calc(env(keyboard-inset-height)+49px)] も使用可能。Safe area と Keyboard を参照)、分数(w-1/2、translate-x-1/2)、シャドウとリング、グラデーション(bg-linear-to-r、bg-radial、bg-conic と from- / via- / to-)、フィルター、backdrop-blur-*、mix-blend-*、変形、transition-* と duration-*、animate-spin / ping / pulse / bounce、グリッドがあり、それぞれに Rust の型付きメソッドが対応しています。フィルターとブレンドのプラットフォーム上の制限は、クラスにも適用されます。
#テーマ
クラスは Tailwind 4 のデフォルトテーマに対して解決されます。テーマを拡張するには、nsh.json の "tailwind" に記述します。記述したエントリーはデフォルトに追加されます(同じ名前のデフォルトがある場合は置き換えられます)。
{
"tailwind": {
"colors": {
"brand": { "50": "#f5f3ff", "500": "#7c3aed", "600": "#6d28d9", "DEFAULT": "#7c3aed" },
"ink": "#1f2328"
},
"radius": { "card": 14 },
"fontFamily": { "display": "Pacifico" },
"breakpoints": { "tablet": 600 }
}
}
このテーマでは、bg-brand-500、text-brand(DEFAULT シェード)、border-ink、rounded-card、font-display、tablet:flex-row が有効なクラスになります。
| キー | 値 | 生成されるもの |
|---|---|---|
colors | CSS の色、またはシェードのオブジェクト(名前のみの場合は "DEFAULT") | bg-*、text-*、border-*、from-*、ring-* など |
spacing | スペーシング 1 ステップあたりのポイント数(デフォルトは 4) | p-*、m-*、gap-*、w-* など |
rem | 任意の値における rem あたりのポイント数(デフォルトは 16) | |
breakpoints | ポイント | name: バリアント |
container | ポイント | max-w-* サイズ |
fontSize | ポイント、または行の高さをサイズの倍数とする [size, lineHeight] | text-* |
fontWeight | 1~1000 | font-* |
fontFamily | フォントファミリー名(システムファミリー、またはインポートしたフォントの名前) | font-* |
tracking | em | tracking-* |
leading | フォントサイズの倍数 | leading-* |
radius | ポイント | rounded-* |
shadow, insetShadow, dropShadow | CSS の box-shadow | shadow-*、inset-shadow-*、drop-shadow-* |
blur | ポイント | blur-*、backdrop-blur-* |
perspective | ポイント | perspective-* |
aspect | 比率(1.5) | aspect-* |
ease | [x1, y1, x2, y2] | ease-* |
未知のキーや無効な値はビルドエラーになります(nsh.json: tailwind: unknown theme key 'colours')。テーマはバンドルにコンパイルされ、最初のフレームの前に適用されます。
Tailwind テーマが形作るのはクラスです。型付きメソッドは Tailwind のデフォルトスケールを維持するため、Rust コンポーネントはどのアプリでも同じ見た目になります。アプリ固有の見た目は、そのテーマのデザイントークン(nsh.json の "theme"、Rust)から取得します。
#テーマトークン
アプリのテーマ(nsh.json 内の "theme")には、デザイントークンが保持されます。shadcn/ui のカラー(background、foreground、primary、muted-foreground、border、ring、chart-1、sidebar、…)、radius、フォントの font-sans と font-mono、そしてアプリが追加する任意のトークンです。クラスでは、Tailwind の名前として CSS の @theme の変数を指定し、Rust コンポーネントは nsh_base の theme() で読み取り、TSX は nsh の token() で読み取ります。そのため、TSX の画面と Rust コンポーネントは同じテーマに従います。
use nsh_base::theme;
view()
.rounded(move || theme().radius)
.border_1()
.border_color(move || theme().border)
.bg(move || theme().card)
.p_4()
.child(text("Balance").text_color(move || theme().card_foreground))
.child(text("Updated just now").text_color(move || theme().muted_foreground))
.child(
view()
.rounded(move || theme().radius)
.bg(move || theme().primary)
.px_4()
.py_2()
.active(|s| s.bg(move || theme().primary.opacity(0.9)))
.child(text("Continue").text_color(move || theme().primary_foreground)),
)<View class="rounded-(--radius) border border-border bg-card p-4">
<Text class="text-card-foreground">Balance</Text>
<Text class="text-muted-foreground">Updated just now</Text>
<View class="rounded-(--radius) bg-primary px-4 py-2 active:bg-primary/90">
<Text class="text-primary-foreground">Continue</Text>
</View>
</View>トークンの名前は、テーマファイルに書かれているとおり、CSS 変数名から -- を除いたものです。たとえば muted-foreground、radius、chart-1 です。Rust では、shadcn のトークンは theme() のメソッド(primary()、muted_foreground()、radius()、font_mono())で、すべてのトークンは theme().color("up")、theme().number("radius-card")、theme().font("font-display") のいずれかです。
| 形式 | 受け取るもの | 例 |
|---|---|---|
| 任意のカラーユーティリティ内のカラー名 | テーマカラー(不透明度も指定可能) | bg-primary、text-muted-foreground/70、bg-up/30、border-t-border、ring-ring、from-accent、placeholder:text-muted-foreground |
(--name)(Tailwind 4 の変数ショートハンド) | テーマカラーまたは数値 | bg-(--primary)、rounded-(--radius)、p-(--gap-card) |
(color:--name)、(length:--name)、(number:--name)、(family-name:--name) | 同上。ユーティリティが複数の種類を受け取る場合に型を指定する | text-(color:--up)、text-(length:--text-title)、font-(family-name:--font-mono) |
font-sans、font-mono | テーマのフォント(テーマで設定している場合) | font-mono |
- 独自のトークン。 CSS の
@themeのすべての変数がそうであるように、テーマファイル内のすべての名前がトークンになります。"up": "#16a34a"からはbg-up、text-up、bg-up/30が作られ、"radius-card": "14px"からはrounded-(--radius-card)が、"font-display": "Pacifico"からはfont-(family-name:--font-display)が作られます。長さの単位はポイントです。(number:--name)は数値をそのまま渡します。 - フォント。
font-sansはアプリのフォントです。独自のフォントファミリーを指定していないすべてのテキストは、ページが Tailwind の--font-sansで描画されるのと同じように、このフォントで描画されます。font-monoはfont-monoとTextView内のコードに使われるフォントです。未設定の場合、どちらもシステムのフォントになります。 - 両方の外観。 トークンにはテーマのライトとダークの値があります。
bg-primary、theme().primary、token("primary")は、dark:クラスなしで外観に応じて自動的に切り替わります。テーマが片方の外観でのみ設定しているカラーは、両方の外観で使われます。 - Tailwind のカラーが優先されます。
"tailwind"のcolorsまたは Tailwind のパレットにある名前はそのカラーになります。したがって、Tailwind のテーマにprimaryというカラーがない場合に限り、bg-primaryはテーマトークンになります。 - テーマはアプリの実行中に変更できます。 ネイティブアプリに埋め込まれたアプリにそのアプリのテーマが渡されたとき、およびテーマが変更されるたびに、トークンを指定するクラス、
theme()を読み取る Rust バインディング、token()を読み取る TSX コードが更新されます。クラスとバインディングはネイティブに更新されます。埋め込み元アプリのテーマにないトークンは、アプリ自身の"theme"の値(それもなければデフォルトテーマの値)を保持します。ブランドのみを追加するホストは、テーマのオーバーライドを渡します。これは、ブランドで変更されるトークンを同じファイル形式で記述したものです(iOS、Android、デスクトップではAppView::set_theme_overrides)。 - ほかのクラスと同様に検査されます。 デフォルトテーマにもアプリの
"theme"にもないトークン名は、ビルド時に未知のクラスとして扱われます(unknown class 'bg-chart-up')。埋め込み元アプリのみが提供するトークンを使うには、アプリの"theme"にもその値を指定してください。その値は、アプリが単独で実行されるときに表示されます。
トークンを指定するクラスは、ほかのクラスと同様にテーマごとに一度だけ解決されます。描画のコストはパレットカラーと同等です。
#TSX での token()
nsh の token(name) は、現在表示されている外観におけるトークンの値です。カラーは #rrggbbaa、長さはポイント数、フォントはファミリー名として返されます。リアクティブであるため、コンポーネント、メモ、エフェクト内でこれを読み取るコードは、テーマや外観が変わると再実行されます。キャンバスやチャートなど、クラスを指定できない場所に渡す値に適しています。
import { Canvas, token } from "nsh";
<Canvas
style={{ height: 160 }}
draw={(cx, { width, height }) => {
cx.beginPath();
cx.moveTo(0, height);
cx.lineTo(width, 0);
cx.strokeStyle = token("chart-1");
cx.lineWidth = 2;
cx.stroke();
}}
/>
名前には型が付いています。NSH はアプリのトークンをテーマから .nsh/types/nsh/tokens.d.ts に書き込むため、テーマに "up" があれば token("up") は string になり、テーマにない名前は型エラーになります。
#クラスの検査
nsh build、nsh dev、nsh run、nsh test は、JSX 内の静的なクラス名(class 内の文字列リテラルと classList のキー)を、ランタイムが使うものと同じパーサーとテーマで検査し、存在しないクラスについて警告します。
warn nsh::build App.tsx:12:36: unknown class `bg-brand-550`
警告によってビルドが失敗することはありません。実行時に組み立てられるクラス(bg-${color()}-500)は、ランタイムが初めてそれを見たときに検査され、未知のクラスごとに一度だけログに記録されます(unknown classes: ...)。型付きメソッドは Rust コンパイラが検査します。
#クラスとスタイルの使い分け
どちらもネイティブに解決され、コストはほぼ同じです。クラスは記述が短く、コードなしでバリアント(押下状態、ダークモード、ブレークポイント)を使えます。インラインスタイルは JavaScript で計算された値を受け取り、ネイティブのアニメーション値(opacity: fade、translate: [x, 0])を受け付けます。ほとんどの TSX アプリでは、静的な見た目にはクラスを使い、計算される値やアニメーションする値には style を使います。Rust コンポーネントでは、どちらにも型付きメソッドを使い、アニメーション値は .bind_opacity(..)、.bind_translate_x(..) などのバインディングでバインドします。
#関連項目
- Styling: style prop、型付きメソッド、エフェクトのプラットフォーム上の制限。
- Tailwind reference: すべてのユーティリティとバリアント。
- Layout: フレックスボックスとグリッド。クラスとしても利用できます。