jantimon
jantimon

Reputation: 38160

Is there a CSS selector for elements containing certain text?

I am looking for a CSS selector for the following <table>:

Name Identity Age
Peter male 34
Susanne female 12

Is there any selector to match all <td>s containing the specific content "male"?

Upvotes: 904

Views: 1477426

Answers (22)

Cees Timmerman
Cees Timmerman

Reputation: 19664

Not with CSS, but JS can do it:

document.querySelectorAll("td").forEach(el => {
    if (el.innerHTML.includes("male")) {
        console.log(el)
    }
})

Upvotes: 0

Dean J
Dean J

Reputation: 40358

If I read the specification correctly, no.

You can match on an element, the name of an attribute in the element, and the value of a named attribute in an element. I don't see anything for matching content within an element, though.

Upvotes: 626

Michael Mintz
Michael Mintz

Reputation: 15556

Some test frameworks have gotten around the limitation of no official TAG:contains("TEXT") CSS Selector by accepting such a selector anyway, and then converting it into an XPath selector before using it to find an element.

For example, in SeleniumBase:

'button > span:contains("Run")' converts to "//button/span[contains(., 'Run')]" before being used to find an element via XPath. This :contains("TEXT") syntax originates from jQuery, where such a selector is valid.

Using that selector in a SeleniumBase script could look like this:

self.click('button > span:contains("Run")')

which is the equivalent of:

self.click("//button/span[contains(., 'Run')]")

Note that SeleniumBase autodetects selectors (unlike vanilla Selenium, where you would normally specify the type of selector via a by arg).

Upvotes: 0

Gerold Broser
Gerold Broser

Reputation: 14782

If you don't create the DOM yourself (e.g. in a userscript) you can do the following with pure JS:

// Add a custom attribute 'text' to all td's, set their values to td.innerText:
for ( td of document.querySelectorAll('td') ) {
  console.debug("text:", td, td.innerText)
  td.setAttribute('text', td.innerText)
}

// Query for the custom attribute 'text' with a specific value:
for ( td of document.querySelectorAll('td[text="male"]') )
  console.debug("male:", td, td.innerText)
<table>
  <tr>
    <td>Peter</td>
    <td>male</td>
    <td>34</td>
  </tr>
  <tr>
    <td>Susanne</td>
    <td>female</td>
    <td>12</td>
  </tr>
</table>

Console output

text: <td> Peter
text: <td> male
text: <td> 34
text: <td> Susanne
text: <td> female
text: <td> 12
male: <td text="male"> male

Upvotes: 8

Ryan Shillington
Ryan Shillington

Reputation: 25167

If you're using Chimp / Webdriver.io, they support a lot more CSS selectors than the CSS spec.

This, for example, will click on the first anchor that contains the words "Bad bear":

browser.click("a*=Bad Bear");

Upvotes: 5

EoghanM
EoghanM

Reputation: 26953

You could transfer the content into an attribute on the cell, and then use CSS to copy that for display purposes (Don't Repeat Yourself — something the other similar answers have ignored). This utilizes content and attr()

td[aria-label]::before { 
  content: attr(aria-label);
  /*text-transform: capitalize; <-- possible */
}
td[aria-label="male"] {
  color: fuchsia;
}
<table>
  <tr>
    <td aria-label="Peter"></td>
    <td aria-label="male"></td>
    <td aria-label="34"></td>
  </tr>
  <tr>
    <td aria-label="Susanne"></td>
    <td aria-label="female"></td>
    <td aria-label="12"></td>
  </tr>
  <tr>
    <td aria-label="Lucas"></td>
    <td aria-label="male"></td>
    <td aria-label="41"></td>
  </tr>
</table>

Why you would not use this method:

  • inability to select the text
  • accessibility: aria-label doesn't really mean 'content'

If those are concerns, you could drop the DRY nice-to-have requirement; but in that case you might as well add a className; class="f" or class="m" to the TD and select based on that.

Upvotes: 3

cst1992
cst1992

Reputation: 3941

Excellent answers all around, but I think I can add something that worked for me in a practical scenario: exploiting the aria-label attribute for CSS.

For the readers that don't know: aria-label is an attribute that is used in conjunction with other similar attributes to let a screen-reader know what something is, in case someone with a visual impairment is using your website. Many websites add these attributes to elements with images or text in them, as "descriptors".

This makes it highly website-specific, but in case your element contains this, it's fairly simple to select that element using the content of the attribute:

HTML:

<td aria-label="male">Male</td>
<td aria-label="female">Female</td>

CSS:

td[aria-label="male"] {
    outline: 1px dotted green;
}

This is technically the same thing as using the data-attribute solution, but this will work for you if you are not the author of the website, plus this is not some out-of-the-way solution that is specifically designed to support this use case; it's fairly common on its own. The one downside of it is that there's really no guarantee that your intended element will have this attribute present.

Upvotes: 6

Buksy
Buksy

Reputation: 12234

You could set content as data attribute and then use attribute selectors, as shown here:

/* Select every cell matching the word "male" */
td[data-content="male"] {
  color: red;
}

/* Select every cell starting on "p" case insensitive */
td[data-content^="p" i] {
  color: blue;
}

/* Select every cell containing "4" */
td[data-content*="4"] {
  color: green;
}
<table>
  <tr>
    <td data-content="Peter">Peter</td>
    <td data-content="male">male</td>
    <td data-content="34">34</td>
  </tr>
  <tr>
    <td data-content="Susanne">Susanne</td>
    <td data-content="female">female</td>
    <td data-content="14">14</td>
  </tr>
</table>

You can also use jQuery to easily set the data-content attributes:

$(function(){
  $("td").each(function(){
    var $this = $(this);
    $this.attr("data-content", $this.text());
  });
});

Upvotes: 72

Anthony
Anthony

Reputation: 77

I find the attribute option to be your best bet if you don't want to use javascript or jquery.

E.g to style all table cells with the word ready, In HTML do this:

 <td status*="ready">Ready</td>

Then in css:

td[status*="ready"] {
        color: red;
    }

Upvotes: -2

user40521
user40521

Reputation: 2129

As CSS lacks this feature you will have to use JavaScript to style cells by content. For example with XPath's contains:

var elms = document.evaluate( "//td[contains(., 'male')]", node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null )

Then use the result like so:

for ( var i=0 ; i < elms.snapshotLength; i++ ){
   elms.snapshotItem(i).style.background = "pink";
}

https://jsfiddle.net/gaby_de_wilde/o7bka7Ls/9/

Upvotes: 30

Eftychia Thomaidou
Eftychia Thomaidou

Reputation: 187

The syntax of this question looks like Robot Framework syntax. In this case, although there is no css selector that you can use for contains, there is a SeleniumLibrary keyword that you can use instead. The Wait Until Element Contains.

Example:

Wait Until Element Contains  | ${element} | ${contains}
Wait Until Element Contains  |  td | male

Upvotes: -4

vhs
vhs

Reputation: 10089

You could also use content with attr() and style table cells that are :not :empty:

th::after { content: attr(data-value) }
td::after { content: attr(data-value) }
td[data-value]:not(:empty) {
  color: fuchsia;
}
<table>
  <tr>
    <th data-value="Peter"></th>
    <td data-value="male">&#x0200B;</td>
    <td data-value="34"></td>
  </tr>
  <tr>
    <th data-value="Susanne"></th>
    <td data-value="female"></td>
    <td data-value="12"></td>
  </tr>
  <tr>
    <th data-value="Lucas"></th>
    <td data-value="male">&#x0200B;</td>
    <td data-value="41"></td>
  </tr>
</table>

A ZeroWidthSpace is used to change the color and may be added using vanilla JavaScript:

const hombres = document.querySelectorAll('td[data-value="male"]');
hombres.forEach(hombre => hombre.innerHTML = '&#x0200B;');

Although the <td>s end tag may be omitted doing so may cause it to be treated as non-empty.

Upvotes: -1

campino2k
campino2k

Reputation: 1671

Doing small Filter Widgets like this:

    var searchField = document.querySelector('HOWEVER_YOU_MAY_FIND_IT')
    var faqEntries = document.querySelectorAll('WRAPPING_ELEMENT .entry')

    searchField.addEventListener('keyup', function (evt) {
        var testValue = evt.target.value.toLocaleLowerCase();
        var regExp = RegExp(testValue);

        faqEntries.forEach(function (entry) {
            var text = entry.textContent.toLocaleLowerCase();

            entry.classList.remove('show', 'hide');

            if (regExp.test(text)) {
                entry.classList.add('show')
            } else {
                entry.classList.add('hide')
            }
        })
    })

Upvotes: -4

DJDaveMark
DJDaveMark

Reputation: 2855

I agree the data attribute (voyager's answer) is how it should be handled, BUT, CSS rules like:

td.male { color: blue; }
td.female { color: pink; }

can often be much easier to set up, especially with client-side libs like angularjs which could be as simple as:

<td class="{{person.gender}}">

Just make sure that the content is only one word! Or you could even map to different CSS class names with:

<td ng-class="{'masculine': person.isMale(), 'feminine': person.isFemale()}">

For completeness, here's the data attribute approach:

<td data-gender="{{person.gender}}">

Upvotes: 4

Eduard Florinescu
Eduard Florinescu

Reputation: 17541

Most of the answers here try to offer alternative to how to write the HTML code to include more data because at least up to CSS3 you cannot select an element by partial inner text. But it can be done, you just need to add a bit of vanilla JavaScript, notice since female also contains male it will be selected:

      cells = document.querySelectorAll('td');
    	console.log(cells);
      [].forEach.call(cells, function (el) {
    	if(el.innerText.indexOf("male") !== -1){
    	//el.click(); click or any other option
    	console.log(el)
    	}
    });
 <table>
      <tr>
        <td>Peter</td>
        <td>male</td>
        <td>34</td>
      </tr>
      <tr>
        <td>Susanne</td>
        <td>female</td>
        <td>14</td>
      </tr>
    </table>

<table>
  <tr>
    <td data-content="Peter">Peter</td>
    <td data-content="male">male</td>
    <td data-content="34">34</td>
  </tr>
  <tr>
    <td data-conten="Susanne">Susanne</td>
    <td data-content="female">female</td>
    <td data-content="14">14</td>
  </tr>
</table>

Upvotes: 3

Jeff Beaman
Jeff Beaman

Reputation: 2941

Looks like they were thinking about it for the CSS3 spec but it didn't make the cut.

:contains() CSS3 selector http://www.w3.org/TR/css3-selectors/#content-selectors

Upvotes: 289

John
John

Reputation: 13756

@voyager's answer about using data-* attribute (e.g. data-gender="female|male" is the most effective and standards compliant approach as of 2017:

[data-gender='male'] {background-color: #000; color: #ccc;}

Pretty much most goals can be attained as there are some albeit limited selectors oriented around text. The ::first-letter is a pseudo-element that can apply limited styling to the first letter of an element. There is also a ::first-line pseudo-element besides obviously selecting the first line of an element (such as a paragraph) also implies that it is obvious that CSS could be used to extend this existing capability to style specific aspects of a textNode.

Until such advocacy succeeds and is implemented the next best thing I could suggest when applicable is to explode/split words using a space deliminator, output each individual word inside of a span element and then if the word/styling goal is predictable use in combination with :nth selectors:

$p = explode(' ',$words);
foreach ($p as $key1 => $value1)
{
 echo '<span>'.$value1.'</span>;
}

Else if not predictable to, again, use voyager's answer about using data-* attribute. An example using PHP:

$p = explode(' ',$words);
foreach ($p as $key1 => $value1)
{
 echo '<span data-word="'.$value1.'">'.$value1.'</span>;
}

Upvotes: 2

Matas Vaitkevicius
Matas Vaitkevicius

Reputation: 61509

For those who are looking to do Selenium CSS text selections, this script might be of some use.

The trick is to select the parent of the element that you are looking for, and then search for the child that has the text:

public static IWebElement FindByText(this IWebDriver driver, string text)
{
    var list = driver.FindElement(By.CssSelector("#RiskAddressList"));
    var element = ((IJavaScriptExecutor)driver).ExecuteScript(string.Format(" var x = $(arguments[0]).find(\":contains('{0}')\"); return x;", text), list);
    return ((System.Collections.ObjectModel.ReadOnlyCollection<IWebElement>)element)[0];
}

This will return the first element if there is more than one since it's always one element, in my case.

Upvotes: 8

Esteban K&#252;ber
Esteban K&#252;ber

Reputation: 36852

You'd have to add a data attribute to the rows called data-gender with a male or female value and use the attribute selector:

HTML:

<td data-gender="male">...</td>

CSS:

td[data-gender="male"] { ... }

Upvotes: 179

Matt Whipple
Matt Whipple

Reputation: 7134

There is actually a very conceptual basis for why this hasn't been implemented. It is a combination of basically 3 aspects:

  1. The text content of an element is effectively a child of that element
  2. You cannot target the text content directly
  3. CSS does not allow for ascension with selectors

These 3 together mean that by the time you have the text content you cannot ascend back to the containing element, and you cannot style the present text. This is likely significant as descending only allows for a singular tracking of context and SAX style parsing. Ascending or other selectors involving other axes introduce the need for more complex traversal or similar solutions that would greatly complicate the application of CSS to the DOM.

Upvotes: 79

moettinger
moettinger

Reputation: 5216

Using jQuery:

$('td:contains("male")')

Upvotes: 184

Edwin V.
Edwin V.

Reputation: 1377

I'm afraid this is not possible, because the content is no attribute nor is it accessible via a pseudo class. The full list of CSS3 selectors can be found in the CSS3 specification.

Upvotes: 8

Related Questions