CSS Attribute Selectors
Learn all seven CSS attribute selectors: target elements by presence, exact value, prefix, suffix, substring, and partial match to style links, inputs, and more without extra classes.
In the previous lesson on grouping and combining selectors, we looked at how combinators let you target elements by their relationship in the HTML tree. Parent, child, sibling — all very useful stuff.
But here’s a scenario none of those combinators can handle on their own: you have a bunch of <a> tags, some point to internal pages, some point to external sites, some link to PDFs. You want to style each type differently. The HTML is the same tag. No different classes on them. How do you style them separately?
That’s exactly what attribute selectors are built for.
They let you reach inside an element’s opening tag, look at its attributes (like href, type, target, lang — anything), and apply styles based on what those attributes say. It’s genuinely one of those CSS features that makes you think, “wait, I can do that without JavaScript?”
Yes. You can. Let’s go through all of it.
01What Makes CSS Attribute Selectors Worth Learning
Quick context before diving into syntax. If you’ve been following this CSS series from the beginning, you already know that type selectors and class selectors are the foundation of targeting elements. Classes are great. You add class="external-link" to every external anchor and style that class.
But that’s extra markup. Every time you add an external link, you have to remember to add the class. Every time someone else edits the HTML, they might forget. Attribute selectors read the attributes that are already there — attributes your HTML needs anyway, like href or type. No extra classes needed.
They’re also great for styling third-party HTML you don’t control, form elements that differ only by type, and language-specific content. Really practical once you know them.
02The Anatomy of an Attribute Selector
Before listing all the variations, let me show you what the syntax looks like so it doesn’t feel foreign.
[
href
^=
“https”
]
Element (optional, can omit for any element)
Square brackets wrap the condition
Attribute name you’re inspecting
Operator (how to match)
Value you’re looking for
The element part before the brackets is optional. Write a[href] to only target anchors, or just [href] to target any element that has an href attribute. The brackets hold your matching condition. Simple enough.
03All Seven CSS Attribute Selector Types, One by One
There are seven distinct attribute selector patterns in CSS. Each one matches differently. Let’s walk through every single one with proper examples.
[attr]
Attribute exists at all (any value, even empty)
[attr=”val”]
Attribute equals exactly this value
[attr~=”val”]
Value is a word in a space-separated list
[attr|=”val”]
Exact match or starts with val- (hyphen)
[attr^=”val”]
Value starts with this string
[attr$=”val”]
Value ends with this string
[attr*=”val”]
Value contains this string anywhere
1. [attr] — Does the Attribute Exist at All?
The simplest one. No operator, no value. Just the attribute name inside brackets. It matches any element that has that attribute present, regardless of what the value is.
<!-- HTML -->
<img src="photo.jpg" alt="A sunset" />
<img src="photo2.jpg" /> <!-- no alt attribute -->
/* CSS */
/* Style only images that HAVE an alt attribute */
img[alt] {
border: 2px solid #2ecc71;
}
/* Style images that are MISSING alt — accessibility warning! */
img:not([alt]) {
border: 2px solid #ff5c7a;
outline: 3px dashed #ff5c7a;
}
That second rule is actually something a lot of developers use in their dev stylesheets to visually flag images without alt text during development. Useful trick to remember.
2. [attr="value"] — Match an Exact, Precise Value
This one matches when the attribute’s value is exactly equal to what you provide. No more, no less. Case-sensitive by default.
/* Match only links that open in a new tab */
a[target="_blank"] {
color: #06b6d4;
}
/* Match only submit buttons (not reset, not button type) */
input[type="submit"] {
background-color: #6366f1;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
}
A word of caution: if the attribute value is "dark blue" (with a space), [class="dark blue"] will only match it if the entire class attribute is exactly "dark blue" — which almost never happens in practice. For class matching, the next selector is more useful.
3. [attr~="value"] — Find a Word in a Space-Separated List
This one is specifically designed for space-separated attribute values — like the class attribute, where an element can have multiple values separated by spaces. It matches when the given word appears anywhere in that list.
<!-- All three of these will match [class~="card"] -->
<div class="card">Single class</div>
<div class="card featured">Two classes</div>
<div class="hero card dark">Three classes</div>
<!-- This one will NOT match — "card" isn't a standalone word here -->
<div class="card-featured">Hyphenated, won't match</div>
[class~="card"] {
padding: 16px;
border-radius: 12px;
background: #1e293b;
}
Honestly, for class targeting, you’d normally just use .card (the class selector). But ~= becomes very useful for custom data attributes where you store multiple values, like data-tags="css design layout".
4. [attr|="value"] — The Language Code Selector
This one is niche but worth knowing. It matches either an exact value, or a value that starts with value- (with a hyphen right after). The hyphen is part of the condition.
<!-- Both of these match [lang|="en"] -->
<p lang="en">English paragraph</p>
<p lang="en-US">American English</p>
<p lang="en-GB">British English</p>
<!-- This does NOT match -->
<p lang="english">No match — no hyphen</p>
/* Style all English content regardless of dialect */
[lang|="en"] {
font-family: Georgia, serif;
}
/* Style all French content, en-FR, fr-CA etc. */
[lang|="fr"] {
font-family: "Palatino Linotype", serif;
}
You’ll see this most in internationalized websites where content can be in lang="fr-CA" or lang="fr-BE" and you want to catch all French variants with one selector.
5. [attr^="value"] — Value Starts With This
The caret symbol (^) comes from regex, where it means “start of string”. Here, it matches when the attribute value begins with your given string.
/* Style all external links (they start with https) */
a[href^="https"] {
color: #06b6d4;
}
/* Style telephone links */
a[href^="tel:"] {
color: #2ecc71;
}
/* Style email links */
a[href^="mailto:"] {
color: #ec4899;
}
/* Target all images from a specific CDN */
img[src^="https://cdn.mysite.com"] {
border-radius: 8px;
}
This one gets used a lot. The “style external links differently” use case is probably the most common in real projects.
6. [attr$="value"] — Value Ends With This
The dollar sign ($) also comes from regex — end of string. This matches when the attribute value ends with your given string. Super useful for targeting files by their extension.
/* Add a PDF icon after links to PDF files */
a[href$=".pdf"]::after {
content: " (PDF)";
font-size: 0.75em;
color: #f9a8d4;
}
/* Style links to zip files */
a[href$=".zip"]::after {
content: " ⬇";
}
/* Target PNG images specifically */
img[src$=".png"] {
image-rendering: crisp-edges;
}
/* Style docx downloads */
a[href$=".docx"]::after {
content: " (Word)";
font-size: 0.75em;
color: #86efac;
}
Those ::after pseudo-elements are coming in the next batch, but the basic idea is that they let you inject small text after the element’s content. Perfect for labeling file types.
7. [attr*="value"] — Value Contains This Anywhere
The asterisk means “contains”. If your string appears anywhere inside the attribute value — at the start, the middle, the end, doesn’t matter — this selector catches it.
/* Match any image hosted on imgur anywhere in the URL */
img[src*="imgur"] {
border: 2px solid #6366f1;
}
/* Match links going to any Google service */
a[href*="google.com"] {
opacity: 0.85;
}
/* Highlight inputs whose name contains "password" */
input[name*="password"] {
border-color: #ec4899;
background: rgba(236, 72, 153, 0.06);
}
*= is the most “greedy” of all the attribute selectors. Since it matches any substring, it can accidentally catch things you didn’t intend. For example, [href*="cat"] would match href="education.html" because “cat” is hiding inside “education”. Be specific with your strings.
04The Case-Insensitive Flag: Add an i at the End
By default, attribute selector matching is case-sensitive. So [type="TEXT"] won’t match type="text".
But you can make any attribute selector case-insensitive by adding a space and the letter i just before the closing bracket:
/* Without i: only matches type="text" exactly */
input[type="text"] { ... }
/* With i: matches type="text", type="TEXT", type="Text" */
input[type="text" i] {
border: 2px solid #6366f1;
}
/* Useful for file extensions (could be .PDF or .pdf) */
a[href$=".pdf" i]::after {
content: " (PDF)";
color: #f9a8d4;
}
The file extension case is actually where this shines most. Users and content management systems often upload files with inconsistent casing. Adding i means you don’t have to write three separate rules for .pdf, .PDF, and .Pdf.
05Three Real-World Uses That Make Attribute Selectors Worth Knowing
Theory is good. Let’s look at actual code you’d write in a real project.
1. Styling Links by Their href: No Extra Classes Needed
This is probably the most cited use case. A typical page has links going in all directions: internal pages, external sites, email addresses, phone numbers, PDFs. You want visual cues for each type so users know what they’re clicking.
/* Default link style */
a {
color: #a5b4fc;
text-decoration: underline;
}
/* External links: open-in-new-tab icon via ::after */
a[href^="http"]::after,
a[href^="https"]::after {
content: " ↗";
font-size: 0.75em;
opacity: 0.7;
}
/* Email links */
a[href^="mailto:"] {
color: #ec4899;
}
/* Phone links */
a[href^="tel:"] {
color: #2ecc71;
}
/* PDF links */
a[href$=".pdf"] {
color: #f9a8d4;
}
a[href$=".pdf"]::after {
content: " (PDF)";
font-size: 0.75em;
}
Each link type gets its own visual identity without you adding a single extra class to your HTML. The attributes are already there doing their job — you’re just reading them.
2. Styling Form Inputs by Type
This is where CSS attribute selectors really save time. A form can have text inputs, email inputs, password inputs, checkboxes, radio buttons, file uploads — and they’re all <input> tags differentiated only by their type attribute.
/* Base styles for all inputs */
input {
padding: 10px 14px;
border-radius: 8px;
border: 1.5px solid #334155;
background: #1e293b;
color: #ffffff;
font-size: 0.9rem;
}
/* Text inputs: indigo accent */
input[type="text"],
input[type="search"] {
border-color: #6366f1;
}
/* Email: cyan accent */
input[type="email"] {
border-color: #06b6d4;
}
/* Password: pink accent, spaced out characters */
input[type="password"] {
border-color: #ec4899;
letter-spacing: 0.15em;
}
/* Required inputs: subtle indicator */
input[required] {
border-left: 3px solid #f59e0b;
}
/* Disabled inputs: muted */
input[disabled] {
opacity: 0.4;
cursor: not-allowed;
}
Compare this to the alternative: adding classes like input-text, input-email, input-password to every single input in every form on your site. With attribute selectors, the type attribute that already exists does the work.
3. Auto-Flagging External Links for Accessibility and UX
If your site links out to external resources, it’s good UX (and often an accessibility consideration) to warn users that a link will open in a new tab or leave the site. Attribute selectors let you do this automatically.
<!-- No classes needed at all -->
<a href="https://mdn.mozilla.org" target="_blank" rel="noopener">MDN Docs</a>
<a href="/about">About Us</a>
<a href="https://github.com" target="_blank" rel="noopener">GitHub</a>
/* Only links that are both external AND open in a new tab */
a[href^="http"][target="_blank"] {
padding-right: 16px;
position: relative;
}
a[href^="http"][target="_blank"]::after {
content: "↗";
position: absolute;
right: 2px;
top: 0;
font-size: 0.7em;
color: #06b6d4;
}
/* Internal links get no icon — their href doesn't start with http */
a[href^="/"]:not([target]) {
color: #a5b4fc;
}
Notice how that first selector combines two attribute conditions in a row: a[href^="http"][target="_blank"]. Let me talk about that properly.
06Stacking Multiple Attribute Selectors Together
You can chain attribute selectors. Write them one after the other with no space between, and the selector only matches elements where all conditions are true simultaneously.
/* input that is BOTH type email AND required */
input[type="email"][required] {
border-color: #f59e0b;
}
/* anchor that starts with https AND has rel="noopener" */
a[href^="https"][rel="noopener"] {
text-decoration-color: #06b6d4;
}
/* img that has both alt AND a title attribute */
img[alt][title] {
cursor: help;
border-bottom: 1px dotted #64748b;
}
/* Any element that has data-theme="dark" AND data-size="large" */
[data-theme="dark"][data-size="large"] {
padding: 24px;
background: #0f1729;
}
You can chain as many as you need. There’s no limit. The specificity goes up with each selector you add, which is the same rule we covered in the CSS cascade article — more specific selectors beat less specific ones.
input[type="text"][required] has a specificity of (0, 2, 1) — two attribute conditions plus one type selector. Keep this in mind if you ever wonder why one rule is overriding another.
07Attribute Selectors vs. Classes: When to Reach for Which
This comes up a lot. The answer isn’t “always use attribute selectors” or “never use them.” It depends on what you’re working with.
Use attribute selectors when:
- The attribute you want to target is already in the HTML for functional reasons (like
href,type,target,lang,disabled,required). - You’re working with content from a CMS, database, or third-party source where you can’t add classes.
- You want to enforce styling rules based on what the attribute actually says, not what class someone happened to add.
- You’re using
data-*attributes for component state (likedata-state="open"on a dropdown).
Use class selectors when:
- There’s no existing attribute that reliably identifies your component — you’re just styling a card, a button variant, a utility class.
- You need maximum browser support and simplest specificity to manage.
- You’re working in a CSS framework or component system where classes are the agreed pattern.
In a real codebase, you’ll use both. Classes for your own UI components, attribute selectors for reading HTML that’s already meaningful.
08CSS Attribute Selector Quick Reference
| Selector | Matches when… | Common use case |
|---|---|---|
| [attr] | Attribute exists (any value) | Flag missing alt on images |
| [attr=”val”] | Exact full match | Target specific input types, target=”_blank” |
| [attr~=”val”] | Word in space-separated list | Match one word in multi-value attributes |
| [attr|=”val”] | Exact or starts with val- (hyphen) | Language code variants (en, en-US, en-GB) |
| [attr^=”val”] | Value starts with string | Style external links (href^=”https”) |
| [attr$=”val”] | Value ends with string | Style by file type (href$=”.pdf”) |
| [attr*=”val”] | Value contains string anywhere | Match partial URLs or attribute fragments |
| [attr=”val” i] | Case-insensitive version of any above | Match .pdf, .PDF, .Pdf with one rule |
09One Full Example Putting It All Together
Here’s a stylesheet that uses several attribute selectors to style a page’s links and form — no extra classes anywhere:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<nav>
<a href="/home">Home</a>
<a href="https://github.com" target="_blank" rel="noopener">GitHub</a>
<a href="https://mdn.mozilla.org" target="_blank" rel="noopener">MDN</a>
</nav>
<form>
<input type="text" placeholder="Your name" required />
<input type="email" placeholder="Email address" required />
<input type="password" placeholder="Password" />
<input type="text" placeholder="Optional notes" />
<input type="submit" value="Send it" />
</form>
<p>Download our <a href="/report.pdf">annual report</a> or
<a href="/assets.zip">asset package</a>.</p>
</body>
</html>
/* ── Internal nav links ── */
nav a[href^="/"] {
color: #a5b4fc;
text-decoration: none;
font-weight: 500;
}
/* ── External links with ↗ indicator ── */
a[href^="http"]::after {
content: " ↗";
font-size: 0.75em;
color: #06b6d4;
}
/* ── PDF and ZIP download links ── */
a[href$=".pdf" i]::after {
content: " (PDF)";
color: #f9a8d4;
font-size: 0.8em;
}
a[href$=".zip"]::after {
content: " ⬇";
font-size: 0.8em;
}
/* ── Form inputs ── */
input[type="text"] {
border-color: #6366f1;
}
input[type="email"] {
border-color: #06b6d4;
}
input[type="password"] {
border-color: #ec4899;
}
/* ── Required inputs get a warm left border ── */
input[required] {
border-left: 3px solid #f59e0b;
}
/* ── Submit button ── */
input[type="submit"] {
background: #6366f1;
color: white;
border: none;
padding: 10px 22px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
Look at how much work that CSS is doing without a single extra class in the HTML. The structure of the HTML itself — the type attributes, the href values, the required flag — that’s all the information the CSS needs.
10What’s Coming Next
We’ve now covered four out of four topics in the “selector fundamentals” part of this series: type and class selectors, combinators, attribute selectors (this one), and next up: pseudo-classes.
Pseudo-classes are where CSS starts to feel almost magical. Things like :hover, :focus, :nth-child(), :not() — they let you style elements based on their state or position in the page, not just what they are. If you’ve ever wondered how dropdowns highlight on hover or how forms turn red when invalid — that’s pseudo-classes.
Attribute selectors are one of those tools that you don’t use in every single stylesheet, but when you need them, nothing else does the job as cleanly. Once you start noticing attributes in your HTML, you’ll start finding places to use this. That’s the sign you’ve actually learned it.
CSS Attribute Selectors
Loading challenge…
Ready to Practice?
Select a difficulty level above to load your challenge.