Icons
The generated icon components in @v4/tokens/icons, how to use one in a component or slot, and the directional opt-in for RTL.
The icon system
The library ships 129 generated icon components (icon-*.tsx) in
packages/tokens/src/icons/, exported from the @v4/tokens/icons subpath. They are the v4
successor to token-builder 1.x's fetchFigmaSVGs + SVGR pipeline, generated by the
@v4/token-parser CLI — no SVGR dependency — from three Figma files: Ghost – Core 2.0
(67J5FrDZn5HzYzf5hJgoNA), ELB – Web – Core (HIjezWNeRQSBGkxx8VqmjA) and ELB – Web –
Content (9iz5GeR9syO68cmPr1ppE1).
What a generated icon looks like
Each icon is a small React function component: an inline <svg> on a 16px grid, painted with
fill="currentColor", marked aria-hidden="true" and focusable="false". They take an
optional size (px) and className:
// packages/tokens/src/icons/icon-arrow-right.tsx — generated, do not edit
export function IconArrowRight({ size = 16, className }: IconProps = {}): ReactNode {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
focusable="false"
className={className}
>
<path d="M12 4L10.59 5.41L16.17 11H4V13H16.17L10.59 18.59L12 20L20 12L12 4Z" />
</svg>
);
}
// the shared prop type — packages/tokens/src/icons/icon.types.ts
export interface IconProps {
/** Rendered square size in px (defaults to the 16px icon grid). */
size?: number;
className?: string;
}
Because they paint with currentColor, an icon takes the text colour of its context — so
a colour-set surface or a color on a wrapper flows straight into the glyph. You do not pass
a colour prop.
Using an icon in a component or slot
Import the named component from @v4/tokens/icons and render it. It carries its own colour
from currentColor, so the call site controls colour through the surrounding text colour:
import { IconChevronRight } from "@v4/tokens/icons";
<span className={styles.iconGlyph}>
<IconChevronRight />
</span>;
The barrel exports every icon with a Icon-prefixed PascalCase name (IconArrowRight,
IconChevronDown, IconCompetitionEuroleague, …), matching its icon-<kebab>.tsx file. The
set includes core UI glyphs (arrows, chevrons, close, filter, download, bookmark), media
controls, social-media marks (with -plain variants from the ELB files), the three
competition marks (IconCompetitionEuroleague, IconCompetitionEurocup,
IconCompetitionNextgen), and the IconFootball* glyphs that come with Ghost's icon set,
which nothing on this site uses.
Icons are real components — when a Figma slot shows an icon, compose the real one; never
hand-roll inline SVG demo shapes for it. Storybook slot demos and mocks must import the real
icon from @v4/tokens/icons too.
How icons are generated and synced
The icons are generated from Figma by the token parser's icons command, wired as the
sync-icons script in @v4/tokens:
# packages/tokens/package.json — six chained invocations (abridged)
v4-tokens icons -k 67J5FrDZn5HzYzf5hJgoNA -o src/icons # Ghost – Core 2.0: every icon
v4-tokens icons -k HIjezWNeRQSBGkxx8VqmjA -o src/icons # ELB – Web – Core: every icon
v4-tokens icons -k HIjezWNeRQSBGkxx8VqmjA -o src/icons --nodes "…" # the six social "-Plain" marks
v4-tokens icons -k 9iz5GeR9syO68cmPr1ppE1 -o src/icons --nodes "…" # Feature, Share-Plain
v4-tokens icons -k 67J5FrDZn5HzYzf5hJgoNA -o src/icons --nodes "…" # Chevron-Down, Chevron-Up
v4-tokens icons -k 9iz5GeR9syO68cmPr1ppE1 -o src/icons --nodes "…" # the three competition marks
Later invocations write into the same directory, so an ELB glyph replaces the Ghost glyph of
the same name. --nodes takes Name@nodeId pairs for icons that live inside a component
instance rather than on the icons page.
Run it with:
pnpm --filter @v4/tokens sync-icons # requires FIGMA_PAT in the environment
Each invocation discovers the icon components in its Figma file (or takes the named
--nodes), exports each SVG, and regenerates both the icon-<kebab>.tsx
components and the index.ts barrel. The name mapping is mechanical: Figma
Icon-Core/Arrow right → IconArrowRight / icon-arrow-right. Every generated file carries
a // Generated by `v4-tokens icons` from Figma "<node name>" — do not edit by hand. banner: to add, remove or change an
icon, change it in Figma and re-sync — never hand-edit the output.
The underlying CLI (v4-tokens icons -k <figma-file-key> -o <out-dir> [--page "- Icons"] [--filter <substr>] [--nodes "<Name@nodeId>,…"]) is documented in @v4/token-parser; the
sync-icons script pins the file keys, nodes and output directory so a refresh is a single
command.
Directional icons
The library uses CSS logical properties throughout, so layout, spacing and scroll logic all
mirror for free under dir="rtl". Glyphs do not. An arrow or chevron drawn pointing right
keeps pointing right on an RTL page, so a "next" paddle, a "back" button or a drill-in arrow
would point the LTR way. directionalIcon is the one house mechanism that fixes that.
The mechanism
directionalIcon is a vanilla-extract class exported from @v4/ghost-core. The whole rule is
a single reflection, keyed on the element's own resolved directionality:
// packages/ghost-core/src/utils/directional-icon.css.ts — the whole rule
export const directionalIcon = style({
selectors: { "&:dir(rtl)": { transform: "scaleX(-1)" } },
});
:dir(rtl) is deliberate rather than an [dir="rtl"] & ancestor selector: it reads the
element's own resolved direction, so a dir="ltr" island nested inside an RTL page
correctly does not flip. It behaves identically in Chromium, Firefox and WebKit, on both
HTML elements and <svg>.
Applying it
Apply the class at the call site, on the element that is (or wraps) the glyph, together with
data-directional="true":
import { IconChevronLeft, IconChevronRight } from "@v4/tokens/icons";
import { directionalIcon } from "../../utils/directional-icon.css"; // in-package
// consumers outside the package: import { directionalIcon } from "@v4/ghost-core";
<span className={classnames(styles.iconGlyph, directionalIcon)} data-directional="true">
{direction === "next" ? <IconChevronRight /> : <IconChevronLeft />}
</span>;
data-directional="true" makes the decision visible in the DOM for inspection and
assertions — the mirroring is a declared decision made at the call site, following the
lgRootWidth / lgOrientation precedent, not something inferred from the icon or the element
type.
When to use it — and when not
The test is: does this glyph MEAN inline-start / inline-end? — not "does it look asymmetric?".
- Mirror — only glyphs whose meaning IS an inline-axis direction:
IconArrowLeft/IconArrowRight/IconChevronLeft/IconChevronRightused to express navigation (next/back paddles, drill-in arrows, pagination). - Do not mirror — block-axis glyphs (
IconArrowUp,IconChevronDown— back-to-top, disclosure chevrons) and every semantic glyph (logos, play triangles, ticks, search glass). Flipping them is meaningless or actively wrong.
There is deliberately no blanket [dir="rtl"] svg { transform: scaleX(-1) }, and there
must never be one — it would flip logos, play triangles, ticks and search glasses. The opt-in
is per icon slot.
Rules and gotchas
- Put it on a symmetric box. Flipping a wrapper also flips its padding, radius and border, so apply it to the glyph itself or to a wrapper that is symmetric on the inline axis (a centred square icon chip is safe).
- Do not stack it on an element that already has a
transform(e.g. a chevron rotated 180° when a disclosure is closed) — the later declaration wins outright. - Physical-by-design exceptions must be documented and tested.
image-with-overlay's gradient is deliberately physical because its underlying Token Studio value is a physical 270° ramp; it carries a comment plus an e2e asserting it does not flip.
Guard rails
Two tests keep the mechanism honest:
packages/ghost-core/src/utils/directional-icon.test.tsx asserts the declaration is present
or absent (jsdom has no layout engine), and apps/e2e/tests/ghost-core/directional-icon.e2e.ts asserts
the computed matrix(-1, 0, 0, 1, 0, 0) at dir="rtl" and none at dir="ltr" across every
browser project.
A note on RTL in Ghost 2.0
Ghost 2.0 has no RTL artboards — every component set is authored LTR-only. The token layer
anticipates RTL (a complete P.RTL.* typography ramp and fontFamilies.rtl ship in
tokens.json), but mirroring behaviour is an engineering decision made against the
WAI/W3C i18n guidance on directional icons, not a design hand-off. Revisit when design ships
RTL artboards.