Speed
Slow motion multiplier
1×
Reduced motion
Show

Small bugs

1 of 4 survives as written
Four structural findings in the brief. One is real and underplayed, one is a misreading of three components, one is correct code the scanner flags, and one cannot be verified in this app at all.Decide here: Which of the structural findings are real once you look at the code.

The three that are not dead

No finding
Selector, ComplexSelector and MultiSelector all declare the same rotation rule. Is anything missing from it?
Selector/Selector.tsx:201-214 — identical at ComplexSelector.tsx:154-166 and MultiSelector.tsx:178-191tsx
// Rotation lives on the chevron glyph itself (passed through `xstyle`), not
// on the layout wrapper above, so the icon's `selector-indicator-icon` theme
// target and the open/closed transform sit on one element — a theme can
// restyle the mark and its rotation through a single selector. The wrapper
// keeps only layout. The status branch renders a different icon, so it never
// picks these up and needs no transition opt-out.
triggerIconRotation: {
transitionProperty: 'transform',
transitionDuration: durationVars['--duration-fast'],
transitionTimingFunction: easeVars['--ease-standard'],
transformOrigin: 'center',
},
triggerIconOpen: {
transform: 'rotate(180deg)',
},
Property, duration and curve, all three tokenised, paired with triggerIconOpen which supplies the transform. There is nothing dead here.The misreading is easy to make and the source explains it: the rotation was moved off the layout wrapper and onto the chevron glyph, which receives it through xstyle. Read the wrapper and you see a component that rotates something with no transition anywhere near it. Worth re-checking before it becomes a milestone line item — it is currently costed as three fixes that do not exist.

The one real no-op is correct

Working as intended
core/Lightbox/Lightbox.tsx:192 — a transitionProperty with no duration. Bug, or opt-out?
Lightbox/Lightbox.tsx:180-194, applied at :691tsx
image: {
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
pointerEvents: 'none',
transitionProperty: 'transform',
transitionDuration: {
default: '200ms',
'@media (prefers-reduced-motion: reduce)': '0ms',
},
transitionTimingFunction: 'ease-out',
},
imageDragging: {
transitionProperty: 'none',
},
// ...applied only while the pointer is down, Lightbox.tsx:691
isDragging && styles.imageDragging,
The zoomed image transitions transform over 200ms so a zoom step glides. While the pointer is down, imageDragging replaces that with transitionProperty: ‘none’, so the image tracks the pointer 1:1 instead of easing 200ms behind it. Removing it would make dragging feel like dragging through syrup.The audit is right that the shape is a no-op and wrong that it is a defect. The lint rule needs the exemption written in from the start: none is the only value of transitionProperty that legitimately appears without a duration.

Why the shape is still worth linting

Both panes declare a transition. Only one of them animates. Which is which, at a glance in review?
Hover the rows
transitionProperty, no duration — reads like motion, does nothing
Row one
Row two
Row three
The same rule with a duration and a curve
Row one
Row two
Row three
This is the argument for the rule even though the package has one instance and it is exempt: the failure is invisible in review. A reviewer sees the word transition and moves on, and the component ships with a state change that snaps.

12 durations with no declared curve

The largest structural finding
A duration with no timing function silently gets the CSS default, which is ease — not --ease-standard. How different are they?
ease — what these sites actually run
--ease-standard — the curve core ships
ease is cubic-bezier(0.25, 0.1, 0.25, 1) — a gentle symmetrical curve. --ease-standard is cubic-bezier(0.24, 1, 0.4, 1), which leaves immediately and decelerates hard. Every site below is running the first one while its neighbours run the second, and nobody wrote that decision down.
SiteDeclarationReading
core/Chat/ChatComposerDrawer.tsx:206transitionDuration: durationVars['--duration-fast']Substantive: a real duration silently taking the CSS default
core/CommandPalette/CommandPaletteInput.tsx:59transitionDuration: '1ms'Substantive: a real duration silently taking the CSS default
core/DateInput/MonthScroller.tsx:165transitionDuration: durationVars['--duration-fast']Substantive: a real duration silently taking the CSS default
core/DateInput/TouchDateField.tsx:438transitionDuration: SWAP_DURATIONSubstantive: a real duration silently taking the CSS default
core/DateInput/Wheel.tsx:110transitionDuration: durationVars['--duration-fast']Substantive: a real duration silently taking the CSS default
core/MobileNav/MobileNav.tsx:74transitionDuration: durationVars['--duration-medium']Substantive: a real duration silently taking the CSS default
core/MobileNav/MobileNav.tsx:130transitionDuration: '0.01s'Reduced-motion escape — the curve is irrelevant at 10ms
core/Stepper/Step.tsx:405transitionDuration:Substantive: a real duration silently taking the CSS default
core/Table/plugins/groupedRows/useTableGroupedRows.tsx:177transitionDuration: '150ms'Substantive: a real duration silently taking the CSS default
core/Table/plugins/tree/useTableTreeData.tsx:265transitionDuration: '150ms'Substantive: a real duration silently taking the CSS default
core/hooks/useKeyboardHint.tsx:107transitionDuration: '150ms'Substantive: a real duration silently taking the CSS default
lab/Drawer/Drawer.tsx:232transitionDuration: '0.01s'Reduced-motion escape — the curve is irrelevant at 10ms
10 of the 12 are substantive — a real duration silently taking ease — across (shared), Chat, CommandPalette, DateInput, MobileNav, Stepper, Table. Only 2 is the 0.01s escape, where having no curve is the right answer. The brief underplays this finding, and the figure has already moved twice as the scanner improved: read it out of the audit rather than quoting it.

The drawer exit, cut short

Confirmed in lab
Slide 410ms, unmount at 250ms. Drag either slider: at what point does an exit stop being an exit?
The brief is right, to the millisecond. lab/Drawer/Drawer.tsx:459 hardcodes a 250ms close timer, while :157 and :219 set the panel and scrim transitions to --duration-medium — 410ms. The dialog is closed at 61% of its own travel, so the last 160ms of the slide never renders.The reduced-motion branch beside it is the tell: the same expression picks 10ms when the user asks for less motion, which means someone thought carefully about the timer and still had to restate the duration by hand. Nothing in either package derives a timer from its transition; MobileNav is the only component that even tries, and it does it by reading getComputedStyle. That is the argument for the JS token mirror in one file.
Page behind
DrawerSlides on --ease-drawer. Removed by a timer that does not know about it.
Slide — 410ms
Unmount timer — 250ms
The panel is removed at 61% of its travel. 39% of the exit never runs.
Slide duration
410ms
Unmount timer
250ms
What the fix does: the panel unmounts when its own animation finishes, so the two numbers cannot drift apart.
The class of bug, and the two ways out of ittsx
// What the brief describes: a literal that has to be kept in sync by hand.
setTimeout(() => setMounted(false), 250);
// What removes the class of bug: the element says when it is done.
const panel = panelRef.current;
if (panel == null) {
setMounted(false);
return;
}
Promise.allSettled(panel.getAnimations().map(a => a.finished)).then(() =>
setMounted(false),
);
// The transitionend form, for a single known property. Note the guard: a
// bubbling transitionend from a child would unmount the panel early, and a
// zero duration fires nothing at all — which is why --duration-instant
// cannot simply be 0.
panel.addEventListener('transitionend', event => {
if (event.target === panel && event.propertyName === 'transform') {
setMounted(false);
}
});
The bug is not the number 250. It is that two numbers have to agree and only one of them is a token: retune --duration-overlay and the timer keeps its old value, so the cut appears in a diff that never touched the drawer.

transition: all

0 today
0 sites. Is a rule with nothing to fix worth writing?
Yes, and it is the cheapest rule in the set. transition: all animates every property that ever changes, including layout properties nobody intended to animate, and it arrives in a codebase one careless line at a time. Core is clean today; the rule keeps it clean and costs one regex.

The count moved twice, and the brief was right

Every structural number on this page is a scanner output, so what the scanner can see decides what gets scheduled. This section started as a grep run against a generated count that looked too small; the generator has since been fixed twice, and criterion 12’s caseload went from something smaller than the brief’s estimate to something larger than it.
A scanner sees shape, not intentThe one no-op it reports is the Lightbox drag opt-out above: correct code with the shape of a defect. Every rule the lint gains has to carry its exemptions from the start, or the first thing it produces is work that ends in “working as intended”.
A grep sees less than a scanner21 of the 34 transform transitions declare transform on its own. 13 declare it inside a longer property list, where a search for transitionProperty: ‘transform’ never finds them — which is how one repository yields three different counts depending on who counted.
The multi-property declarations are the interesting half: criterion 12 findings like the rest, and 2 of them criterion 6 findings as well, because they animate a layout property in the same breath as a compositor one.
SiteDeclaration
core/BottomSheet/BottomSheetPanel.tsx:144transitionProperty: 'transform, opacity'
core/Button/Button.tsx:91transitionProperty: 'background-image, background-color, color, opacity, transform'
core/Chat/ChatDictationButton.tsx:70transitionProperty: 'transform, background-color'
core/Chat/ChatLayoutScrollButton.tsx:71transitionProperty: 'opacity, transform, max-width'
core/ComplexSelector/ComplexSelector.tsx:128transitionProperty: 'background-image, background-color, color, opacity, transform'
core/MultiSelector/MultiSelector.tsx:208transitionProperty: 'background-image, background-color, color, opacity, transform'
core/Overlay/OverlayScrim.tsx:58transitionProperty: 'opacity, visibility, transform'
core/Resizable/ResizeHandle.tsx:194transitionProperty: 'opacity, background-color, transform'
core/Selector/Selector.tsx:230transitionProperty: 'background-image, background-color, color, opacity, transform'
core/Switch/Switch.tsx:288transitionProperty: 'transform, width, height'
core/Table/plugins/rowExpansion/useTableRowExpansion.tsx:86transitionProperty: 'transform, color'
core/Toast/Toast.tsx:65transitionProperty: 'opacity, transform'
core/TopNav/TopNavMegaMenu.tsx:131transitionProperty: 'opacity, transform, overlay, display'
Audit, fixed: 34 sites / 23 componentsBrief: 20+ components
This page found the discrepancy by grepping when the generated number looked too small, and the generator has since been fixed twice: once so a value that wraps onto its own line is still read, and once so a rule containing nested per-state objects is still scanned. Both bugs hid matches, and both made the brief look wrong when it was right. The lasting fix is a parser rather than a regex — a StyleX rule is a TypeScript object literal and can be read as one — but the habit matters more than the tool: when a generated count disagrees with someone who has read the code, check the generator first.

What would have caught each of these

FindingLint ruleCaseload today
A duration with no declared curverequire-timing-function — a transitionDuration in a rule with no transitionTimingFunction and no shorthand.12 sites today, 10 of them substantive.
A transitionProperty with no durationno-noop-transition — same shape, inverted. Must exempt transitionProperty: 'none', which is a deliberate opt-out.1 site today, and it is the exemption case.
transition: allno-transition-all — a property list containing all.0 today. Pure regression guard.
A hardcoded duration, delay or curvemotion-tokens-only — any time literal or bezier literal outside the token file.58 today. Needs an allowlist for the reduced-motion escape.
A timer literal standing in for a transitionno-animation-timeout — a setTimeout whose literal matches a duration token value, inside a component that transitions.1 literal timer in core, and it is unrelated to motion.
A CSS transition on transformno-transform-transition — criterion 12, the motion-library blocker.34 sites across 23 components — criterion 12's entire caseload.
Five of the six are mechanical. The sixth — a timer standing in for a transition — is the only one that needs a heuristic, and it is the only one of the six that describes a bug a user would notice.