@codefusion-cc/tailwind
v0.1.1
Published
Tailwind 4 spacing and dividers that Safari does not pay for: stack-y, stack-x, rule-y and rule-x without the :last-child selectors that make WebKit restyle the page on every DOM insertion, and a check that finds such selectors in an app's built CSS
Maintainers
Readme
@codefusion-cc/tailwind
Tailwind 4 spacing and dividers that Safari does not pay for, and a check that keeps the slow ones out.
Tailwind 4 writes space-y-4 as :where(.space-y-4 > :not(:last-child)) (and space-x-*, divide-y, divide-x
the same way). Once a page has such a rule, WebKit – Safari, and every browser on iOS – restyles a whole subtree for
each element added to or removed from the page. Measured on a shop page of 1,400 elements in WebKit: 41–49 ms for
one inserted <div>, 0 ms without those rules. On a phone that is every drawer, toast, cart line and route change
reacting late. The utilities here give the same spacing with a margin on every child after the first
(:where(.stack-y-4 > :not(legend) + *)), which WebKit checks only on the children of .stack-y-4: 0 ms.
npm install @codefusion-cc/tailwind@import 'tailwindcss';
@import '@codefusion-cc/tailwind/stack.css';Tailwind also scans the repository's other text files for class names, README and scripts included: a sentence that
mentions space-y-4 or divide-y brings the slow rule back. Keep the scan to the sources:
@import 'tailwindcss' source('.'); /* the folder of this CSS file, e.g. src/ */
@import '@codefusion-cc/tailwind/stack.css';| Instead of | Use |
| --- | --- |
| space-y-4 | stack-y-4 (any number Tailwind's spacing takes: stack-y-1.5, stack-y-20) |
| space-x-2 | stack-x-2 |
| divide-y divide-gray-200 | rule-y rule-y-gray-200 (any theme colour) |
| divide-x divide-gray-200 | rule-x rule-x-gray-200 |
Zero specificity, like Tailwind's: a child's own margin or border utility wins. The dividers are not called
divide-* on purpose: tailwind-merge (twMerge, cn) would read divide-rows divide-rows-gray-200 as two divide
colours and drop the first. Two differences from Tailwind's:
- The gap is the margin above the next child, not below the previous one. After an inline child (a
<label>, a link) or one out of the flow (absolutely positioned,sr-only) it shows, where Tailwind's never did: their own bottom margin moved nothing. Give such a container explicit margins, or make the child a block, when moving it over. Tailwind'ssr-onlyis handled for you: the child after a legend or heading for screen readers only gets no gap, as withspace-y. - A divider is the next child's top border instead of the previous child's bottom one: same line, same place, but inside the next child's box, which shows only when the children have backgrounds of their own.
In an app's tests
slowSelectors(css) (@codefusion-cc/tailwind/testing) lists the selectors in a stylesheet that WebKit checks on
every element for its place among its siblings: a child-index pseudo-class (:last-child, :nth-child(),
:first-of-type…) in a compound with no type, class, id or attribute of its own, directly or inside :is(),
:where(), :not() or :has(). Tailwind's space-* and divide-*, :where(.list > :last-child) and .card :last-child
are such selectors; .last\:border-0:last-child (Tailwind's last: variant) and li:last-child are not. Run it on
the app's built CSS:
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { compile } from '@tailwindcss/node'
import { Scanner } from '@tailwindcss/oxide'
import { slowSelectors } from '@codefusion-cc/tailwind/testing'
import { expect, it } from 'vitest'
it('has no CSS that makes WebKit restyle the page on every insertion', async () => {
const entry = new URL('../src/index.css', import.meta.url)
const compiler = await compile(readFileSync(entry, 'utf8'), { base: fileURLToPath(new URL('.', entry)), onDependency: () => {} })
const candidates = new Scanner({ sources: [{ base: process.cwd(), pattern: '**/*', negated: false }] }).scan()
expect(slowSelectors(compiler.build(candidates))).toEqual([])
})It returns each selector once, in order, and an empty array for clean CSS; it never throws.
