Understanding Selectors
When you want to apply rules to specific elements, you use selectors in the nodeRules or childNodeRules properties. Markuplint's selector syntax supports CSS Selectors, extended pseudo-classes, and regular expressions — giving you flexible control over which elements a rule targets.
CSS Selectors
Markuplint supports parts of W3C Selectors Level 4.
Supported selector syntaxes and operators
| Selector Type | Code Example | Support |
|---|---|---|
| Universal selector | * | ✅ |
| Type selector | div | ✅ |
| ID selector | #id | ✅ |
| Class selector | .class | ✅ |
| Attribute selector | [data-attr] | ✅ |
| Attribute selector, Exact match | [data-attr=value] | ✅ |
| Attribute selector, Include whitespace separated | [data-attr~=value] | ✅ |
| Attribute selector, Subcode match | [data-attr|=value] | ✅ |
| Attribute selector, Partial match | [data-attr*=value] | ✅ |
| Attribute selector, Forward match | [data-attr^=value] | ✅ |
| Attribute selector, Backward match | [data-attr$=value] | ✅ |
| Negation pseudo-class | :not(div) | ✅ |
| Matches-Any pseudo-class | :is(div) | ✅ |
| Specificity-adjustment pseudo-class | :where(div) | ✅ |
| Relational pseudo-class | :has(div) :has(> div) | ✅ |
| Directionality pseudo-class | :dir(ltr) | ❌ |
| Language pseudo-class | :lang(en) | ❌ |
| Hyperlink pseudo-class | :any-link | ❌ |
| Link History pseudo-class | :link :visited | ❌ |
| Local link pseudo-class | :local-link | ❌ |
| Target pseudo-class | :target | ❌ |
| Target container pseudo-class | :target-within | ❌ |
| Reference element pseudo-class | :scope | ✅ |
| Current-element pseudo-class | :current :current(div) | ❌ |
| Past pseudo-class | :past | ❌ |
| Future pseudo-class | :future | ❌ |
| Interactive pseudo-class | :active :hover :focus :focus-within :focus-visible | ❌ |
| Enable and disable pseudo-class | :enable :disable | ❌ |
| Mutability pseudo-class | :read-write :read-only | ❌ |
| Placeholder-shown pseudo-class | :placeholder-shown | ❌ |
| Default-option pseudo-class | :default | ❌ |
| Selected-option pseudo-class | :checked | ❌ |
| Indeterminate value pseudo-class | :indeterminate | ❌ |
| Validity pseudo-class | :valid :invalid | ❌ |
| Range pseudo-class | :in-range :out-of-range | ❌ |
| Optionality pseudo-class | :required :optional | ❌ |
| Empty-Value pseudo-class | :blank | ❌ |
| User-interaction pseudo-class | :user-invalid | ❌ |
| Root pseudo-class | :root | ✅ |
| Empty pseudo-class | :empty | ❌ |
| Nth-child pseudo-class | :first-child | ✅ |
| Nth-child pseudo-class | :nth-child(2) :nth-last-child(2) :last-child :only-child | ❌ |
Nth-child pseudo-class (of El Syntax) | :nth-child(2 of div) :nth-last-child(2 of div) | ❌ |
| Nth-of-type pseudo-class | :nth-of-type(2) :nth-last-of-type(2) :first-of-type :last-of-type :only-of-type | ❌ |
| Nth-col pseudo-class | :nth-col(2) :nth-last-col(2) | ❌ |
| Pseudo elements | ::before ::after | ❌ |
| Descendant combinator | div span | ✅ |
| Child combinator | div > span | ✅ |
| Next-sibling combinator | div + span | ✅ |
| Subsequent-sibling combinator | div ~ span | ✅ |
| Column combinator | div || span | ❌ |
| Nesting selector | & & div | ❌ |
| Multiple selectors | div, span | ✅ |
A selector that uses a syntax marked ❌, or gives an extended pseudo-class an argument it does not accept (for example :aria(foo) or :model(foo)), is reported as an error of the configuration, the same as a selector that cannot be parsed. This applies to every selector the configuration holds, whether in nodeRules, childNodeRules, pretenders or overrides, and whether or not the document has an element it would be matched against.
Markuplint supports the :has selector. Combined with combinator operators, it allows very flexible element selection.
{
"nodeRules": [
{
// Apply to all elements that are <picture>'s child and previous siblings of <img>
"selector": "picture img:has(~)",
"rules": {
"require-attr": true
}
}
]
}
Case of tag names
A type selector compares tag names case-insensitively for HTML elements, as CSS does. In a language whose parser tells the case of a tag name apart — JSX, Svelte, Astro, and MDX — it compares them as written: FooBar matches <FooBar> but not <foobar>, and DIV does not match <div>. The same goes for nodeName of a regular expression selector. Write the selector in the case of the tag.
A Vue template has its own rule, as Vue resolves a tag to a component by any spelling of its name (Component Name Casing). A type selector of a component stands for each spelling: FooBar, fooBar and foo-bar match <FooBar>, <fooBar> and <foo-bar> alike, and none of them matches <foobar>. A selector of one word is the name as written or capitalized, so Transition matches <transition>. An HTML element is told by its name in lower case: button matches <button> but not <Button>, and Button matches <Button> but not <button>. What is an HTML element is Vue's own list of native tags: a tag that the list does not hold, such as <marquee> or <search>, resolves to a component, so Marquee matches <marquee> too. A regular expression selector is not normalized and is matched with the name as it is written.
Case of attribute names
An attribute selector compares the name as the browser's DOM does, so a selector follows what the page has and not how the source spells it. On an HTML element and on a custom element the case is ignored: [role] matches <div ROLE> and [DATA-VALUE] matches <div data-value>. On an SVG or MathML element the name the DOM gives the attribute is compared exactly, as CSS does. Where the HTML parser reads the name (HTML, Pug, Astro, and the static attributes of Svelte), the DOM has the spelling of the specification: [viewBox] matches <svg VIEWBOX>, and [viewbox] does not. Where code sets the name through the DOM API (JSX, MDX, Vue, and the dynamic attributes of Svelte), an SVG or MathML element keeps the name as written: [ROLE] matches <svg ROLE>, and [role] does not. The props of a component are compared as written; in Vue, [my-label] and [myLabel] are one prop. A regular expression selector for the name of an attribute is matched with the name the DOM gives it (a Vue prop, with either spelling), and ignores case where an attribute selector does.
Trees
The content of a <template> element and a declarative shadow root (a <template shadowrootmode> that is attached to its parent element) are trees of their own, as they are in the DOM. A selector is matched within the tree of the element: section p does not match a <p> in the content of a <template> that is in a <section>, and :has() and :closest() do not cross the boundary either. In React and Vue, the children of a <template> without a directive are in the tree of the document, so they are matched as any other children are.
Specificity
Markuplint applies specificity the same way as CSS Selectors, allowing you to control the priority of rule application.
{
"nodeRules": [
{
// Apply
"selector": "#id.class-name", // Specificity: 1-1-0
"rules": {
"require-attr": true
}
},
{
// Don't apply (Ignore)
"selector": ".class-name", // Specificity: 0-1-0
"rules": {
"require-attr": false
}
}
]
}
When selectors have the same specificity, rules are applied in order of their settings.
Markuplint supports the :where selector, which always has zero specificity.
{
"nodeRules": [
{
// Don't apply
"selector": ":where(#id.class-name)", // Specificity: 0-0-0
"rules": {
"require-attr": true
}
},
{
// Apply (Overwrite)
"selector": ".class-name", // Specificity: 0-1-0
"rules": {
"require-attr": false
}
}
]
}
Extended selectors
Markuplint provides extended pseudo-class-like selectors for HTML-specific matching.
(Deprecated — use:closest:is()instead):aria:role:model:display-size
| Syntax | Description |
|---|---|
:closest(selectors) | Deprecated. Use :is(selectors *) instead. See migration guide. |
:aria(has name) | ARIA pseudo-class |
:role(heading) | ARIA Role pseudo-class |
:model(interactive) | Content Model pseudo-class |
:display-size(> 1) | Display Size pseudo-class |
These selectors are experimental features and do not guarantee compatibility. They may be removed in the next major version if they conflict with CSS Selectors.
:closest
:closest() is deprecated and will be removed in the next major version. Use the standard :is() pseudo-class with the descendant combinator instead.
Concept: Element.closest method
:closest(selectors)
Matches elements that have an ancestor matching the given selectors.
Migration: Replace :closest(X) with :is(X *):
| Before | After |
|---|---|
:closest(nav) | :is(nav *) |
:closest(.wrapper) | :is(.wrapper *) |
div:closest(nav):closest(section) | div:is(nav *):is(section *) |
:aria
ARIA pseudo-class
:aria(syntax)
| Syntax | Code | Description |
|---|---|---|
has name | :aria(has name) | Apply elements have accessible name |
has no name | :aria(has no name) | Apply elements have no accessible name |
:role
ARIA Role pseudo-class
:role(roleName)
:role(roleName|version)
For example, :role(button) matches <button> and <div role="button">.
You can specify version of WAI-ARIA by separating the pipe like :role(form|1.1).
:model
Content Model pseudo-class
:model(contentModel)
For example, :model(interactive) matches <a>(with href attr), <button>, and more.
A category that lists custom elements in the HTML Standard, such as flow, phrasing and palpable, also matches custom elements (<x-foo>) and components (<Foo />). An element that a component pretends to be is judged by the name it pretends to be: it matches only when that name contains a hyphen.
An element that the HTML Standard allows only as the first child of its parent matches only there: :model(flow) matches <style> only as the first child. The first child is counted as the HTML Standard counts it, so non-whitespace text before the element counts (comments and inter-element whitespace do not): the <style> in <div>text<style> does not match. A template expression before the element ({{ msg }}) does not count, as what it outputs is known only at run time; text and elements inside a conditional block count, as the content of the block is taken as present. :first-child written in a selector keeps the Selectors definition, which counts elements only.
:display-size
Display Size pseudo-class
:display-size(operator number)
The operator is one of =, >, >=, < and <=, and the number is a non-negative integer. For example, select:is([multiple], :display-size(> 1)) matches a <select> that has the multiple attribute or whose display size is greater than 1.
It matches only a <select> element of the HTML namespace, and compares its display size. The display size is the number that the size attribute is parsed as by the rules for parsing non-negative integers, or, when the attribute is absent or cannot be parsed, 4 if the element has the multiple attribute and 1 if it has not. Leading whitespace, a + sign, leading zeros and trailing characters are allowed, so size="01" is 1, size="2abc" is 2, and size="0" is 0. An attribute selector cannot tell these apart, as [size]:not([size="1"]) takes size="01" for something other than 1.
The size is read as the text it is written in, so a template expression (:size="n") is not a number: its display size is the one for an attribute that cannot be parsed.
Regular expression selector
Use regular expressions to select elements by matching the node name, attribute name, attribute value, or a combination of these.
{
"childNodeRules": [
{
"regexSelector": {
"nodeName": "/^[a-z]+$/",
"attrName": "/^[a-z]+$/",
"attrValue": "/^[a-z]+$/"
},
"rules": {
"require-attr": "true"
}
}
]
}
For more complex matching patterns, see the regexSelector property reference.
regexSelector and selector cannot be used together. If both are specified, selector takes priority.
Next steps
- Applying Rules — Use selectors in
nodeRulesandchildNodeRules - Usecases — Real-world examples using selectors and regex selectors
- Configuration Properties — Full reference for
nodeRules,childNodeRules, andregexSelector