# Vrt Authoring

> Author dedicated Storybook visual regression stories for 2nd-gen components. Use when adding or reviewing `.vrt.ts` files, Chromatic VRT coverage, forced-colors coverage, pseudo-state snapshots, global stylesheet coverage, or custom-property VRT coverage.

- Skill: `adobe/vrt-authoring` (Agent Skill)
- Install (CLI): `npx skillmds@latest add adobe/vrt-authoring`
- Raw SKILL.md: https://api.skillmd.com/api/skills/adobe/vrt-authoring/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Adobe (https://skillmd.com/u/adobe)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/adobe/vrt-authoring

---


# VRT authoring

This skill is the quick reference. The authoritative guide is `CONTRIBUTOR-DOCS/02_style-guide/04_testing/04_visual-regresssion-testing.md` — when detail and this skill disagree, the guide wins, and new long-form guidance belongs there with a pointer here.

## Pattern

- Put VRT stories in `2nd-gen/packages/swc/components/<component>/test/vrt/*.vrt.ts` or `2nd-gen/packages/swc/patterns/<pattern>/test/vrt/*.vrt.ts`.
- Keep docs stories for examples; keep `.vrt.ts` stories for dense visual coverage.
- Aim for maximum meaningful coverage: include every size, variant, state, anatomy, theme, static-color, global-style, custom-property, and component-specific visual axis that can produce a useful visual difference. Cover CJK language rendering explicitly when text metrics can change, e.g. `lang="ja"` / `lang="ko"` / `lang="zh"` line-height, wrapping, or truncation. Skip only impossible, unsupported, or truly redundant combinations.
- Don't cover a visual axis your component only forwards to a slotted or composed child with no CSS of its own for it (e.g. a layout component passing `static-color` through to its children); that's already covered by the child's own VRT file. Only cover it here if this component's own CSS does something with that state.
- Use shared helpers from `.storybook/helpers`: `createPermutations`, `groupPermutationsBy`, `row`, `theme`, `staticColorBackground`, `forcePseudoStates` (and `forcePseudoState` for forcing state on individual slotted elements), `vrtParameters`, and `forcedColorsVrtParameters`.
- Keep unit files data-driven: local case lists and renderers only. Move reusable mechanics to `.storybook/helpers`.

## Grouping permutations into rows

A single dense row of every permutation is hard to scan in a Chromatic diff. Use `groupPermutationsBy(permutations, key)` to split one flat permutation list into one labeled `row()` per value of `key` (e.g. one row per variant), plus a `default` row for permutations that lack the key.

- **Choosing the grouping key is a per-component decision, not a fixed rule.** Group by the axis that carries the most meaning for that component:
  - Components with a `variant` axis (button, badge, action-button, status-light): group by `'variant'`.
  - Components whose primary axis is something else: group by that instead (e.g. `'size'`, `'direction'`).
  - Components with no natural grouping axis (divider, icon, avatar, typography): grouping by a missing key collapses everything into one `default` row, which adds no value. Prefer a single ungrouped `row()` in that case.
- Split forced pseudo-states (`data-force-state`) into their own per-state rows (`hover`, `focus-visible`, `active`) rather than folding them into the variant rows; filter them out of the main grouping first.
- **Every item in a row must be identifiable.** A single multi-item row is only fine when each item carries visible text that names it (a button label, link text, typography sample). When items render near-identical visuals with no visible label — small controls (color handle, color loupe), or fixed-content widgets (accordion, avatar states) — a generic `States` row tells a reviewer nothing about which item is which. Give each such state its own row labeled with the state name instead.
- Once the row heading conveys the grouping axis (variant/state), keep the component's own label plain (e.g. `Button`). Do not print the permutation's axis values into the component — the row heading and visual rendering already carry that.
- Label rows with the `swc-Detail` typography classes (`swc-Detail swc-Detail--sizeM`), not inline font styles. `staticColorBackground` stacks its rows vertically and overrides `--swc-detail-font-color: currentColor` so labels stay legible on the contrast backgrounds.
- For composed patterns, prefer deterministic realistic content over behavior demos: fixed prompts, sources, attachments, feedback states, and response text.
- Before skipping global styles, check `2nd-gen/packages/swc/stylesheets/global/` for a matching generated stylesheet such as `global-<component>.css` and cover its plain-class API when present.

## Story shape

- `<component>.vrt.ts`: permutations for size, variant, state, anatomy, static-color, wrapping, and truncation.
- `*-global-styles.vrt.ts`: plain global class coverage for `<a>` / `<button>` or equivalent elements.
- `*-custom-properties.vrt.ts`: one reference/override row per public custom property.

## Forced pseudo-states

`:hover` / `:focus-visible` / `:active` can't be triggered in a static Chromatic capture, so `forcePseudoStates` mirrors the component's own pseudo-state rules and applies a `data-forced-<state>` **attribute** (never a class — a class trips `:not([class])` default-style guards and drops default styling from the snapshot) from the story's `play`. Quick reference:

- Tag permutations with `data-force-state`; `forcePseudoStates('<tag>[data-force-state]', internalSelector?)` forces the host, or a shadow-internal element when `internalSelector` is given (e.g. Button's `.swc-Button`).
- Force only the states a component actually styles — a forced state with no matching rule just adds a snapshot that can never differ (e.g. Card has no `:active` rule, so it forces only `hover`/`focus-visible`).
- To force a state on a **slotted light-DOM child** the shared helper can't reach (e.g. `::slotted(a:hover)` on a linked title), write a small custom `force<Component>States` play function that also sets `data-forced-<state>` on that element.
- Forced-colors mode gets its own story (`forcedColorsVrtParameters`), since it replaces the whole page palette.

Full rationale (attribute-vs-class, nested-rule mirroring, the custom-function pattern) lives in the VRT testing guide: `CONTRIBUTOR-DOCS/02_style-guide/04_testing/04_visual-regresssion-testing.md`.

## Positioned overlay components

Components that position a surface with `PlacementController` use Floating UI `shift` (clipping ancestors capped by the visual viewport; `autoUpdate` recomputes on scroll). Opening many instances on a taller-than-viewport page can clamp `start`/`end` (not `top`/`bottom`) onto the same spot. Keep every trigger in the initial viewport (place `start`/`end` first, pin the capture viewport, or split the story), disable `shouldFlip`, and do not use `row()`. Full rationale and a worked example (popover's `test/vrt/`) are in the VRT testing guide's "Positioned overlay components" section.

## Custom properties

- Use `customPropertyRows()` to render reference vs override rows.
- Use `coveredCustomProperties()` plus `verifyCustomPropertyCoverage()` in the story `play` function.
- Compare against `.storybook/custom-elements.json` so documented API-table custom properties cannot drift from VRT coverage.
- Choose an override value that renders **obviously different** from the default (e.g. `magenta`, `0px`, an exaggerated size) so the reference and override cells are visibly distinct. If a property silently stops applying, its override cell collapses to match the reference — a distinct value is what makes that regression catchable by visual review.
- Render each override in a context where its effect actually shows (e.g. a padding token that is only live at a given density, or a property that needs its slot populated).

