CSS ID-Selector to Attribute-Selector Converter
Convert CSS #id selectors to [id="name"] attribute selectors instantly. Reduce specificity, improve maintainability, and follow modern CSS best practices.
About
CSS ID selectors carry a specificity weight of 1−0−0, which is 100 times heavier than a single class selector at 0−1−0. A single misplaced #header .nav a rule can cascade into hours of debugging when you try to override it with a class-based selector downstream. Converting #id selectors to their attribute-selector equivalent [id="id"] reduces specificity to 0−1−0, matching class-level weight. The selector still targets the same element. The behavior is identical. Only the specificity math changes.
This tool parses your raw CSS, identifies every ID selector in every rule (including nested @media and @supports blocks), and rewrites them to attribute form. It does not touch hex color values like #fff inside declarations, nor does it alter strings or url() references. Pro tip: after conversion, run your test suite. Attribute selectors do not match in document.getElementById calls in JavaScript, so your CSS refactor is safe, but verify any JS that relies on specificity order for style overrides.
Formulas
CSS specificity is computed as a tuple (a, b, c, d) where each component counts occurrences of a selector category. The conversion performed by this tool changes the b component (ID count) to the c component (class/attribute count).
Where: a = inline style (0 or 1), b = count of ID selectors (#id), c = count of class selectors, attribute selectors, and pseudo-classes, d = count of element selectors and pseudo-elements.
#main .nav → b = 1, c = 1 ⇒ (0,1,1,0) = 110[id="main"] .nav → b = 0, c = 2 ⇒ (0,0,2,0) = 20The regex pattern used for conversion matches ID selectors while avoiding hex color values inside declaration blocks:
/#([a-zA-Z_][a-zA-Z0-9_-]*)/gThis pattern is applied only to the selector portion of each CSS rule, never to declaration values. The tool splits the CSS into blocks by tracking brace depth, isolates selectors (text before the opening {), applies the transform, and reassembles the output.
Reference Data
| Selector Type | Example | Specificity (a,b,c) | Decimal Weight | Override Difficulty |
|---|---|---|---|---|
| Inline style | style="..." | 1,0,0,0 | 1000 | Requires !important |
| ID selector | #nav | 0,1,0,0 | 100 | Another ID or !important |
| Attribute selector (id) | [id="nav"] | 0,0,1,0 | 10 | Any class-level rule |
| Class selector | .nav | 0,0,1,0 | 10 | Another class or higher |
| Pseudo-class | :hover | 0,0,1,0 | 10 | Same as class |
| Attribute selector (other) | [type="text"] | 0,0,1,0 | 10 | Same as class |
| Element selector | div | 0,0,0,1 | 1 | Any class or higher |
| Pseudo-element | ::before | 0,0,0,1 | 1 | Same as element |
| Universal selector | * | 0,0,0,0 | 0 | Anything overrides |
| Combinators | >, ~, +, | 0,0,0,0 | 0 | No specificity contribution |
:is() / :not() | :not(#foo) | Uses highest argument | Varies | Depends on argument |
:where() | :where(.a) | 0,0,0,0 | 0 | Always zero specificity |
| Two IDs chained | #a #b | 0,2,0,0 | 200 | Extremely hard |
| ID + class | #a.b | 0,1,1,0 | 110 | Needs ID-level rule |
| After conversion | [id="a"].b | 0,0,2,0 | 20 | Two classes suffice |
Frequently Asked Questions
#foo and [id="foo"] match exactly the same element: the one with id="foo". The only difference is specificity weight. #foo has specificity (0,1,0,0) while [id="foo"] has (0,0,1,0). The matched DOM node set is identical per the CSS Selectors Level 4 specification.{). Hex color values appear inside declaration blocks (after {), so they are never touched. Values like color: #336699; pass through unchanged.@media, @supports, or any other at-rule with a block body, it processes the inner rules independently. Each nested selector gets the same ID-to-attribute conversion. The at-rule wrapper itself is preserved verbatim.#foo inside :not(#foo) and convert it to :not([id="foo"]). This is correct behavior. Note that :not() uses the specificity of its argument, so converting the inner ID selector to an attribute selector also reduces the specificity of the entire :not() pseudo-class from ID-level to class-level.--my-color: #abcdef; exist inside declaration blocks, which the converter never modifies. The var(--my-id) function is also inside declarations and is left untouched. Only selectors are transformed.document.getElementById('foo') still works because the element's id attribute hasn't changed. document.querySelector('#foo') also still works because the browser's query engine accepts both forms. Your JavaScript is unaffected.#foo\.bar or #\31 23) are not currently supported and will be left partially converted. For such edge cases, manual review of the output is recommended.