css-authoring
Skill3 starsChanged 2 months agoMITView on GitHub
Guides hand-authored CSS toward channel variables, modern selectors and verified browser support. USE FOR: writing or reviewing component CSS, styling states and variants, choosing between modern selectors, checking whether a CSS feature is safe to ship, keeping specificity flat. DO NOT USE FOR: generated or compiled CSS output, authoring design tokens, framework styling systems such as Tailwind or CSS-in-JS, choosing the values themselves.
1 other project uses a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- equinor/skillsedited, first seen Sep 8, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 1
- equinor/skills★ 299% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository7 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- colour-fill-tiersSkill
- design-system-adrSkill
- spacing-ladderSkill
- typography-scaleSkill
- typography-weight-matchingSkill
- typography-x-height-alignmentSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 84
Prefer the modern feature when it says the intent better — and verify support first (section 3).
- Very specific: line 155
Only caniuse's own features live under features-json/, so an mdn-* identifier always misses, and caniuse's coverage is not exhaustive — …
- Distinctive: line 187
Not interoperable yet: don't build the component's core mechanism on it.
- Very specific: line 217
Keep specificity flat (class and attribute both weigh (0,1,0)) and let @layer decide precedence between concerns.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CSS authoring
How to write CSS that stays legible as it grows: every colour, size, and state change flows through a small set of named channels, selectors say what they mean, and browser support is checked, not remembered.
This skill emits no values: the sizes, colours and weights come from the token skills (see Related). This is the shape the CSS that consumes them takes.
This skill targets modern, evergreen browsers. It recommends features on the basis that they are Baseline available, not that they are universally supported. Backward compatibility with older browsers is the consuming project's responsibility — section 3 is how you find out what your own target actually allows before you write the rule.
1. Channel variables — the core pattern
Declare each channel a component paints with as a pseudo-private custom
property (prefixed --_), declare every CSS property exactly once, and
make every variant, size, and state override the variable, never the
property.
.btn {
--_bg: var(--color-fill-default);
--_bg-hover: var(--color-fill-hover);
--_fg: var(--color-text-strong);
color: var(--_fg);
background-color: var(--_bg); /* the only background-color in the file */
}
.btn[data-variant='danger'] {
--_bg: var(--color-fill-danger);
--_bg-hover: var(--color-fill-danger-hover);
}
.btn:not(:disabled):hover {
--_bg: var(--_bg-hover); /* hover is a one-variable diff */
}A state rule assigns from a channel; it never assigns a literal token.
A hover rule that assigned var(--color-fill-hover) directly would flatten
every variant it touched. Assigning var(--_bg-hover) instead means what the
rule resolves to is a channel each variant owns.
State rules come last. Variant attributes stack, so
.btn[data-variant='danger'][data-size='small'] is (0,3,0) — a tie with
.btn:not(:disabled):hover (class and attribute selectors weigh the same, and
:not() takes the specificity of its argument) — and a third axis would
out-specify it. Ties resolve by source order, so keep the state rules at the
end of the component's rules, or in a later @layer; never rely on the state
selector being heavier.
Why this wins:
- A state change is a diff, not a restatement. Hover changes one value; it cannot accidentally change the property, a shorthand, or the cascade.
- The cascade stays legible. With one
background-colordeclaration there is nothing to out-specify; overrides compose instead of competing. - Channels are the component's API. Reading the
--_block at the top of the root rule tells you everything the component can vary.
Conventions:
--_prefix marks the variable as private to the component — set it inside the component's rules only, never from outside.- Optional channels get a fallback at the use site, so absence is a valid
state, not an error:
outline-color: var(--_outline, transparent);box-shadow: var(--_elevation, none); - Size parameters are channels too (
--_font-size,--_inset,--_gap):[data-size='small']overrides the parameters; the geometry properties that consume them are written once.
2. Modern CSS, deliberately
Prefer the modern feature when it says the intent better — and verify support first (section 3).
- Nesting for component-scoped part rules (
& .icon { … }). :where()to add grouping or hooks at zero specificity — utilities and resets that must never win a fight.:has()for real parent/sibling state (label:has(> input:disabled)) instead of mirroring state onto extra classes with JS.:not()with selector lists::not(:disabled, [aria-selected='true'])— one honest guard instead of stacked negations.- Logical properties (
inline-size,padding-block,margin-inline) unless a physical direction is genuinely meant. @layerto make ordering explicit (@layer reset, tokens, components): a later layer wins without specificity games.:focus-visible, never bare:focus, for focus rings.- Math functions (
calc(),round(),clamp()) to keep derivations in the stylesheet instead of baking their results.round()landed later than the rest of this list; look it up before use (section 3,round-mod-rem) rather than assume.calc()rejects unitless0in addition and subtraction — write0pxfor a semantic zero.
One trap worth naming: box-sizing. min-height/min-width resolve
against the content box by default. Buttons get border-box from the UA
stylesheet; a <div> with the same padding measures larger. Set
box-sizing: border-box explicitly wherever geometry matters. (Runtime state
is section 4's third bullet.)
3. Verify support — never recall it
Your knowledge of browser support has a cutoff; the web does not. Before using a feature you have not verified in this session, check it.
Ask before you emit a rule that leans on a feature's support:
Which browsers does this product ship to — is there a
browserslist?
Find the project's own matrix first. It overrides every global figure:
browserslist in package.json, a .browserslistrc, or a targets field in
the build config. A product with a declared support floor makes "95% global"
both too strict and too lax.
If the project declares no matrix, that is the finding. Say so, and propose
one rather than quietly substituting global usage figures — those answer a
question about the whole web, not about the people who use this product.
browserslist is the form to propose: every build tool already reads it, so it
turns a support policy from prose somebody remembers into a value a tool can
check.
# .browserslistrc — the browsers this product actually ships to
last 2 versions
not dead- Canonical machine-readable data, always current:
https://raw.githubusercontent.com/Fyrd/caniuse/main/features-json/<slug>.json— readusage_perc_y(global support %) andstatsper browser. Slugs match caniuse URLs — for the features this skill recommends:css-has,css-nesting,css-cascade-layers,css-focus-visible,css-logical-props,css-matches-pseudo(:is()),css-not-sel-list(:not()with a list),css-math-functions(min()/max()/clamp()only),calc. All nine returned JSON on 2026-09-08. - Human view:
https://caniuse.com/<slug>orhttps://caniuse.com/?search=<term>. - Baseline status, and the fallback when caniuse has no feature at all:
https://api.webstatus.dev/v1/features/<id>, searchable with?q=<term>. caniuse does not track:where()orround(); webstatus does, aswhereandround-mod-rem.
If the fetch misses, do not fall back on memory — that is the failure this
section exists to prevent. A guessed slug returns GitHub's 404 page, not JSON.
Only caniuse's own features live under features-json/, so an mdn-*
identifier always misses, and caniuse's coverage is not exhaustive — newer
features may exist only on webstatus.dev. Search
https://caniuse.com/?search=<term>, then
https://api.webstatus.dev/v1/features?q=<term>; if both come up empty, say in
your answer that you could not verify rather than asserting from memory.
Rules of thumb:
- Widely available — Baseline Widely available governs: use freely.
usage_perc_yis context, not a second gate. The two measure different things — Baseline counts ~30 months of interop across a core browser set,usage_perc_yis market-share weighted and lags it — so a feature can be Baseline widely and still sit below 95%.:has()and nesting both do, and both are safe. Treat a low usage figure as a prompt to check the project's matrix, not as a veto. - Newly available — Baseline newly, or not yet Baseline: the project's matrix
decides. On a managed, auto-updating fleet a Baseline-newly feature is
usually fine; where there is a long tail of older browsers, ship a graceful
fallback and say so in the commit or PR. Not Baseline at all usually means
one engine: an evergreen target is still two engines, Chromium and WebKit
(iOS is WebKit in practice), so read the Safari and iOS Safari columns
before relying on it, and treat Gecko as best-effort — a note and a
fallback, not a veto.
Pick the fallback mechanism that matches the feature:
@supports selector(…)for selectors (:has(),:where(),:focus-visible),@supports (prop: val)for properties and values, and a preceding declaration the older engine can parse for an unsupported value —var()'s fallback only covers an unset custom property, not a value that failed to parse. Nesting is detectable with@supports selector(&);@layeris not detectable at all — where either is outside the matrix, author the flat form. - Not interoperable yet: don't build the component's core mechanism on it.
4. Selector discipline
- One class on the component root (
.btn); nothing else about the component needs a class of its own unless the element is ambiguous. - Variants, sizes, and boolean options are
data-*attributes, never modifier classes — ADR-0006 rule 3 (Accepted 2026-06-29; link checked 2026-09-08):.btn[data-variant='danger'][data-size='small']. Name the attribute after the axis, so the markup says which axis each value belongs to. Let the default value also match the attribute's absence (:is([data-size='md'], :not([data-size]))), so resting markup stays bare. Booleans are valueless presence attributes (data-readonly), exactly like the platform's owndisabled. - Attributes carry design-time configuration; pseudo-classes and ARIA
carry runtime state. Style
:hover,:disabled,[aria-selected='true'],[aria-pressed='true']directly — never mirror them intodata-*. (The same split the headless libraries expose: Radix's styling guide says "when components are stateful, their state will be exposed in adata-stateattribute" — state the element computes; the variants an author chooses are a different kind of information.) - Semantic descendants are targeted by element, scoped with
>(& > details > summary,& > :is(input, select, textarea),& th, & td) so consumer content inside the component can never collide. Inner classes only where the element alone is ambiguous (two icon slots). - Ancestor mode scopes (
[data-theme]/[data-density]-style subtree switches) belong to the token layer, not to components — a component selector should never mention them. - No
#idselectors, no!important— if you need either, the layer order or the channel design is wrong; fix that instead. - Keep specificity flat (class and attribute both weigh
(0,1,0)) and let@layerdecide precedence between concerns.
A note on lint configs: stylelint-config-standard accepts all of the above
as-is. A selector-class-pattern that demands BEM (block__element--modifier)
contradicts ADR-0006 — do not copy one from an older repo without checking
what it enforces.
Representative requests
Four acceptance criteria — the button with variants and a hover, the review
of committed component CSS, "can I use :has() here", and adding a size —
plus a routing check:
references/representative-requests.md.
Positions this skill takes
Five, each with what backs it: channels over restated properties, state rules
last rather than heavier, data-* for configuration and the platform for
state, support verified live with Baseline governing, and the matrix in a
browserslist rather than in prose:
references/positions.md.
Related
typography-scale,spacing-ladder,colour-fill-tiers— where the values a channel is set to come from: the size and line-height, the inset and padding, the fill rung. This skill says how the CSS that consumes them is shaped, not what they are.typography-weight-matching— its matched weights and letter-spacing are channel values too (--_weight,--_tracking).
Provenance
The channel-variable pattern and the selector rules are the ones the component
emitter in the EDS token rework (equinor/ids-meetup-oslo-26, Equinor-internal;
not an official EDS project) enforces
on every generated stylesheet and checks in its harness; this skill is how
they travel to CSS written by hand. data-* over modifier classes is
ADR-0006 in equinor/design-system (MIT, public). The support-verification
endpoints are caniuse's features-json and webstatus.dev, both checked live
on 2026-09-08.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

