830

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

Peter    | male    | 34
Susanne  | female  | 12

Is there any selector to match all TDs containing "male"?

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
jantimon
  • 36,840
  • 23
  • 122
  • 185
  • 1
    The problem is that this would be very hard to implement in a performant way. – Ms2ger Oct 06 '09 at 13:11
  • 11
    An XPath selector can do it with the .text() method (if you prefer not to use JavaScript executor). – djangofan Feb 03 '13 at 20:46
  • 18
    Here's an exemple of how you can do it using xpath : //h1[text()='Session'] and you can test xpath in Chrome by typing $x("//h1[text()='Session']") in the console – VinnyG Jun 11 '13 at 17:53
  • 2
    This would be so convenient. For example, a table cell containing a checkmark or the string "Yes", "Pass", "OK" etc. could be green. – Andreas Rejbrand Dec 13 '14 at 00:34
  • the `$x` answers the question. for the original question, `$x("//td[text()='male']")` does the trick – XoXo Jan 06 '17 at 19:15
  • @Ms2ger and that's why it is usually implemented in an imperformant way with JavaScript :q – BarbaraKwarc Feb 28 '17 at 04:23
  • The best answer for doing this in current standards is found below the accepted answer, here: https://stackoverflow.com/a/41281583/1163705 and if jQuery is not an option it can be done in Vanilla JS as well. – Xandor May 01 '19 at 14:13
  • Is it possible to find css selector like xpath "contains" method? I have dynamic css for the element, because this element can be too many times on the page. – Mikhail Barinov Aug 16 '21 at 09:48

21 Answers21

580

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.

Dean J
  • 39,360
  • 16
  • 67
  • 93
  • 324
    There is one edge case: `:empty`. – Ciro Santilli OurBigBook.com Jul 06 '14 at 10:55
  • 52
    To answer my own question: Yes, it is still true for today! Content selectors were deprecated! No more content selectors since CSS3. (https://www.w3.org/TR/css3-selectors/#selectors) – Wlad Sep 22 '16 at 12:42
  • 14
    @CiroSantilliTRUMPBANISBAD that is, technically, not an edge case **yet**, `:empty()` only selects something without children, text in DOM is not "just" text, it's a text **Node**. Similar principle as `:has()` , this **MIGHT** (or may not) change a bit in future if following feature from **DRAFT** would be adopted - **FOLLOWING FEATURE IS NOT SUPPORTED BY ANY BROWSERS YET (2021)**: "The :empty pseudo-class represents an element that has no children except, optionally, document white space characters ."https://drafts.csswg.org/selectors-4/#empty-pseudo – jave.web Feb 09 '21 at 16:48
254

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

e-motiv
  • 5,795
  • 5
  • 27
  • 28
Jeff Beaman
  • 2,583
  • 1
  • 14
  • 3
  • 28
    `Looks like they were thinking about it for the CSS3 spec but it didn't make the cut.` And for good reason, it would violate the whole premise of separating styling, content, structure, and behavior. – Synetech Jul 06 '15 at 22:01
  • 104
    @Synetech It can actually help the separation of styling from content, as it means that the content doesn't need to know about its client is going to consider as important to base styling on. Right now our html content typically is tightly paired to the css by including classes that we know the styler cares about. They are already shifting towards letting CSS at the content, as evidenced by attribute value selectors in CSS3. – DannyMeister Jul 16 '15 at 23:23
  • 18
    @Synetech and how is it "behaviour", exactly? To me it is a matter of presentation. It doesn't *do* anything – it *presents* certain types of content in a certain way. – BarbaraKwarc Feb 28 '17 at 04:27
  • 3
    @BarbaraKwarc, that's not what I said. Look up principals of web-design. In good web-design, styling is supposed to be contained in CSS files, content in databases, structure in HTML files, and behavior in JavaScript file. Ideally, you shouldn't mix them. In this case, it's not the behavior that's getting mixed, it's the styling and content (putting content in the style). That's bad because for one thing, it complicates things thanks to i18n; e.g., will you have a separate CSS entry for every single language you support? – Synetech Jul 21 '18 at 10:44
  • 27
    @DannyMeister, oh you don't have to convince me. A couple of years on and I find myself back here while trying to look for exactly this functionality, and upset that not only does it not exist, but it was rejected. eBay just changed their UI and made their site very hard to use. I'm trying to fix it with a user stylesheet, but their markup doesn't distinguish between auctions and BIN listings, so the only way to highlight the number of bids in a list is by matching the element by its textual content. All it takes to convince someone is for them to experience a use-case scenario first hand. – Synetech Jul 21 '18 at 10:48
  • 3
    IMHO, the whole notion of grouping content based on it's type is silly. I also see this always in frameworks; separate folders for scripts, stylesheets, images etc... I want to group stuff based on functionality. So the 'photo-album' package contains scripts, stylesheets, images etc for the photo album functionality. Also I don't see the principal objection about mixing these types. In fact mixing them well is what we do to build great sites. Like a chef mixing his ingredients. We don't need plates with sections on them for the different food types! :) – Stijn de Witt Dec 08 '18 at 13:56
  • 39
    To ensure a good separation of content and styling, we won't have a :contains() selector. So, instead of styling a cell based on its content (eg, a "status" column of "Open", or "Resolved"), we'll duplicate the content in both the display and the class attributes, and then select on that. Excellent! – Jon Marnock Jun 17 '19 at 06:31
  • 3
    This would be useful for uBlock Origin / Adblock Plus filters. – baptx Jul 10 '21 at 21:00
  • 1
    Yep, came here because I wanted to block certain elements with certain text via uBlock. – Thomas Jan 21 '22 at 07:45
  • 1
    I would like to add to the conversation that the reality of the situation is that the separation between styles and content in HTML markup has failed, and anyway, nowadays the separation between data and visuals is generally achieved with web frameworks such as React. – David Callanan Aug 06 '22 at 17:07
  • 1
    Did anybody polyfill this? I had a wander around GitHub without success. The philosophical objections don't seem to hold up today, seeing as we can query based on pretty much anything else whatsoever about an element, including aria labels. Matt Whipple's objections still seem quite good. But having `:contains` available would be especially good for one scenario: custom stylesheets for existing apps. – Danyal Aytekin Mar 26 '23 at 21:44
176

Using jQuery:

$('td:contains("male")')
Aliaksei Kliuchnikau
  • 13,589
  • 4
  • 59
  • 72
moettinger
  • 4,949
  • 2
  • 15
  • 20
  • 10
    Ended up needing the opposite of this, which is: jQuery(element).not(":contains('string')") – Jazzy Nov 06 '13 at 22:58
  • 480
    Except that is not CSS, that is JavaScript. – Synetech Jul 06 '15 at 22:00
  • 23
    Agreed - that's why my answer said I was using jQuery, not CSS. But that part of my answer was edited out. =) – moettinger Jul 07 '15 at 01:22
  • 33
    but the word fe*male* itself also contains the word 'male' no? is it working just 'cos male is first? Bug waiting to happen if they are re-ordered? – Michael Durrant Mar 26 '16 at 22:56
  • 11
    @Michael Durrant: You jest, but cross-element string matching (which is an even bigger problem than regular substring matches within the same element) is in fact one of the biggest reasons :contains() was dropped. – BoltClock Jun 17 '16 at 05:19
  • @MichaelDurrant You could filter out the female elements with a not after the selection. `$('td:contains("male")').not('td:contains("female")')`. Not 100% fool proof but would help greatly. – Xandor Jul 23 '19 at 20:25
  • 5
    Not CSS, not relevant to this question. – Roland Jegorov Apr 09 '20 at 08:40
  • 3
    Yes, it's not CSS, but when all the answers above say that it's not possible in CSS, it being possible in jQuery becomes relevant. Once/If there is support for all main browsers via CSS you can then say this isn't relevant – Redzarf Dec 16 '22 at 12:27
  • ity works in python BeautifulSoup too but contains replaced with -soup-contains , it saved me from a lot of trouble thanks man – urek mazino Aug 30 '23 at 13:52
171

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"] { ... }
Rudolph Gottesheim
  • 1,671
  • 1
  • 17
  • 30
Esteban Küber
  • 36,388
  • 15
  • 79
  • 97
  • 15
    It's perfectly okay to use custom data attributes with Javascript and CSS. See [MDN Using Data Attributes](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Using_data_attributes) – Michael Benjamin Oct 02 '15 at 00:00
  • 1
    I agree the data attribute is how it should be handled, BUT, a CSS rule like td.male is often much easier to set up, especially with angular which could look something like: – DJDaveMark Dec 04 '17 at 09:28
  • 1
    Gender is of course a typical example where classes would work. But what if the data is more varied than just `male` or `female`? The fact that `class` is filled with other classes, order of them is not guaranteed, there may be collissions with other class names etc makes `class` a worse place for this kind of stuff. A dedicated `data-*` attribute isolates your data from all that stuff and makes it easier to do partial matching etc on it using attribute selectors. – Stijn de Witt Dec 08 '18 at 14:17
  • There's another answer with a working code snippet which uses data attributes in a similar manner. – vhs Jan 09 '19 at 14:54
  • Why doesn't CSS provide simply a predefined attribute name for the text content? such as `-value` for innerText ? I don't see a difference between the possibility to access any possible attribute value and text value, particularly if I look at JSON where everything is like an attribute. If they are too much afraid that people try to access innerHTML, they just could make the attribute match innerText. The difference might be that innerText is visible to the user but imagine syntax highlighting with CSS! It would be so much more useful than `:empty` to match paragraphs without text. – ChrisoLosoph Aug 23 '23 at 11:52
76

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.

Matt Whipple
  • 7,034
  • 1
  • 23
  • 34
  • 5
    This is true. This is what XPath is for. If you can execute the selector in JS/jQuery or a parsing library (as opposed to CSS-only), then it would be possible to use XPath's `text()` function. – Mark Thomas Jun 07 '13 at 13:25
  • That's a good point about *content*. But how I wish you could have a `:contains()` to select an elements which contains other specific elements. Like `div:contains(div[id~="bannerAd"])` to get rid of the ad and it's container. – Lawrence Dol Mar 17 '20 at 17:49
  • 2
    @LawrenceDol, that actually arrived in some browsers in September 2022 as `:has()`. – MiniGod Nov 23 '22 at 13:54
65

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());
  });
});
toms
  • 515
  • 6
  • 23
Buksy
  • 11,571
  • 9
  • 62
  • 69
  • 3
    note that `.text()` and `.textContent` are pretty heavy, try to avoid them in long lists or large texts when possible. `.html()` and `.innerHTML` are fast. – oriadam Jul 18 '17 at 14:09
  • 2
    If you are going to use jQuery, use the contains selector: `$("td:contains(male)")` – huwiler Sep 23 '19 at 20:04
  • If the content is editable by the user (`contenteditable='true'` - my case), it's not enough to set the value of the `data-content` attribute before the page is rendered. It has to be updated continuously. Here what I use: `` – caram Apr 03 '20 at 14:31
28

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/

Gerold Broser
  • 14,080
  • 5
  • 48
  • 107
user40521
  • 1,997
  • 20
  • 8
  • 1
    this is javascript, How is this related to the question? – rubo77 Aug 11 '16 at 01:27
  • 8
    As you cant select by content in css you will have to use js to do that. While you are right where you point out that this isn't css, the use of a data attribute isn't selecting by content. We can only guess why the reader wants to select cells by content, what kind of access she has to the html, how many matches, how big the table is etc. etc – user40521 Sep 07 '16 at 16:00
  • Is there a similar function that instead of returning the elements, it will just return boolean if the content is found or not? Would like to avoid detecting `elms.snapshotLength > 0` as it first get all the elements containing same content instead of just telling immediately if the content is found or not – Michael Buen Sep 20 '21 at 06:21
  • Nevermind, just read from the documentation. Just need to pass `XPathResult.BOOLEAN_TYPE` instead of `XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE` – Michael Buen Sep 20 '21 at 06:48
10

This is in fact unrelated to OPs problem, but it solves a similar problem:

:has()

Example: The following selector matches only elements that directly contain an child:

a:has(> img)

References:

https://developer.mozilla.org/en-US/docs/Web/CSS/:has

https://caniuse.com/?search=has

ohcibi
  • 2,518
  • 3
  • 24
  • 47
den232
  • 682
  • 6
  • 14
8

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.

Esteban Küber
  • 36,388
  • 15
  • 79
  • 97
Edwin V.
  • 1,327
  • 8
  • 11
7

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.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Matas Vaitkevicius
  • 58,075
  • 31
  • 238
  • 265
  • You can use `TD:contains('male')` if you're using Selenium: imho only use it if you cannot update the source to add classes. e.g. if you are testing legacy code or your company wont let you speak to the devs (eurgh!) because your tests will be brittle and will break if someone changes the text later to `masculine` or changes language. SauceLabs docs shows use of `contains` here (https://saucelabs.com/resources/articles/selenium-tips-css-selectors) +cc @matas vaitkevicius have I missed something? – snowcode Nov 14 '19 at 09:10
7

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

for ( td of document.querySelectorAll('td') ) {
  console.debug("text:", td, td.innerText)
  td.setAttribute('text', td.innerText)
}
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
Gerold Broser
  • 14,080
  • 5
  • 48
  • 107
7

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.

cst1992
  • 3,823
  • 1
  • 29
  • 40
  • 1
    By "not the author of the website" I mean you have no real control over the content of your target website. – cst1992 Jun 03 '22 at 09:43
4

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}}">
DJDaveMark
  • 2,669
  • 23
  • 35
  • 1
    Template bindings are a great solution. But it feels a bit ick to create classnames based on the markup structure (though that's pretty much what [BEM](http://getbem.com) does in practice). – vhs Jan 09 '19 at 14:50
4

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>
Eduard Florinescu
  • 16,747
  • 28
  • 113
  • 179
3

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");
Ryan Shillington
  • 23,006
  • 14
  • 93
  • 108
2

@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>;
}
John
  • 1
  • 13
  • 98
  • 177
0

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.

EoghanM
  • 25,161
  • 23
  • 90
  • 123
-1

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;
    }
Anthony
  • 79
  • 4
  • 2
    Maybe you meant to use a `data-*` attribute? I don't think `status` is a valid attribute for ``, but I might be wrong :-) Check out this page: `https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*` – if I put the link in here it breaks due to the `*` :-( – Thomas Ebert Jan 08 '21 at 15:39
-1

If you want to apply style to the content you want. Easy trick.

    td { border: 1px solid black; }
    td:empty { background: lime; }
    td:empty::after { content: "male"; }
    <table>
      <tr>
        <td>Peter</td>
        <td><!--male--></td>
        <td>34</td>
      </tr>
      <tr>
        <td>Susanne</td>
        <td>female</td>
        <td>14</td>
      </tr>
    </table>

https://jsfiddle.net/hyda8kqz/

Arsen
  • 131
  • 1
  • 7
-3

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')
            }
        })
    })
campino2k
  • 1,618
  • 1
  • 14
  • 25
-3

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