Virtual nodes, list markers
Pseudo-Elements
Virtual selectors that target elements that "do not
physically
exist"
in the HTML code but are "conjured out of thin air"
by CSS,
or target "specific parts" of a whole.
Below, we will examine pseudo-elements in detail.
CSS Pseudo-Elements Targeting the Invisible: States, Interactions, and Virtual Parts
CSS Pseudo-Element Map (By Usage Areas) Typical learning / implementation flow from left to right; each box carries a different color code
First, you add virtual content to the box and fragment the text; then comes the top layer and media; and finally, the shadow bridges. The arrow direction indicates the recommended reading order.
Typography and selection
Modal / fullscreen curtain
Video / WebVTT
Slot content & part API
- 1 · Content #34d399
- 2 · Text #818cf8
- 3 · Top Layer #fbbf24
- 4 · Media #f472b6
- 5 · Shadow #a78bfa
::-webkit-scrollbar* and similar prefixed
selectors
are non-standard; they only work in specific engines — shown as a separate warning on the map.
While the word "Pseudo" etymologically means "fake", "artificial", or "imitation"; in the world of CSS, this term refers to one of the most powerful mechanisms of design that materializes abstract concepts that do not physically exist in the HTML code.
Standard selectors (Tag, Class, ID) target static and immutable "entities" on the page; whereas Pseudo selectors target the "states" of these entities over time or their geometric "parts".
A web page is not a static painting; it is a living, breathing, interactive environment in constant dialogue with the user.
Pseudo structures allow the developer to give logical commands to the browser without touching the HTML skeleton or writing extra JavaScript code.
The answers to questions like "What should happen when the user hovers over this box?", "How should every third element of the list look?", or
"How should the first letter of a text be emphasized?" are hidden within these selectors.
In technical architecture, this concept is divided into two main disciplines:
Historical Evolution: Single Colon vs. Double ColonDuring the CSS2 era, a single colon (:before) was used for both classes and elements.
However, with CSS3, the double colon ( ::before ) standard was introduced to clearly distinguish the concepts of "State" and "Fragment".
While browsers still support the single colon for backward compatibility, the modern and correct syntax is the double colon.
Invisible Architecture: Shadow DOM Pseudo-elements are actually created in a special layer of the browser called the Shadow DOM.
This is why you cannot select these virtual parts when you say document.querySelector('.element') with JavaScript; they are ghosts that only CSS can see.
Ending HTML Pollution Adding empty <span> or <div> tags to HTML just for visual decoration (an icon, a line, or quotation marks) pollutes your code and can confuse screen readers.
Pseudo-elements shift this decorative burden from HTML to CSS, separating your content from your presentation.
Critical Rule: The "Content" Requirement For a pseudo-element (::before or ::after) to exist on the screen, the
content: ''; property must be defined within the CSS.
If this property is missing, even if the value is empty, the browser will not generate that element; this is the "Life Energy" of pseudo-elements.
Virtual Building Blocks Pseudo-Elements
If the "Pseudo-Classes" we just examined change an element's mood, then "Pseudo-Elements" add new limbs to that element, break it into parts, or apply its makeup.
This is the sole way to add aesthetic details, icons, or typographic decorations to a page using only CSS without touching the HTML code.
These do not physically exist in the DOM tree; the browser creates them virtually during the "render" phase.
Logic of "Ghost" ElementsIn web development, "Content" and "Presentation" must be separated by distinct lines.
A web page consists of a skeleton carrying the data and the clothing draped over it.
Pseudo-elements exist to preserve this principle.
For Example: If you want to place a decorative "star icon" next to a heading, adding it as a physical tag (using an empty <span>) means "polluting" the code.
Because that star has no semantic value; it is merely an ornament.
Pseudo-elements allow you to add visuals to the page as if an extra element were there, while keeping your code pristine.
Behind the Technical Curtain: DOM and Render TreeWhen your web browser opens a page, a complex construction process occurs in the background within seconds.
Understanding this process helps us realize why Pseudo-Elements are "pseudo" and why they exist on the screen even though they are invisible in the source file.
We can compare this relationship to that of an "Architect and Interior Designer."
DOM Tree (Document Object Model): The Architect's BlueprintThe DOM is the "absolute reality" of a web page.
It is the version of your code read by the browser and transformed into a hierarchical family tree.
What it Sees: It only sees tags (div, p, span) and the text within them.
Its Duty: To create the skeleton and semantic structure of the page.
Its Limit: The DOM does not understand colors, sizes, or decorations.
It is merely a technical report stating, "There is a heading here, with a paragraph below it."
Render Tree (Visualization): The Interior Designer's WorkThe Render Tree is the "final visual product" created by draping style rules over the architect's blueprint.
The browser takes the skeleton from the DOM, reads the rules in the file, and decides what and how to draw on the screen.
What it Does: It filters out invisible elements (display: none) and paints the rest.
Its Duty: To create the final image that appeals to the user's eye.
The "Ghostly" Nature of Pseudo-ElementsPseudo-Elements (::before, ::after) emerge magically right in the gap between these two stages.
Absent in the DOM: If you look at your code, you won't see these elements.
Because they don't come from the database, and they occupy no physical space between tags.
Born During Rendering: The moment the browser begins painting the page (as the Render Tree forms), the file kicks in and whispers to the browser:
"Yes, it's not in the blueprint, but draw a red box next to that heading anyway."
Why Do We Use Double Colons (::)?In the early years of CSS, both state-declaring classes ( :hover ) and virtual elements ( :before ) were written with a single colon.
However, in modern standards, an important syntax rule was introduced to distinguish these two concepts:
Single Colon (:): Denotes a state or logical sequence.
Double Colon (::): Denotes a geometric part or a virtual object.
This distinction allows a developer to answer the question, "Is this an event or a component?" in seconds just by looking at the code.
The "Invisible" Workers of DesignPseudo-elements are the "makeup artists" of modern web design.
They work everywhere: from massive quotation marks at the start of a blockquote to custom bullet icons in lists, from color filters over photos to invisible walls holding complex layouts together.
While providing rich visuals to the user, they keep the pure text scanned by search engines and screen readers simple and clear.
From DOM to Layout: Generated Pseudo Boxes ::before / ::after — Not DOM children; boxes generated by the browser (sample schema)
The diagram below illustrates only the most frequently used pair. These boxes are not written as siblings in the HTML; the style engine generates additional layout boxes during calculation — a mechanism distinct from the shadow tree in the Shadow DOM.
::marker, ::first-line,
::selection, ::backdrop …) operate on different targets; the same
"before / after
content box" model is not always applicable.
Tag + text nodes; ::before does not exist here.
Typical arrangement for top-to-bottom painting / hit-test order.
::before and ::after boxes reside
within the padding boundary — inside the formatted box of the actual content —
and in most
cases, are ordered before and after within the content area flow alongside the
text.
The margin and border lines remain outside these generated boxes; they are the element's own
margin/border.
querySelector. Think of them as boxes "standing next to" the content visually,
rather than
a parent-child relationship.
The Creative Twins of Design and Virtual Layers ::before and ::after:
These two are the most frequently used and versatile tools in the CSS world, often referred to as the designer's "Swiss Army Knife."
Although their names translate to "before" and "after," these words do not represent a temporal sequence, but rather a spatial position.
They are virtual extensions and visual prosthetics added to an HTML element without writing additional code.
When building a web page, sometimes the available HTML tags ( div, p, span ) fall short of realizing your design dreams.
This is exactly where ::before and ::after come into play.
By infiltrating an existing tag, these structures create "virtual real estate" at the start and end points of that tag.
These areas never appear in your file's source code.
They are not stored in the database and cannot be copy-pasted.
They are purely visual illusions created by CSS during the browser's "painting" phase.
Thanks to these features, they allow you to decorate the page like a painter, equip it with icons, or add complex geometric shapes without touching the information-carrying skeleton.
In short; if HTML tags are the bricks and walls of a house, ::before and ::after are the wallpaper, paintings, and decorative lighting of that house.
They remain invisible in the house's structure ( architecture ), but they completely transform its atmosphere.
The Golden Rule: "content" is MandatoryFor these virtual elements to work, the content property acts as the "ignition key."
Even if you write everything else ( width, color, position ) perfectly in your CSS block, if you don't include the content: ''; line ( even if it's empty ), the browser will refuse to generate the element.
The Positioning Paradox: Inside or Outside? Because of their names, many developers assume these elements are placed outside the targeted box; however, this is a critical misconception.
::before is placed immediately after the opening tag; ::after is placed immediately before the closing tag.
So technically, both are children of the targeted element and live within the parent's boundaries.
Default Behavior: The "Inline" Trap When created, these elements have a default display: inline; property.
Therefore, they will not respond even if you give them width or height.
To shape them like boxes, you must define display: block; or display: inline-block;, or break them out of the flow with
position: absolute;.
Virtual Container Logic Hidden Compartments Within
The best way to understand these elements is to imagine every HTML tag as a box ( parcel ).
Under normal conditions, this box only contains the text or images you have written. ::before and ::after are extra, invisible compartments placed inside this box without touching the existing content.
Technically, these virtual elements behave like children ( children ) of the main element.
However, they are like "stepchildren"; they are invisible in the HTML family and are only recognized by the CSS family.
The "Inline" NatureAt the moment they are created, these virtual boxes behave as if they were single letters or words. This essentially means:
In technical terms, they possess the display: inline property.
For instance, if you add a red box inside a <div> using ::before, this box will not appear above the text; it stands immediately to the left of the text, on the same line.
If you wish to give them dimensions (width/height) or move them independently of the parent element, you must explicitly tell them, "You are now a block" (display: block).
|
Stage
|
Description
|
|---|---|
|
Main Element Start (Start Tag)
|
The initial moment the browser begins
constructing
the element and technically opens the virtual container's lid.
|
|
::before (Virtual Vanguard)
|
The first piece virtually injected at
the very
beginning of the content, before actual HTML text or images are rendered on
screen.
|
|
Actual Content
|
The central area where the real text or images provided between the HTML tags—originating from code or databases—are placed. |
|
::after (Virtual Rearguard)
|
The final piece that triggers
immediately after
the actual content ends, but just before the closing tag appears.
|
|
Main Element End (End Tag)
|
The boundary where the element's limits are drawn, the container is closed, and CSS styling for that specific scope concludes. |
The "Content" Rule The Key to Digital Existence
According to the operational principles of web browsers, an element must possess a physical presence or data to be rendered on the screen.
Since pseudo-elements (::before and ::after) are not inherently present in the HTML code, you must prove their existence to the browser.
The content property is that very evidence.
You can think of this rule as the "life signal" of the pseudo-element.
No matter how much width, height, background color, or shadow you define in your CSS file; if you do not include the content line, the browser will treat those definitions like an "unclaimed letter" and discard them.
Before generating a pseudo-element, the browser engine first checks this line: "Is there content? No. Then do not create this element."
Generating Visual Boxes with Empty ContentThe most common usage, content: ""; (empty double quotes), technically represents an "empty string."
This sends the following message to the browser: "Place a box here; it won't contain text, but the box itself shall exist."
Through this, you can create decorative objects that aren't filled with text but can be visually shaped (like squares, circles, or lines).
Remember: something without content cannot have an appearance.
Therefore, the content property is the ground zero where design begins.
Preventing HTML Pollution Semantic Integrity
In web development, "Clean Code" is not just about code looking pretty; it's about separating functions into the correct layers.
While HTML handles what a web page is (heading, paragraph, list), CSS manages how it looks.
When these two disciplines intertwine, it leads to "structural pollution," which reduces project sustainability and quality.
In older web development practices, empty <div> or <span> tags were often added inside the HTML just to draw a decorative line under a heading or place a decorative icon in the corner of a box.
This method bloats the HTML file with meaningless tags and ruins code readability, much like scattering random letters across the pages of a book.
Moving Decorative Content to CSS (::before / ::after)The ::before and ::after pseudo-elements provide a definitive solution to this problem.
By completely abstracting decorative elements from the HTML structure, they move them where they belong: the CSS layer.
Consequently, your HTML code remains a "semantic" structure that only holds pure data and content.
Google bots crawling your page or screen readers working for visually impaired users ignore these virtual decorations created with CSS and focus directly on the content itself.
This ensures that no matter how fancy the design is, the information hierarchy and accessibility remain intact.
Usage Scenarios The Unsung Heroes of Design
In modern web design, pseudo-elements are not just a technical necessity; they are tools that dismantle barriers to creativity.
The touches that rescue an interface from looking like a "flat document" and add character and depth are usually achieved through this duo.
The human brain processes visuals much faster than text.
Therefore, seeing a phone receiver next to a "Contact" button or a small arrow at the end of an external link accelerates the user experience.
::before and ::after allow you to position these icons as if they were an intrinsic part of the text.
Instead of embedding extra <img> or <i> tags within your HTML, you keep your code clean by calling the icon via CSS.
The icon grows, shrinks, and changes color along with the text; in other words, it moves in perfect biological harmony with the typography.
Decorative Atmosphere and GeometryBackground shapes used to add depth to web pages, elegant lines under headings, or colorful ribbons on cards...
Most of these are actually the handiwork of ::before or ::after.
For Example: Instead of adding a blurry, abstract circle behind a section as an "empty box" in the HTML, you create it as a pseudo-element.
This way, you create a visual atmosphere without compromising the semantic structure of the page.
Almost everything designers do for the sake of "decoration" is entrusted to these virtual layers.
Typographic Richness: Quotation MarksThat classic aesthetic derived from magazine and book design; specifically, the massive, elegant quotation marks at the start and end of a <blockquote>...
These marks are not actually characters belonging to the text itself; they are "framing" elements.
If you type them manually, they get copied when a user selects the text, leading to data pollution.
However, when added via ::before and ::after, they remain purely as visual accessories; they make the text pleasant to read without interfering with the actual data.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Tooltip - ::before</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<div class="container">
<h2 class="title">
Modern Tooltip <span>::before</span>
</h2>
<button class="info-btn" data-tooltip="This action cannot be undone.">
Hover Me
</button>
</div>
</body>
</html>
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: 'Inter', 'Segoe UI', sans-serif;
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: radial-gradient(circle at 20% 20%, #1e293b, #020617);
}
/* layout */
.container {
text-align: center;
}
/* 🔥 modern heading */
.title {
font-size: 32px;
font-weight: 700;
margin-bottom: 40px;
color: #e2e8f0;
position: relative;
display: inline-block;
}
/* gradient text */
.title span {
background: linear-gradient(135deg, #6366f1, #22d3ee);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* underline accent */
.title::after {
content: "";
position: absolute;
left: 50%;
bottom: -10px;
transform: translateX(-50%);
width: 60%;
height: 3px;
background: linear-gradient(90deg, #6366f1, #22d3ee);
border-radius: 10px;
opacity: 0.7;
}
/* 🔥 modern button */
.info-btn {
position: relative;
padding: 14px 32px;
font-size: 16px;
border: none;
border-radius: 12px;
background: rgba(99, 102, 241, 0.15);
color: #e0e7ff;
backdrop-filter: blur(10px);
border: 1px solid rgba(99, 102, 241, 0.3);
cursor: pointer;
transition: all 0.3s ease;
}
/* hover depth */
.info-btn:hover {
transform: translateY(-2px) scale(1.02);
background: rgba(99, 102, 241, 0.25);
box-shadow: 0 10px 30px rgba(99, 102, 241, 0.25);
}
/* 🔥 tooltip using attr() function */
.info-btn::before {
content: attr(data-tooltip);
position: absolute;
bottom: 130%;
left: 50%;
transform: translateX(-50%) translateY(10px);
background: rgba(15, 23, 42, 0.95);
color: #cbd5f5;
padding: 10px 16px;
border-radius: 10px;
font-size: 13px;
letter-spacing: 0.3px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
box-shadow: 0 15px 40px rgba(0,0,0,0.5);
backdrop-filter: blur(8px);
transition: all 0.25s ease;
}
/* tooltip arrow */
.info-btn::after {
content: "";
position: absolute;
bottom: 115%;
left: 50%;
transform: translateX(-50%) translateY(10px);
border-width: 6px;
border-style: solid;
border-color: rgba(15, 23, 42, 0.95) transparent transparent transparent;
opacity: 0;
transition: all 0.25s ease;
}
/* hover triggers */
.info-btn:hover::before,
.info-btn:hover::after {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
Typographic Beginning / Drop Cap ::first-letter
Recall Medieval manuscripts or old fairy tale books.
The first letter of that opening sentence, "Once upon a time...", was often drawn as a massive, ornate, and sometimes illustrated masterpiece.
In graphic design literature, this is known as a "Drop Cap."
::first-letter carries this aesthetic legacy into modern web pages.
It allows you to practice this art using only CSS, without the need to wrap that specific letter in a separate <span> within your HTML.
Visual Anchor and Reading PsychologyUsers do not read web pages word for word; they scan them first.
In a long article or a dense block of grey text, the eye needs a branch to hold onto.
A stylized first letter acts as a powerful visual anchor that tells the reader, "This is the starting point; begin reading here."
It breaks the monotony of the text and adds an editorial, professional feel to the content.
Block Element Requirement: The most important technical rule for this selector is that ::first-letter can only be applied to block-level elements.
This means it works on paragraphs ( <p> ), headings ( <h1> ), or boxes (<div>); however, it will not work on inline elements (<span>, <a>).
This is because the concept of a "first letter" is directly related to the paragraph flow and line structure.
Technical Reason: Why the Restrictions?The primary reason for these restrictions lies in how browsers process text.
Text is rendered in invisible strips called line boxes.
If ::first-letter allowed properties that radically change page layout (such as flexbox or grid), the paragraph's line structure would shatter, rendering the text unreadable.
Therefore, browsers operate on the principle: "Style the letter alone, but do not disrupt the text flow."
Rule Stays Intact Even as Content ChangesThe beauty of this selector being "pseudo" lies in its dynamism.
If your content management system changes the text, the first letter of the new text automatically inherits this style.
Letter "A" goes, letter "B" arrives, and your design scales perfectly without breaking.
This is a prime example of sustainable design.
Limited Styling Freedom: Creativity Within BoundariesWhen using ::first-letter, you find yourself not on an infinite canvas, but in a playground with set rules.
Browsers have placed strict limits on the properties you can use on this virtual element to preserve text flow and line integrity.
You cannot use every style applicable to a div tag (like layout rules such as display: flex or position: absolute).
However, the permitted properties are more than enough to create a typographic masterpiece.
Typographic Manipulation (Font Properties)This is the strongest weapon in this field.
You can not only increase the font-size, but also change its character.
For example, if your text is set in a modern Sans-Serif font, you can achieve a wonderful contrast by transforming just the first letter into a classic font ( font-family: Serif ).
You can also increase visual weight by making it bold (font-weight) or italic (font-style).
Visual Weight and Contrast (Color and Background)The fastest way to separate the first letter from the rest of the text is color.
With the color property, you can paint the letter in a vibrant color completely different from the text.
Going further, you can assign a background color or even apply an image.
This technique makes the letter look like an embedded "icon" or "graphic element" rather than just a typeface character.
Breathing Room and Framing (Box Model)The enlarged letter must not overlap other characters or crowd them too tightly.
This is where the Box Model comes in.
By using margin, you can push surrounding words away to create dedicated space.
By using padding and border, you can encase the letter in a box and draw an elegant frame around it.
This creates the "boxed initial" effect often seen in magazine layouts.
Depth and Texture (Text Effects): You can use text-shadow to give the impression that the letter is rising off the page.
Correct use of shadows instantly makes a flat design three-dimensional.
However, note that these properties only affect the letter itself; they do not disrupt the rest of the paragraph's line structure.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Drop Cap - ::first-letter</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<div class="article">
<h2 class="title">Editorial Drop Cap</h2>
<p class="text">
In modern web design, typography is not just about readability, but also about the experience.
Vividly emphasizing the first letter grabs the reader's attention and gives the content a
professional introduction. This technique revives the classic magazine aesthetic in the digital world.
</p>
</div>
</body>
</html>
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 40px;
font-family: 'Inter', 'Segoe UI', sans-serif;
background: radial-gradient(circle at top, #0f172a, #020617);
color: #e2e8f0;
display: flex;
justify-content: center;
}
/* container */
.article {
max-width: 700px;
}
/* heading */
.title {
font-size: 28px;
margin-bottom: 20px;
color: #cbd5f5;
}
/* paragraph */
.text {
font-size: 17px;
line-height: 1.8;
color: #cbd5f5;
}
/* 🔥 MAGIC: ::first-letter */
.text::first-letter {
float: left;
font-size: 70px;
line-height: 1;
font-weight: 700;
margin-right: 14px;
margin-top: 6px;
padding: 10px 16px;
border-radius: 12px;
background: linear-gradient(135deg, #6366f1, #22d3ee);
color: white;
box-shadow:
0 10px 25px rgba(99, 102, 241, 0.4),
inset 0 0 10px rgba(255, 255, 255, 0.2);
backdrop-filter: blur(6px);
}
The Power of the First Line / Lead Line ::first-line
This pseudo-element targets only the first line of a block-level element.
However, the concept of the "first line" here is not static; it is dynamic.
As screen sizes change, windows narrow, or font sizes increase, the number of words fitting into that first line shifts.
::first-line adapts to this in real-time, styling only the words currently positioned at the top.
Reading Experience: Making the first sentence or line font-weight: bold or text-transform: uppercase in article introductions makes it easier for the reader to scan the content.
It sends a message to the user: "Something important starts here; focus."
Permitted PropertiesMuch like ::first-letter, this element supports a limited set of styles.
Generally, font settings, colors, and backgrounds can be modified; however, box model properties like margin or padding may not behave consistently across all browsers.
Technical Constraint: The "Block" Requirement The ::first-line selector only functions within "block-level" containers (p, div, article).
If you apply this style to an inline element like a span tag, the browser will ignore it.
This is because inline elements technically do not have "lines" of their own; they are the line itself.
Hierarchy and Layering What happens if both ::first-letter and ::first-line are defined for the same paragraph?
The first letter is technically inside the first line. However, in the CSS hierarchy, ::first-letter is more specific and takes precedence.
Thus, if you make the first line blue and the first letter red, the first letter remains red while the rest of the line becomes blue. The first letter does not inherit the first line's style; it overrides it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lead Line - ::first-line</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<div class="article">
<h2 class="title">Lead Line Highlight</h2>
<p class="text">
In modern content design, the first line is the most critical area for capturing the
reader's attention. When this area is highlighted correctly, the user adapts to the
content faster, and the reading experience becomes more fluid. This technique is
frequently used, especially in blog and editorial designs.
</p>
</div>
</body>
</html>
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 40px;
font-family: 'Inter', 'Segoe UI', sans-serif;
background: radial-gradient(circle at top, #0f172a, #020617);
color: #e2e8f0;
display: flex;
justify-content: center;
}
/* container */
.article {
max-width: 720px;
}
/* heading */
.title {
font-size: 28px;
margin-bottom: 20px;
color: #cbd5f5;
}
/* paragraph */
.text {
font-size: 17px;
line-height: 1.8;
color: #cbd5f5;
}
/* 🔥 MAGIC: ::first-line */
.text::first-line {
font-weight: 600;
letter-spacing: 0.4px;
background: linear-gradient(90deg, rgba(99,102,241,0.25), rgba(34,211,238,0.15));
color: #e0e7ff;
/* subtle glow */
text-shadow: 0 0 8px rgba(99,102,241,0.3);
}
User Selection and Branding ::selection
The ::selection pseudo-element targets the portion of text that a user has "highlighted" by dragging the mouse cursor or, on mobile, by long-pressing with a finger.
This is the moment of most intense interaction with the content; selecting text usually signals an intention to copy, research, or read more carefully.
Under normal circumstances, the text selection color (usually that classic blue) is determined by the browser or the operating system.
The ::selection selector allows you to take control of this system-level behavior and tell the browser: "No, on my site, the selection color will be my brand's color, not the OS default blue."
Escape from the Standard: By default, browsers show selected text as "white text on a blue background."
This standard is safe, but it is uninspiring.
Brand Identity: By using ::selection, you can transform this color into your corporate identity (e.g., black text on a yellow background).
Elegance in Details: When a user encounters your brand colors while trying to copy text, it sends a powerful subconscious message about how meticulously the site has been designed.
It proves that "the experience continues even where the design technically ends."
Accessibility Warning: The contrast ratio of the chosen colors is vital for text readability.
Only color, background-color, and text-shadow properties can be modified here.
Pro Tip: Shadow CleanupIf your normal text has a text-shadow as a visual effect, this shadow often ruins readability during selection and creates a "muddy" look.
The best practice is to reset the shadow upon selection: ::selection { text-shadow: none; }. This ensures the selected text appears razor-sharp and clear.
Scoped CustomizationThe selection color does not have to be uniform across the entire site.
If your site has a "Dark Mode" section with a black background, you might use a yellow selection color there, while using purple for the section with a white background.
For Example: By defining p.dark-theme::selection { ... }, you can make the customization specific only to that paragraph or section.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Branded Selection - ::selection</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<div class="article">
<h2 class="title">Custom Selection Experience</h2>
<p class="text">
Try selecting this text. In modern web design, every moment of user interaction matters.
Even text selection can be a part of the brand identity. With the right colors and contrast,
you can provide the user with a cleaner and more professional experience.
</p>
<p class="text highlight">
This area, however, has a different selection style. Thanks to scoped selection,
you can create different user experiences in various sections of the page.
</p>
</div>
</body>
</html>
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 40px;
font-family: 'Inter', 'Segoe UI', sans-serif;
background: radial-gradient(circle at top, #0f172a, #020617);
color: #e2e8f0;
display: flex;
justify-content: center;
}
/* container */
.article {
max-width: 720px;
}
/* heading */
.title {
font-size: 28px;
margin-bottom: 20px;
color: #cbd5f5;
}
/* text */
.text {
font-size: 17px;
line-height: 1.8;
margin-bottom: 20px;
color: #cbd5f5;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
/* 🔥 GLOBAL SELECTION */
::selection {
background: linear-gradient(135deg, #6366f1, #22d3ee);
color: #020617;
/* critical: clean visual */
text-shadow: none;
}
/* 🔥 SCOPED SELECTION */
.highlight::selection {
background: #facc15;
color: #020617;
}
UI Hints and Placeholders ::placeholder
This pseudo-element applies styles to the temporary and faint text visible within form input fields (input, textarea) before the user enters a value.
This text is not technical data, but rather a guide explaining the purpose of the input box to the user.
Default browser placeholder styles are often generic and very light gray.
This might conflict with your brand's color palette or remain so faint on certain screens that it becomes unreadable.
By using ::placeholder, you can change the text color, reduce the font size, or make it italic to help distinguish it from actual user-entered text.
UX Improvement: If the placeholder color is too similar to the final text color, it can create inconsistency; however, if it's identical, the user might mistake the box for being "already filled."
Therefore, designers usually lower the opacity or choose a softer color tone for placeholders.
Technical Limitations: This pseudo-element can only affect text properties such as color, font-style, and font-weight.
Box model properties ( width, height, margin ) do not work here because they belong to the container input element itself, not the placeholder text.
Pro Tip: The Firefox Opacity Trap A common pitfall for developers: You set the placeholder color to red; it looks vivid in Chrome but appears faint in Firefox.
This happens because Firefox applies a default opacity: 0.5 to placeholders.
To ensure colors look identical and vibrant across all browsers, you must always include ::placeholder { opacity: 1; }.
Critical Warning: A Placeholder is Not a "Label"In modern designs, developers sometimes remove the <label> to save space and rely solely on the placeholder.
This is a serious accessibility mistake.
The moment a user starts typing, the placeholder disappears. If the user gets distracted, they lose the answer to: "What was I supposed to type here?" ( Short-term Memory Issue ).
Animations and Transitions: Placeholders also support the transition property.
For Example: You can create a pleasant micro-interaction ( input:focus::placeholder { ... } ) where the placeholder gently slides to the right or fades out when the input is clicked (:focus).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Placeholder UX</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<div class="form-container">
<h2 class="title">Login Form</h2>
<input type="text" placeholder="Enter your username">
<input type="email" placeholder="Your email address">
</div>
</body>
</html>
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Inter', 'Segoe UI', sans-serif;
background: radial-gradient(circle at top, #0f172a, #020617);
}
/* container */
.form-container {
width: 320px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* heading */
.title {
color: #cbd5f5;
margin-bottom: 10px;
}
/* input */
input {
padding: 14px 16px;
border-radius: 12px;
border: 1px solid rgba(148, 163, 184, 0.2);
background: rgba(15, 23, 42, 0.6);
color: #e2e8f0;
backdrop-filter: blur(10px);
outline: none;
transition: all 0.3s ease;
}
/* hover/focus */
input:focus {
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
}
/* 🔥 placeholder base */
input::placeholder {
color: #94a3b8;
opacity: 1; /* Fix for Firefox default opacity */
transition: all 0.3s ease;
}
/* 🔥 focus animation */
input:focus::placeholder {
transform: translateX(6px);
opacity: 0.4;
}
/* 🔥 subtle hover */
input:hover::placeholder {
opacity: 0.8;
}
List Markers and Numbering ::marker
This pseudo-element directly targets the automatic marker located at the beginning of Ordered (ol) and Unordered ( ul ) lists, or the arrow indicator of the summary tag.
Until the modern era of CSS, styling these tiny bullets or numbers independently of the main text was nearly impossible.
Previously, to make a list item black while making its bullet red, developers had to disable the default list style (list-style: none) and then use ::before to create a "fake" bullet.
::marker eliminates this complexity.
You can now directly select the marker and change its color or font.
Aesthetic Freedom and Content Management: This selector does more than just change colors; thanks to the content property, it allows you to replace boring bullets with emojis or custom characters.
For Example: By writing li::marker { content: "✅ "; }, you can instantly transform a standard list into a "To-Do List."
Technical Limitations (Box Model): ::marker does not support all CSS properties.
Browsers only permit text-oriented properties on this element; color, font-size, font-family, and content can be used safely.
However, layout properties like margin, padding, background, or width do not work here.
To adjust the marker's position, you still need to manipulate the padding values of the parent li tag.
Dynamic Counters and Automation: Combined with the CSS counter() function, ::marker becomes far more powerful.
For Example: To change a standard "1, 2, 3" sequence into "Chapter 1:", "Chapter 2:", the code content: "Chapter " counter(list-item) ": "; is used.
This allows you to create auto-incrementing custom headings without breaking the HTML structure.
Accessibility and Browser SupportWhen performing visual customizations, one must not forget screen readers.
Content added via ::marker is voiced by most assistive technologies.
If you use content: "★"; purely for decorative purposes, a visually impaired user might hear the word "Star" at the beginning of each item.
Therefore, careful attention must be paid to semantic integrity when choosing symbols.
While Safari and Firefox fully support this feature, default styles will kick in on very legacy browsers.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feature List</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<section class="features">
<h2>Platform Features</h2>
<ul class="feature-list">
<li>Fast and optimized performance</li>
<li>User-friendly modern interface</li>
<li>Secure data management</li>
<li>Easy integration infrastructure</li>
</ul>
</section>
</body>
</html>
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 60px 20px;
font-family: 'Inter', sans-serif;
background: #f8fafc;
color: #1e293b;
display: flex;
justify-content: center;
}
/* container */
.features {
max-width: 700px;
}
/* heading */
.features h2 {
font-size: 28px;
margin-bottom: 25px;
color: #0f172a;
}
/* list */
.feature-list {
padding-left: 24px;
}
/* item */
.feature-list li {
font-size: 17px;
line-height: 1.8;
margin-bottom: 14px;
color: #334155;
}
/* 🔥 MODERN MARKER */
.feature-list li::marker {
content: "✦ ";
color: #6366f1;
font-size: 18px;
font-weight: bold;
}
Advanced Pseudo-Elements Pseudo-Elements
Up to this point, we have styled simple parts like the "first line" or "marker" of texts; however, modern CSS can dive much deeper than that.
The selectors we will examine next move beyond the superficial layer of your HTML page, allowing us to access the browser's own generated interfaces and modern component architecture ( Shadow DOM ).
Some HTML elements are not just simple tags; they house hidden mechanisms produced by the browser itself.
Internal UI Controls: A file upload button (input type="file") is actually a complex structure with a button and a file name area.
In the past, we couldn't interfere with these, but with new-generation pseudo-elements, we can now style these "native" buttons.
Top Layer: In the modern web, Dialog windows or Fullscreen modes live in a top layer, independent of the page's z-index battles. We need special selectors to manage the background and behavior of this layer.
Media and Feedback LayersThe web is not just static text; it contains videos, audio files, and user interactions.
Customizing the font of captions inside a video or the red wavy line that underlines a misspelled word is not possible with standard CSS selectors.
Pseudo-elements in this group allow you to hook into the internal structure of media players and browser spell-checkers.
Modern Architecture: Shadow DOM and Encapsulation: Modern web development is built upon Web Components.
To remain unaffected by the outside world, these components wrap themselves in a protective bubble called the Shadow DOM.
Crossing Boundaries with ControlNormal CSS selectors cannot enter the Shadow DOM.
However, sometimes you need to style a specific part of a component from the outside.
This is where functional pseudo-elements like ::part() and ::slotted() come into play, allowing us to reach the points permitted by the component without violating encapsulation rules.
File Selection Button ::file-selector-button
The <input type="file"> tag is one of the oldest and most functional, yet design-wise most problematic components in web history.
This element is not actually a single piece; the browser splits it into two parts behind the scenes (within the Shadow DOM): A button ("Choose File") and a text area displaying the selected file's name.
The ::file-selector-button pseudo-element allows you to surgically extract and style that internal button component.
Historical Legacy: The "Ugly Duckling" SyndromeUntil the recent past, browsers did not permit us to touch this button.
It appeared grey and clunky in Chrome, shiny and rounded in Safari, and angular in Windows.
This often caused a modern, carefully designed website to look as if a 90s-era operating system component had been forgotten in the middle of the page.
The Old "Hack" Method: Invisible Input: To hide this aesthetic flaw, developers used the "Label Hack" for years: hide the actual input with opacity: 0 or display: none, place a fake label button in front of it, and redirect the click event.
While this method worked, it often led to accessibility issues and errors in keyboard navigation.
Modern Solution and Direct AccessThere is no longer a need for tricks. Thanks to this pseudo-element, you can treat the file upload button just like a standard button.
Standard Button Properties: You can freely change the background color (background), corner roundness (border-radius), internal spacing (padding), and font.
You can even use margin-right to increase the distance between the button and the adjacent file name text.
Hover and Active States: You can combine this pseudo-element with pseudo-classes.
By using input::file-selector-button:hover, you can change the button's color when the user hovers over it.
Vendor Prefixes and Legacy NamesBefore this property was standardized, browsers used different names.
Webkit-based browsers (Chrome, Safari) used ::-webkit-file-upload-button, while older Edge/IE used ::-ms-browse.
In modern code, using the standard name is sufficient, but it remains useful to know these aliases for legacy systems.
Unreachable Text: The File NameThere is an important distinction here: we are only styling the button.
The text appearing next to the button, such as "No file chosen" or "photo.jpg," is controlled by the browser and cannot ( yet ) be targeted directly with a CSS selector.
While you can change the font of that text (by applying the font to the input itself), changing its content or exact position remains difficult.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload UI</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<div class="upload-card">
<h2>Upload File</h2>
<p>Select your file or click to upload</p>
<input type="file">
</div>
</body>
</html>
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #f1f5f9, #e2e8f0);
}
/* card */
.upload-card {
background: white;
padding: 30px;
border-radius: 16px;
width: 320px;
text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
}
/* title */
.upload-card h2 {
margin-bottom: 10px;
color: #0f172a;
}
/* text */
.upload-card p {
font-size: 14px;
color: #64748b;
margin-bottom: 20px;
}
/* input container feel */
input[type="file"] {
width: 100%;
padding: 20px;
border: 2px dashed #cbd5f5;
border-radius: 12px;
background: #f8fafc;
cursor: pointer;
transition: all 0.3s ease;
}
/* hover area effect */
input[type="file"]:hover {
border-color: #6366f1;
background: #eef2ff;
}
/* 🔥 BUTTON STYLING */
input[type="file"]::file-selector-button {
border: none;
padding: 10px 18px;
margin-right: 12px;
border-radius: 8px;
background: #6366f1;
color: white;
font-size: 14px;
cursor: pointer;
transition: all 0.25s ease;
}
/* hover */
input[type="file"]::file-selector-button:hover {
background: #4f46e5;
}
/* active */
input[type="file"]::file-selector-button:active {
transform: scale(0.96);
}
The Background Curtain ::backdrop
::backdrop is like the built-in "Cinema Mode" of the modern web.
It is that magical layer that covers the rest of the world behind an element when it takes center stage.
This pseudo-element is not created inside any HTML element, but rather within the context of the browser's special stack called the Top Layer, generated immediately in front of the viewport.
Its primary purpose is to lock the user's focus onto the featured content and visually de-emphasize the background content.
Triggers: When Does It Appear?You cannot simply add ::backdrop to an ordinary div; only specific APIs generate this layer:
- When a <dialog> element is opened using the .showModal() method.
- When any element is triggered into fullscreen mode via the Fullscreen API.
- When the new-generation Popover API is utilized.
Visual Design: The Frosted Glass Effect: By default, browsers ( User Agent Stylesheet ) assign a semi-transparent black color to this layer, but you have complete control.
Especially when used with the backdrop-filter property, it becomes an essential part of modern interfaces.
By blurring the background content with blur(10px), you can create a sense of depth similar to the iOS or Windows Aero interfaces.
No InheritanceA common mistake is assuming that the backdrop inherits styles from its parent element.
Technically, ::backdrop does not behave as a child of the element; it acts as a sibling ( or even a distant cousin at the top of the tree ).
Therefore, you may occasionally need to re-define fonts or variables specifically for this scope.
Ending the Z-Index WarsIn the past, we assigned crazy values like z-index: 9999 to place a curtain behind a modal.
Thanks to the Top Layer mechanism, ::backdrop is entirely independent of the page's z-index context.
It is always at the top and never creates conflicts.
Top Layer and the z-index Stack Where does ::backdrop reside; why is it separate from the normal stack?
::backdrop cannot
be applied
to arbitrary elements; it does not generate without a Top Layer trigger
such as
dialog.showModal(), the Fullscreen API, or a popover.
::backdrop
The layer that dims the rest of the page; sits
underneath the modal, yet still above the entire page stack
Ordering within the Top Layer box: first the curtain (::backdrop), followed by the actual dialog on top. Together,
this duo rises
above the entire normal z-index world.
::backdrop, the dialog does not
enter the
normal
z-index race; within the Top Layer, the curtain is ordered at the bottom and
content at the top
— both of which sit above the page stack.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Glass Modal</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<button class="open-btn">Open Modal</button>
<dialog id="modal">
<h2>Notification</h2>
<p>This is a modern modal example. The background has been de-emphasized with a blur effect.</p>
<button class="close-btn">Close</button>
</dialog>
<script src="script.js?v=1.0.150"></script>
</body>
</html>
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #6366f1, #22d3ee);
}
/* open button */
.open-btn {
padding: 14px 28px;
border: none;
border-radius: 10px;
background: #0f172a;
color: white;
cursor: pointer;
}
/* modal */
dialog {
border: none;
border-radius: 16px;
padding: 25px;
max-width: 320px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
}
/* 🔥 BACKDROP */
dialog::backdrop {
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(6px);
}
/* close */
.close-btn {
margin-top: 15px;
padding: 10px 20px;
border: none;
border-radius: 8px;
background: #6366f1;
color: white;
cursor: pointer;
}
const modal = document.getElementById('modal');
document.querySelector('.open-btn').onclick = () => modal.showModal();
document.querySelector('.close-btn').onclick = () => modal.close();
Note
WebVTT also defines the ::cue-region pseudo-element; however, since this
feature is
not supported by any browser, practical use is not currently possible.
For this reason, only the ::cue example is demonstrated here.
Managing Video Captions ::cue
As video and audio consumption increases across the web, ensuring these contents are understandable by everyone has become a necessity.
The <video> and <audio> tags can render captions on the screen by reading external text files ( WebVTT format ) via the <track> tag.
However, default browser caption styles are often basic, sometimes difficult to read, and disconnected from your site's design.
This is where the ::cue pseudo-element comes in, allowing you to intervene in this standard appearance and make captions an integral part of your brand.
Technical Context: Penetrating the Shadow DOMWhen captions are enabled in a video player, those texts do not exist in the normal flow ( Light DOM ) of your HTML page.
The browser generates these texts within the internal world of the video player ( User Agent Shadow DOM ).
::cue is a special key that allows CSS to enter this "closed box." It enables you to target those momentary lines of text that appear as the video plays.
Styling Capabilities and Limitations: What Can We Change?::cue is not as flexible as other HTML elements. To prevent disrupting the video flow and maintain performance, browsers only permit specific properties.
You cannot arrange a caption with flexbox or apply complex animations to it. Permitted properties are limited to the following:
Targeting Different Scenarios- ::cue - Global style for all captions
- video::cue - Specifically for videos
- ::cue(b) - Bold text within WebVTT
- ::cue(.className) - Texts with specific classes
Readability and UX: The Video Background Issue Video content is dynamic; the background changes constantly (it becomes black, white, or contains complex patterns).
If you only define color: white;, the caption will vanish when the video scene turns white.
The Golden Rule: Preserve ContrastProfessional broadcasters (Netflix, YouTube, etc.) usually add a semi-transparent black background behind the text.
To achieve this: ::cue { background-color: rgba(0, 0, 0, 0.6); color: white; } is the safest approach.
Alternatively, if you do not want a background box, you can use a strong text-shadow to give letters a black outline effect. This ensures the text "separates" from the video content.
Internal WebVTT Tags: The WebVTT format is not just plain text. It can include tags to indicate the speaker ( Voice Span ) or for emphasis.
With advanced selectors like ::cue(v[voice="Narrator"]), you can style lines spoken only by the "Narrator" as italic and yellow. This is a great way to visually encode who is speaking for the viewer.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Subtitle Card</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<div class="video-card">
<div class="video-box">
<video controls>
<source src="info.mp4" type="video/mp4">
<track src="subtitles.vtt" kind="subtitles" srclang="en" default>
</video>
</div>
<div class="video-info">
<h3>Streaming Subtitle Demo</h3>
<p>
This example demonstrates how <strong>::cue</strong> can transform subtitles
into a modern piece of UI.
</p>
</div>
</div>
</body>
</html>
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Inter', sans-serif;
background: radial-gradient(circle at top, #020617, #020617);
}
/* 🔥 CARD */
.video-card {
width: 520px;
border-radius: 16px;
background: rgba(15, 23, 42, 0.9);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
overflow: hidden;
}
/* 🎬 Video Area */
.video-box {
width: 100%;
background: black;
}
.video-box video {
width: 100%;
display: block;
}
/* 📝 Info Panel */
.video-info {
padding: 16px 18px;
}
.video-info h3 {
margin: 0 0 8px;
font-size: 16px;
color: #e2e8f0;
}
.video-info p {
margin: 0;
font-size: 14px;
color: #94a3b8;
line-height: 1.5;
}
/* 🔥 SUBTITLE STYLING */
video::cue {
color: white;
background: rgba(0, 0, 0, 0.65);
padding: 4px 10px;
border-radius: 6px;
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.8);
}
WEBVTT
NOTE Demo subtitle file for ::cue styling
00:00:01.000 --> 00:00:04.000
<v Narrator>This is a narrator's speech.</v>
00:00:05.000 --> 00:00:08.000
<v Speaker>This is a different speaker.</v>
00:00:09.000 --> 00:00:12.000
This is <c.highlight>very important</c> information.
00:00:13.000 --> 00:00:16.000
Subtitles can be <b>branded</b> and customized.
CSS ::grammar-error Managing the Green Squiggly Line
Modern browsers are no longer just viewers; they are also sophisticated text editors.
Thanks to these editor capabilities, user-provided content is not just rendered on the screen but also analyzed for meaning, structure, and linguistic rules.
When a user fills out a form or types into a text box ( textarea, contenteditable ), the browser analyzes the text using background AI-powered algorithms.
This analysis process is not limited to word checks; it also evaluates sentence flow, tense consistency, and grammatical context.
If the words are spelled correctly but there is a logical error in the sentence structure, such as subject-verb agreement or tense suffixes, the browser marks it with the ::grammar-error pseudo-element.
This marking is usually presented as visual feedback to the user, most often appearing as a squiggly underline.
While developers cannot fully control the underlying logic, they can use ::grammar-error to customize the color, style, or emphasis of this visual indicator.
Spelling Error vs. Grammar ErrorIn web development, these two concepts are often confused, but technically they are entirely distinct mechanisms:
Understanding this distinction is critical for designing appropriate user feedback.
::spelling-error: Occurs when a word cannot be found in the dictionary.
It is typically indicated by a red wavy line. (e.g., "Reciept" instead of "Receipt")
These errors are mostly related to missing letters, extra characters, or incorrect character usage and are evaluated directly at the word level.
::grammar-error: Occurs when words are correct but their arrangement is wrong.
It is typically indicated by a green or blue wavy line. (e.g., "I goes home.")
These errors are evaluated at a higher level—sentence and context level—and usually require more complex analysis.
Therefore, ::grammar-error provides a smarter and more contextual feedback mechanism in terms of user experience.
Style Manipulation and Boundaries Preserving Sentence Structure
Unlike spelling errors, grammar errors often span longer segments of a sentence.
Consequently, the marked area is rarely just a single word; it is frequently a semantic unit composed of multiple words.
If browsers permitted layout-altering properties like font-size or padding here, the entire paragraph would constantly jitter and shift as the user types.
This behavior would ruin readability—especially during live editing—and severely degrade the user experience.
Therefore, browser engines only allow styles for ::grammar-error that provide visual emphasis.
Essentially, this pseudo-element does not alter the document flow; it acts as an annotation layer overlaid on the existing text.
In this context, ::grammar-error is merely a "makeup" layer applied to the text, rather than "plastic surgery" that modifies the text's skeleton.
From a developer's perspective, this means prioritizing safe properties such as background-color, underlines, outline, or text-decoration when styling.
In short; the goal is not to rearrange the text, but to display the error to the user without distortion or distraction.
|
Style Type
|
Description and Recommendations
|
|---|---|
|
Smart Underlines
Text Decoration
|
This is the industry standard.
|
|
Background Management
Background
|
Be very careful when using
background
colors, as grammar errors can be quite long.
|
|
Text Color
Color
|
You can change the color to
dim the text
or emphasize the error.
|
|
Depth with Shadow
Text Shadow
|
By adding a subtle text-shadow, you can lift the error slightly "off the page" or create a "focal point" feel with a soft blur. |
|
Scenario
|
Description
|
|---|---|
|
Brand Consistency (Branding)
|
If your site's theme is
predominantly
green, the browser's default green error line may become invisible.
|
|
Educational Applications
|
If you are building a language learning app, you may want to emphasize grammar errors with a thicker line or a more striking background than the standard one. |
|
Dark Mode
|
Default colors can sometimes
cause
contrast issues on dark backgrounds.
|
Critical Note: Browser and Language Support Visible to Some, Not All
This feature is a perfect example of the Progressive Enhancement principle in web development literature.
According to this principle, while the core experience remains functional for every user, extra visual and functional improvements are activated in supported browsers.
When you add this code to your CSS file, an older browser (or a version that does not support the feature) reads this line,
says "I don't recognize this," and skips it without causing any errors.
This behavior stems from CSS being a naturally tolerant and resilient language.
In other words, your site won't crash and the layout won't break; that specific style simply won't be applied, which gives the developer the freedom to "use it without fear."
Therefore, experimental or limited-support features like ::grammar-error should be used without making core functionality dependent on them.
Decision Mechanism: Who Does the Browser Listen To?Critical Point: Neither Chrome nor Firefox decides on their own whether a sentence is grammatically incorrect.
Instead of taking on this heavy processing load ( natural language processing ), browsers consult the Operating System ( Windows, macOS, Android ) on the user's device.
This means the same web page can produce different grammatical results across different devices.
For Example: A sentence marked as an error for one user might be considered entirely correct for another.
This discrepancy arises from system dictionaries, language packs, and user settings.
Furthermore, if spell-checking is disabled entirely on some systems, ::grammar-error will never be triggered.
Thus, developers should treat this feature not as a guaranteed mechanism, but as an auxiliary layer.
In short; control lies with the user environment, not you. You simply make the output of that system more understandable and aesthetic.
Processing Flow How is ::grammar-error Triggered?
Note: ::grammar-error is not created by the developer in HTML; the browser adds this pseudo-element automatically based on grammatical feedback.
dynamically creates the ::grammar-error pseudo-element. This element does not exist in the HTML source.
Note
The following live example relies entirely on the native power of CSS.
However, it should be kept in mind that the functionality of the ::grammar-error selector depends on the current language processing capabilities of your browser and operating system.
If you wish to build a more consistent, browser-independent structure that yields the same flawless results for every user, utilizing external JavaScript-based grammar libraries (such as LanguageTool, etc.) would be a more professional solution.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Grammar Editor</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<section class="editor-container">
<h3>Modern Grammar Editor</h3>
<p class="info-badge">If your browser's spell-check is active, you can see the errors.</p>
<div class="editor-area" contenteditable="true" spellcheck="true">
I goes to the cinema yesterday evening. (Grammar Error Example)
<br><br>
Wrongly spelled words are different. (Spelling Error Example)
</div>
<div class="legend">
<span class="item grammar">::grammar-error (Purple Highlight)</span>
<span class="item spelling">::spelling-error (Orange Underline)</span>
</div>
</section>
</body>
</html>
body {
background: #f8fafc;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
margin: 20px;
}
.editor-container {
max-width: 850px;
width: 100%;
background: white;
padding: 30px;
border-radius: 28px;
box-shadow: 0 20px 35px -8px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(226, 232, 240, 0.6);
}
h3 {
margin: 0 0 10px 0;
color: #1e293b;
}
.info-badge {
background: #dbeafe;
color: #1e40af;
padding: 6px 12px;
border-radius: 8px;
font-size: 0.85rem;
margin-bottom: 20px;
display: inline-block;
}
.editor-area {
width: 100%;
padding: 20px;
font-size: 1.1rem;
line-height: 1.6;
border: 1.5px solid #e2e8f0;
border-radius: 20px;
min-height: 180px;
outline: none;
color: #0f172a;
transition: border-color 0.2s;
}
.editor-area:focus {
border-color: #6366f1;
}
/* 🔥 GRAMMAR ERROR STYLING */
.editor-area::grammar-error {
text-decoration: wavy underline #8b5cf6;
text-decoration-thickness: 2.5px;
text-underline-offset: 5px;
background-color: rgba(139, 92, 246, 0.08);
}
/* 🔥 SPELLING ERROR STYLING */
.editor-area::spelling-error {
text-decoration: wavy underline #f97316;
text-decoration-thickness: 2px;
background-color: rgba(249, 115, 22, 0.05);
}
.legend {
margin-top: 20px;
display: flex;
gap: 15px;
}
.item {
font-size: 0.8rem;
font-weight: 600;
}
.grammar { color: #8b5cf6; }
.spelling { color: #f97316; }
CSS ::spelling-error Beyond the Red Line
One of the most familiar visual feedbacks for internet users is that bright red squiggly line appearing under a misspelled word.
This visual cue provides instant feedback, making it easier for users to notice typos and significantly reducing error rates, especially in form inputs.
It is the browser's or the operating system's way of saying: "I couldn't find this word in the dictionary."
This check mechanism is generally tied to the active language dictionary and can produce different results based on the user's selected language.
However, this default style might not be suitable for every design language.
Especially in Dark Mode interfaces or pastel-toned designs, the default red can appear overly aggressive or illegible.
Furthermore, in projects with strong brand identities, this default style can disrupt design integrity and look like a foreign element in the UI.
The ::spelling-error pseudo-element allows you to match this standard warning with your site's aesthetics.
This ensures you continue to provide error information to the user while presenting the warning in a softer, more harmonious, and more controlled manner.
Technical Difference: Spelling vs. GrammarDistinguishing between these two concepts is critical for using the correct selector:
While one operates at the word level, the other analyzes at the sentence level, and this difference directly impacts your styling strategy.
(e.g., "I goes home"). This is typically indicated in Green/Blue.
Correctly understanding this distinction ensures that the feedback given to the user is both accurate and meaningful.
In short; while ::spelling-error offers a fast and direct check mechanism, ::grammar-error performs a deeper and more contextual analysis.
Styling Freedom and Boundaries What Can We Paint?
To understand why browsers have such strict rules for ::spelling-error, imagine this scenario:
A user is typing quickly and makes a typo.
What if you had used CSS to say, "Increase the font size of the misspelled word (font-size: 20px)" or "Add spacing around it (margin: 5px)"?
The erroneous word would suddenly swell, pushing other words in the line and momentarily breaking the entire paragraph's Layout.
This is not just a visual problem; it creates a significant performance cost, forcing the browser to recalculate the entire page with every change.
Having the text jitter or shift every time a user makes an error would result in a terrible user experience ( Bad UX ).
Especially in long texts or on low-performance devices, this could even lead to issues like latency, freezing, and input lag.
Therefore, browsers prohibit all properties that require layout recalculation ( Reflow ) and only allow properties that perform "painting over" ( Repaint ).
In other words, ::spelling-error does not change the DOM structure or the flow layout; it acts as a visual layer added on top of the existing text.
This approach both preserves performance and provides the user with a seamless typing experience.
In short; the goal here is not to rearrange the text, but to emphasize the error without distortion, without breaking the flow, and without causing distraction.
|
Decoration Type
|
Description and Examples
|
|---|---|
|
Advanced Underlines
Text Decoration
|
You can control the style of the line, not just the color.
Use text-decoration-style: dotted; to
create a
dotted underline or |
|
Background Highlights
Highlighter
|
By painting the misspelled
word with a
semi-transparent color like |
|
Text Color and Contrast
Color
|
You can paint the misspelled
word red
using color: #ff0000;.
|
|
Shadow Illusions
Text Shadow
|
While you cannot change the
font-weight, you can use text-shadow to give the word a
"boldened" or "glowing" effect.
|
|
Scenario
|
Why is it Necessary?
|
|---|---|
|
Sensitive Interfaces
|
In an application where the
user is
writing poetry or creating creative text, aggressive red lines can be
"uninspiring."
|
|
Code Editors
|
In web-based code editors,
variable names
are often not found in the dictionary.
|
|
Accessibility (A11Y)
|
Red-green differentiation is
difficult for
some types of color blindness.
|
Trigger: The Spellcheck Attribute The Key on the HTML Side
To maintain a balance between performance and user experience, browsers do not perform spell-checking on every single field.
For Example: Seeing red lines while filling out a username or an email field is annoying, as these fields often contain words that aren't in a dictionary.
Because of this, browsers follow a built-in "Intent Reading" protocol:
If you want your CSS styles to work in a short input field (like a "Subject" line), you must force the browser to check it.
To do this, you must explicitly add the spellcheck="true" attribute to the HTML element.
This code effectively tells the browser:
"Dear browser, even though this is a short field, the user will be writing meaningful sentences here. Please enable spell-checking."
A Vital Distinction: The Role of CSSThere is a common misconception: adding ::spelling-error to your CSS does not initiate spell-checking.
CSS is a "Painter," not an "Editor."
The Editor ( Browser/Operating System ) reads the text, finds the error, and marks it, saying, "Look, there's a mistake here."
The Painter (CSS) then simply says, "Okay, since an error was found, I will paint this part red." If no error is identified by the system, CSS has nothing to apply its styles to.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Draft Notebook (Blog Writing Panel)</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<div class="draft-container">
<div class="draft-badge">DRAFT MODE</div>
<div class="writing-zone" contenteditable="true" spellcheck="true">
Some of the words hear are intentionally mispelled.
In the digital world, writing without errors is difficult, but making aesthetic errors is possible.
Please do not try to clear this area, just feel the highlights.
</div>
<div class="writing-footer">
<span>Characters: 142</span>
<span>Readability: High</span>
</div>
</div>
</body>
</html>
/* General Container */
.draft-container {
background: #fff;
border-left: 5px solid #ff4757;
/* Accent on error color */
padding: 2rem;
border-radius: 4px 16px 16px 4px;
box-shadow: 10px 10px 30px rgba(0, 0, 0, 0.03);
font-family: 'Segoe UI', system-ui, sans-serif;
max-width: 650px;
margin: 20px auto;
}
.draft-badge {
font-size: 10px;
font-weight: bold;
letter-spacing: 1px;
color: #ff4757;
margin-bottom: 1rem;
}
.writing-zone {
font-size: 1.2rem;
line-height: 1.8;
color: #2f3542;
outline: none;
min-height: 120px;
}
.writing-footer {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid #f1f2f6;
display: flex;
justify-content: space-between;
font-size: 12px;
color: #a4b0be;
}
/* --- MAIN FOCUS: SPELLING ERROR CUSTOMIZATION --- */
.writing-zone::spelling-error {
/* 1. Highlighter Effect (Background) */
background-color: rgba(255, 71, 87, 0.15);
/* 2. Advanced Underline (Dotted and Thick) */
text-decoration: underline dotted #ff4757;
text-decoration-thickness: 2px;
/* 3. Shadow Illusion (Slightly emboldens the words) */
text-shadow: 0.5px 0 0 #ff4757;
/* 4. Color */
color: #ff4757;
}
/* On selection (to avoid mixing with highlights) */
.writing-zone::selection {
background: #2f3542;
color: #fff;
}
CSS ::slotted() Light Leaking In
In modern web development, we can create our own custom HTML tags ( Custom Elements ), such as <my-card>.
This approach allows us to establish reusable, encapsulated, and independent component architectures in large-scale projects.
These components utilize Shadow DOM to remain isolated from the external world.
However, there are times when we need to pass external content, like a heading or an image, into this card.
At this point, the data flow becomes bidirectional: the component receives data from the outside world while striving to maintain its own internal styling rules.
On the HTML side, this content transfer is handled by the <slot> element.
On the CSS side, the authority to style this "guest" content lies with the ::slotted() pseudo-element.
This structure enables the component developer to control the internal layout while ensuring that external content remains consistent within defined rules.
Especially in UI libraries ( cards, modals, lists ), the slot mechanism makes content entirely flexible while preserving stylistic integrity.
The Logical Paradox: Who Owns the Content?An element passed into a slot (such as an h1 tag) physically still resides in the main page ( Light DOM ), but is visually rendered inside the component ( Shadow DOM ).
This situation creates a common "ownership problem" in web development: who does the content belong to?
These elements, which are outside in code but inside in appearance, cannot be directly controlled via traditional CSS selectors.
::slotted() is the only legitimate way for a component to "touch" content that it does not technically own.
This allows the developer to guide external content into a specific design system rather than leaving it entirely unmanaged.
In short; ::slotted() is the tool used to strike a delicate balance between isolation and flexibility.
Selector Boundaries and the "Flattened Tree" Rule What Can We Select?
The easiest way to understand this restriction is to think of the <slot> mechanism as a "Gift Drop-off Point."
The user ( Light DOM ) hands a gift box ( div ) to your component ( Shadow DOM ).
By using ::slotted(), you can wrap the outside of this gift box, add a ribbon, or change its color.
However, you cannot open the box and rearrange the items inside (the span, p, or img tags within the div). The box remains closed, and its contents are the responsibility of the sender.
Technical Constraint: The Flattened TreeWhen the browser renders the page, it virtually merges the normal DOM tree with the Shadow DOM tree, a process called the "Flattened Tree."
During this flattening, the ::slotted() selector is programmed to see only the elements that have direct contact with the slot.
In other words, the CSS engine does not look at the "children" of the element entering through the slot.
This is why selectors establishing parent-child relationships ( combinators ), such as ::slotted(div p), do not work.
The Principle of EncapsulationThis is not a bug; it is a security and architectural feature.
If your component could intervene in the deepest parts of the content provided by the user, you might unintentionally break the user's own styles.
The Web Components standard dictates: "A component should only manage what belongs to it and the outer shell of the box delivered to it."
|
Source
|
Priority Status
|
|---|---|
|
Light DOM (Page CSS)
|
( Winner ) Since the element physically resides in the main page, the global styles of the page have the highest priority. |
|
::slotted() (Component CSS)
|
( Secondary Priority ) The
component can
suggest a "default" style for the content.
|
|
Browser Default
|
Lowest priority.
|
|
Scenario
|
Solution and Logic
|
|---|---|
|
Design Systems (UI Kits)
|
Imagine building a card (<my-card>) component.
|
|
Layout and Geometry
|
A list component might require
every item
passed into it to have a specific width or behave as a Flex Item.
|
|
Media Control
|
To prevent images ( img ) added by the user from
overflowing the
component, the ::slotted(img) {
max-width: 100%;
} rule is vital.
|
The Concept of "Polite Styling" Component Authority vs. User Intent
The use of ::slotted() carries a philosophical depth often referred to in web literature as "Polite Styling."
At the core of this approach lies a balance between control and freedom for the component developer.
The goal is not to force a design upon the user, but to provide logical and aesthetic defaults that make their work easier.
When you, as a component developer, define a button style with ::slotted(button), you are essentially saying:
"If the user hasn't specified a custom color, use this blue that I've designed."
This ensures your component offers a consistent and elegant look out-of-the-box.
However, this style is a suggestion, not an absolute rule.
This approach provides significant advantages in terms of flexibility and reusability, especially within design systems.
The User Has the Final WordAccording to CSS Specificity rules, styles applied by the user in the Light DOM will always override the styles you provide via ::slotted().
This behavior preserves one of the strongest principles of the web platform: user control.
No matter how well-designed your component is, the final decision belongs to the developer using it.
This is a brilliant feature; your component provides a beautiful default appearance without restricting the user, allowing for full customization.
In short; a good component is not just functional, but also respectful.
::slotted() and ::part() side-by-side
The Light DOM node projected into a slot · The part
gate within the
shadow
<my-card><h2>Title</h2></my-card>
::slotted(h2) inside Shadow — Provides polite default styling
to the
h2 placed directly into the slot (low specificity; page styles
generally
prevail).
Boundary: ::slotted() only selects that
top-level node assigned to the slot; you cannot target its descendants
(e.g.,
h2 span) with this selector.
my-card::part(icon) { color: red; }
part="icon" (or multiple names space-separated) only within
the shadow
tree — A gate opened by the component author to the outside world.
Condition: The ::part() selector matches
nodes inside
the shadow that actually possess a part attribute; if the attribute is absent,
the rule is
not applied.
part and intentionally exposed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>::slotted() - Shadow Demo</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<h1>::slotted() Demo - Polite Styling vs Light DOM</h1>
<!-- EXAMPLE 1: Only component's default styles (::slotted) are active -->
<my-card>
<h2 class="slot-title">🎯 This heading became orange via ::slotted(h2)</h2>
<p>This paragraph received background and border-radius via ::slotted(p).</p>
<button>Button turned green via ::slotted(button)</button>
</my-card>
<hr>
<!-- EXAMPLE 2: Light DOM inline styles override ::slotted -->
<my-card>
<h2 class="slot-title" style="color: red;">🔴 This heading: Light DOM inline style (red) → Overrides ::slotted orange</h2>
<p style="background: white; border: 1px solid black;">This paragraph has inline style → Overrides ::slotted background</p>
<button style="background-color: gray; border-radius: 0;">This button is gray via inline style → ::slotted green is inactive</button>
</my-card>
<hr>
<!-- EXAMPLE 3: Showing priority of Light DOM global CSS -->
<my-card>
<h2 class="slot-title">💜 This heading: Light DOM global CSS (purple) → Overrides ::slotted orange (Specificity won)</h2>
<button>This button: Light DOM global CSS (gold) → Overrides ::slotted green</button>
</my-card>
<hr>
<div style="margin-top: 20px; padding: 12px; background: #f0f0f0; border-radius: 8px;">
<h3>📘 ::slotted() Summary:</h3>
<ul>
<li>✅ <strong>Selectable:</strong> Elements placed <strong>directly</strong> into the slot (h2, p, button etc.)
</li>
<li>❌ <strong>Non-selectable:</strong> Descendants of those elements (h2 > span, p > strong)</li>
<li>🎯 <strong>Priority:</strong> Light DOM styles (global or inline) > ::slotted() > Browser defaults</li>
<li>💡 <strong>Philosophy:</strong> "Polite Styling" - suggests a default, doesn't impose.</li>
<li>🔄 <strong>Difference from ::part():</strong> ::part() accesses <strong>intentionally exposed</strong> custom parts inside the shadow tree. ::slotted() accesses the direct child coming from outside.</li>
</ul>
</div>
<script src="script.js?v=1.0.150"></script>
</body>
</html>
/* These styles can INTERVENE with slot content inside the component and override ::slotted() */
my-card .slot-title {
color: purple !important;
/* Winning style - Light DOM prevails */
font-style: italic;
}
/* Global button style from Light DOM */
my-card button {
background-color: gold;
border: 2px solid darkgoldenrod;
font-weight: bold;
}
// CUSTOM ELEMENT definition (Contains Shadow DOM)
class MyCard extends HTMLElement {
constructor() {
super();
// Create Shadow DOM (closed/isolated environment)
const shadow = this.attachShadow({ mode: "open" });
// SHADOW DOM STYLES (Internal component styles)
shadow.innerHTML = `
<style>
/* Component's own container */
.card {
border: 2px solid #3498db;
border-radius: 12px;
padding: 16px;
background: #f9f9ff;
font-family: system-ui, sans-serif;
max-width: 400px;
margin: 20px 0;
}
.card h3 {
margin-top: 0;
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 8px;
}
/* ================================= */
/* ::slotted() - POLITE STYLING */
/* ================================= */
/* Targets any element placed directly into the slot */
::slotted(*) {
margin: 12px 0;
display: block;
}
/* Default styling for headings inside the slot */
::slotted(h2) {
color: #e67e22;
font-size: 1.5rem;
font-weight: 600;
border-left: 4px solid #e67e22;
padding-left: 12px;
}
/* Default styling for buttons inside the slot (polite suggestion) */
::slotted(button) {
background-color: #2ecc71;
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
::slotted(button:hover) {
background-color: #27ae60;
transform: scale(1.02);
}
/* Default styling for paragraphs inside the slot */
::slotted(p) {
color: #555;
line-height: 1.5;
background: #fff8e7;
padding: 8px;
border-radius: 8px;
}
/* ⚠️ IMPORTANT: This won't work - ::slotted cannot select descendants */
/* ::slotted(p span) { color: red; } -> DOES NOT WORK! */
/* Component's own slot tag */
slot {
display: block;
margin-top: 8px;
}
</style>
<div class="card">
<h3>📦 My Shadow Component</h3>
<div class="slot-area">
<!-- Content will be projected here -->
<slot></slot>
</div>
<div style="font-size: 11px; color: #999; margin-top: 12px;">
⚡ ::slotted() is applying polite styling
</div>
</div>
`;
}
}
// Register custom element
customElements.define("my-card", MyCard);
CSS ::part() The Key to the Closed Box
The fundamental purpose of Shadow DOM is Encapsulation.
This means styles inside the component do not leak out, and styles from the outside cannot enter.
Thanks to this isolation, components function in a predictable, secure, and conflict-free manner.
However, this "total isolation" can sometimes turn into a disadvantage.
In the real world, developers want to customize components to fit their specific projects rather than using them exactly as they are.
As a designer, you might want to change only the color of the "Play" button within a pre-built "Video Player" component.
This creates a balance problem: if the component remains entirely closed, flexibility is lost; if it is completely open, encapsulation loses its meaning.
::part() acts as a controlled bridge established between these two extremes.
Previously, it was impossible to reach inside a component. Now, ::part() allows you to access elements that the component author has specifically marked with a "You can paint this" label, bypassing the Shadow DOM barrier.
This marking is typically done using the part attribute in HTML, allowing the component to expose a small but controlled styling API to the outside world.
Thus, ::part() serves as an intentionally opened "customization gate" rather than providing random access.
Through this approach, the internal structure of the component is preserved while providing the necessary flexibility to the developer.
Logical Difference: Slotted vs. Part::slotted() → Targets content provided by the user from the outside ( The content belongs to the user and is projected into the component ).
::part() → Targets fixed parts of the component itself ( The part belongs to the component and is a piece of its internal structure ).
Understanding this difference is vital; one controls the content, while the other controls the structure.
In short; while ::slotted() represents flexibility, ::part() represents controlled access.
Contract-Based Styling Attribute and Pseudo-Element Mapping
For the magic of ::part() to work, the component author must first add the part="name" attribute to the element within the Shadow DOM.
This process essentially means the component is opening a "controlled gateway" to the outside world.
This is identical to the logic of a Public API ( Public Interface ) in software architecture.
The developer decides which parts to export, while everything else remains private (hidden).
This allows the internal structure of the component to be completely changed while the contract exposed to the outside remains stable.
A well-designed component clearly defines which parts can be styled and exposes them to the outside world with meaningful and consistent names.
This naming is not just technical; it is a piece of documentation that directly influences how the component will be consumed.
CSS Side: ConsumingAn external developer can use the my-component::part(name) selector to write styles as if that element were in the normal DOM.
This provides the developer with powerful control and the advantage of working without needing to know the component's internal structure.
Thanks to this mechanism, even if the internal structure changes ( such as using a span instead of a div ), the external CSS won't break as long as the part name remains the same. This is the pinnacle of sustainability.
This approach plays a critical role in large-scale projects and team collaborations, as different developers can progress without breaking each other's code.
Simultaneously, this structure ensures that components are versionable.
Even if the internal structure shifts, backward compatibility can be maintained by preserving the external API.
In short; ::part() is not just a styling tool, it is an official communication protocol between components.
|
Feature
|
Detail
|
|---|---|
|
Pseudo-Class Chaining
|
You can use state-based pseudo-classes
like ::part(btn):hover or ::part(input):focus.
|
|
Structural Constraints
|
You cannot access children
inside
a part, such as ::part(box) span.
|
|
Multiple Identifiers
|
You can assign multiple roles to an
element: part="tab active".
|
Advanced: Part Forwarding Access in Nested Components
If a <user-card> component uses another component inside it, such as <custom-button>, an external stylesheet using ::part can only access the outermost layer by default.
This is because every component has its own Shadow DOM boundary, and these boundaries function in isolation from one another.
This structure creates a layered architecture in nested components, where each layer maintains its own domain of control.
However, this isolation makes reaching deep-seated components difficult and renders direct styling impossible.
To expose the "part" of a nested button to the outside world, the intermediate component must "Forward" that part.
This essentially represents a delegation of authority between components.
This process is handled via the exportparts attribute, where the intermediate layer re-exposes specific parts of the sub-component to the outside world.
Consequently, even in complex multi-layered systems, it becomes possible to change the color of a button at the very bottom from the very top.
This structure is particularly useful in large UI systems for advancing the style flow between components in a controlled, chained manner.
Result: Theming::part() is the backbone of the theme systems in modern web component libraries ( Shoelace, Ionic, etc.).
Through this architecture, components transform from rigid designs into dynamic structures shaped by themes.
It grants developers the freedom to fully customize a component simply by writing CSS, without modifying the component's source code (akin to a No-Code / Low-Code logic).
This approach also significantly simplifies the implementation of global theme management within design systems.
In short; ::part() and exportparts work in tandem to establish a flexible and scalable style architecture even within an encapsulated component world.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>::part() Demo - Shadow DOM Gateways</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<h1>🎯 <code>::part()</code> - Gateways of the Shadow DOM Structure</h1>
<p>With <code>::part()</code>, you can style <strong>controlled areas</strong> inside a component from the outside.
</p>
<!-- EXAMPLE 1: Default component -->
<h3>📦 Example 1: Component customized via ::part()</h3>
<custom-card></custom-card>
<!-- EXAMPLE 2: Component with a different theme applied -->
<h3>🎨 Example 2: Dark theme (via Light DOM CSS)</h3>
<div class="theme-dark">
<custom-card></custom-card>
</div>
<!-- EXAMPLE 3: Multiple components on the same page -->
<h3>🔄 Example 3: Same component, different styles</h3>
<custom-card style="display: inline-block; margin-right: 20px;"></custom-card>
<custom-card style="display: inline-block;"></custom-card>
<hr style="margin: 40px 0;">
<!-- INFO BOX -->
<div style="background: #f0f4f8; padding: 20px; border-radius: 16px; font-family: monospace;">
<h3>📘 ::part() Fact Sheet</h3>
<ul style="line-height: 1.8;">
<li>✅ <strong>Selectable:</strong> <strong>Any element</strong> marked with <code>part="name"</code></li>
<li>✅ <strong>Pseudo-class chaining:</strong> <code>::part(btn):hover</code>,
<code>::part(input):focus</code> ✅
</li>
<li>❌ <strong>Non-selectable:</strong> Child elements within a part (<code>::part(box) span</code> ❌)</li>
<li>✅ <strong>Multiple names:</strong> Can be used as <code>part="button primary"</code></li>
<li>🔗 <strong>Advanced:</strong> <code>exportparts</code> is used for nested components</li>
<li>🎯 <strong>Difference from ::slotted():</strong> <code>::slotted()</code> styles PROJECTED content,
<code>::part()</code> styles the component's OWN internal pieces
</li>
</ul>
</div>
<div
style="margin-top: 20px; padding: 16px; background: #fff3cd; border-left: 4px solid #ffc107; border-radius: 8px;">
<strong>💡 Note:</strong> In this example, <strong>5 different gateways</strong> have been opened: <code>part="icon"</code>, <code>part="card-header"</code>,
<code>part="card-body"</code>, <code>part="badge"</code>, and <code>part="submit-btn"</code>.
You can modify each one from the Light DOM using <code>custom-card::part(name)</code>.
</div>
<script src="script.js?v=1.0.150"></script>
</body>
</html>
/* Target the part="submit-btn" of the component */
custom-card::part(submit-btn) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
padding: 12px 28px;
font-size: 1rem;
font-weight: bold;
border-radius: 50px;
color: white;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
/* Pseudo-class chaining on hover - AMAZING FEATURE! */
custom-card::part(submit-btn):hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
/* Active (click) state */
custom-card::part(submit-btn):active {
transform: translateY(1px);
}
/* Target part="icon" */
custom-card::part(icon) {
width: 24px;
height: 24px;
fill: #ff6b6b;
transition: fill 0.2s, transform 0.2s;
vertical-align: middle;
margin-right: 8px;
}
/* Icon hover effect */
custom-card::part(icon):hover {
fill: #ee5a24;
transform: scale(1.1);
}
/* Target part="card-header" */
custom-card::part(card-header) {
font-size: 1.8rem;
font-weight: 800;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
border-left: 4px solid #764ba2;
padding-left: 16px;
}
/* Style part="card-body" content */
custom-card::part(card-body) {
font-size: 1rem;
line-height: 1.6;
color: #2d3748;
background: #f7fafc;
padding: 16px;
border-radius: 12px;
}
/* Customize part="badge" */
custom-card::part(badge) {
background: #48bb78;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
display: inline-block;
}
/* ⚠️ This will NOT work - children inside a part are inaccessible */
/* custom-card::part(card-body) span { color: red; } -> DOES NOT WORK! */
/* Target multiple components simultaneously */
.theme-dark::part(card-header) {
background: linear-gradient(135deg, #f093fb, #f5576c);
-webkit-background-clip: text;
background-clip: text;
}
.theme-dark::part(card-body) {
background: #2d3748;
color: #e2e8f0;
}
.theme-dark::part(submit-btn) {
background: linear-gradient(135deg, #f093fb, #f5576c);
}
class CustomCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
// Component internal structure - gateways opened via part attributes
shadow.innerHTML = `
<style>
/* Shadow DOM internal styles - default appearance */
.card {
border: 2px solid #e2e8f0;
border-radius: 20px;
padding: 20px;
background: white;
font-family: system-ui, -apple-system, sans-serif;
max-width: 450px;
margin: 20px 0;
transition: all 0.3s;
}
.card-header {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 12px;
color: #2d3748;
}
.card-body {
margin: 16px 0;
color: #4a5568;
}
.btn {
background: #cbd5e0;
border: none;
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}
.icon-svg {
width: 20px;
height: 20px;
fill: #a0aec0;
}
.badge {
background: #e2e8f0;
color: #4a5568;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.7rem;
}
.flex-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 16px;
}
</style>
<div class="card">
<div class="flex-row">
<!-- GATEWAYS OPENED VIA PART - Controlled access for the outside world -->
<div part="card-header" class="card-header">
<svg part="icon" class="icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M12 2L15 8.5L22 9.5L17 14L18.5 21L12 17.5L5.5 21L7 14L2 9.5L9 8.5L12 2Z" fill="currentColor"/>
</svg>
Welcome
</div>
<span part="badge" class="badge">NEW</span>
</div>
<div part="card-body" class="card-body">
This is a Shadow DOM component. Areas marked with the <strong>part="..."</strong> attribute
are exposed to the outside world in a controlled manner.
</div>
<button part="submit-btn" class="btn">✨ Explore</button>
</div>
`;
}
}
customElements.define("custom-card", CustomCard);
CSS ::-webkit-scrollbar The Key That Starts the System
When you scroll down a web page, the gray bar that appears on the right doesn't actually belong to your site; it is a part of the user's Operating System
(Windows, macOS, Android).
Even though this bar is rendered by the browser, its appearance is largely determined by operating system and user settings.
This is why it looks different on every computer. The ::-webkit-scrollbar pseudo-element allows you to cut this external dependency and take full control via CSS.
The moment you use this selector, the browser removes the default scrollbar and creates a blank canvas waiting for your instructions.
Now, the scrollbar is no longer just a system component; it becomes a part of your site's design language.
Especially in modern interfaces, even the scrollbar can be used as an element reflecting brand identity in terms of color, thickness, and interactive feel.
However, this power comes with responsibility; a poorly designed scrollbar can significantly degrade usability.
Critical Warning: Non-Standard StructureThe -webkit- prefix in its name indicates that this is not a W3C standard.
This means the feature is not guaranteed to function identically across all browsers.
Supported By: Chrome, Edge, Safari, Opera, and all Chromium-based browsers ( Over 80% of the global market ).
This wide support makes the feature highly usable in practice; however, it remains non-universal.
Not Supported By: Firefox (Firefox uses its own, more limited scrollbar-color standard).
Therefore, the best approach is to treat scrollbar styling as an enhancement layer and avoid making critical user experiences dependent on it.
In short; ::-webkit-scrollbar is a powerful tool that must be used with caution: you have control, but compatibility is not always guaranteed.
Vertical Scrollbar Anatomy The ::-webkit-scrollbar family (WebKit / Chromium)
Sizing and Activation From Invisibility to Existence
If you do not provide a width ( width ) or height ( height ) to the ::-webkit-scrollbar selector, the scrollbar will remain invisible because its default size is "zero"—this is one of the most critical rules of this selector family.
No matter how much you style the other parts ( thumb, track ), they will not be rendered on the screen if the main container has no dimensions.
This behavior demonstrates that the scrollbar system operates on a container logic: the area must be defined first before the internal components can be rendered.
In other words, ::-webkit-scrollbar is actually the base skeleton structure required for all sub-parts to exist.
If this skeleton is not established, the sub-parts will never be seen by the user, even if they are technically defined in your CSS.
width: Determines the thickness of the vertical scrollbar.
height: Determines the thickness of the horizontal scrollbar.
These values are not just visual preferences; they are critically important for usability.
Extremely thin scrollbars may look aesthetic, but they can make interaction difficult, especially on touch devices or with low-precision inputs.
Conversely, overly thick scrollbars can narrow the content area and create an unnecessary visual burden.
Therefore, the ideal approach is to strike a balance between design and accessibility.
In short; making the scrollbar visible is only the beginning; what truly matters is presenting it in the right size and the right context.
|
CSS Property
|
scrollbar
|
track
|
thumb
|
button
|
|---|---|---|---|---|
|
background-color
|
|
|
|
|
|
background-image
(gradient, pattern)
|
|
|
|
|
|
border-radius
|
(limited)
|
|
|
|
|
box-shadow
|
|
|
|
|
|
:hover
pseudo-class
|
|
|
|
|
|
:active
pseudo-class
|
|
|
|
|
|
CSS Selector
|
Scope
|
Use Case
|
|---|---|---|
|
::-webkit-scrollbar
(Global Selector)
|
Global
Affects the entire document |
Theme/Design System Consistent scrollbar across the whole page |
|
.element::-webkit-scrollbar
Ex: .chat, .modal
|
Local
Only the targeted element |
Custom Components Such as chat, modal, sidebar |
|
body::-webkit-scrollbar
(Body Element)
|
Scoped
Only the body element |
Main Scrollbar Customizing the main window scrollbar |
|
*::-webkit-scrollbar
(Universal Selector)
|
All Elements
Every scrollable element |
Radical Theming Performance risk (Rarely used) |
The Mobile Device Paradox Touchscreens and Vanishing Bars
Modern mobile operating systems like iOS and Android hide scrollbars by default to avoid consuming screen real estate, showing them only during the act of scrolling ( Auto-hiding ).
This approach is a space optimization strategy designed to provide the maximum possible content area on mobile devices.
Furthermore, since users on touchscreens perform scrolling directly on the content with finger gestures, the need for a persistent visual scrollbar is much lower than on desktops.
Consequently, the scrollbar on mobile acts more as a temporary feedback tool rather than a primary control element.
If you define custom styles using ::-webkit-scrollbar, you might break this "auto-hide" feature and force the bar to remain permanently on the screen.
This can create an unwanted sense of cramped space in mobile designs and negatively impact the user experience.
Especially on small screens, a constantly visible scrollbar narrows the content area and can lead to visual clutter.
Additionally, such customizations can lead to unexpected performance issues on certain mobile browsers.
Therefore, it is generally recommended to use the @media (hover: hover) query to apply these customizations only to devices with a mouse ( Desktop ).
Alternatively, queries like @media (pointer: fine) can be used to create more precise control scenarios.
In short; rather than designing a custom scrollbar for mobile, the best design decision is often to leave it untouched.
The Scroll Path (Track) ::-webkit-scrollbar-track
This selector represents the fixed and stationary background of the scrollbar. The handle (Thumb) moves along this track.
This structure can be thought of as a physical rail system: the track defines the direction, while the thumb progresses along this path.
The track is more than just a blank foundation; in modern interfaces, it acts as a "breathing" buffer zone between the page content and the scrollbar.
It also serves as a passive guide, showing the user the boundaries of the scrollable area.
Therefore, track design is a crucial element that guides the general user experience from the background, even if it isn't noticed directly.
Design Tip: Perception of DepthThe Inset Shadow mentioned in the previous table is the secret to professional UI design.
Using
box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
instead of a flat color creates the feeling that the scrollbar is "inside" the page rather than "on
top" of it.
This small touch adds depth, layering, and a sense of physical reality to the interface.
Especially in flat designs, such subtle details make the component look more professional and "finished."
The Track is Non-Interactive: Why Hover Doesn't Work?::-webkit-scrollbar-track is not an interactive element.
State-based pseudo-classes like :hover, :active, or :focus are not supported by browsers for this element, or they function inconsistently.
The technical reason for this is that the track is purely a "foundation" or "rail," designed as a decorative layer that does not expect input from the user.
The track provides reference rather than interaction; it shows the user where they are but does not communicate with them directly.
Interaction ( color changes, scaling, etc. ) only works on the mobile part, the ::-webkit-scrollbar-thumb.
This distinction provides a clear distribution of roles in the user experience: the track provides direction, while the thumb provides control.
|
Property Group
|
Usage and Impact
|
|---|---|
|
Background
|
You can use patterns or gradients (
linear-gradient ) instead of just a solid
background-color.
|
|
Depth (Shadows)
|
box-shadow: inset
... is the most popular technique.
|
|
Rounding (Radius)
|
You can make the ends of the track
oval using
border-radius.
|
|
Borders
|
You can clearly separate the track
from the
content by drawing a thin line around it.
|
|
Strategy
|
Code Example
|
Final Result
|
|---|---|---|
|
Vertical Spacing (Margin)
|
margin: 4px 0;
|
The track does not stick to the top
and bottom
edges of the container.
|
|
Horizontal Spacing (Margin)
|
margin: 0 4px;
|
The horizontal bar is pulled inward from the left and right edges, creating a "floating" effect. |
|
Minimal Design
|
background: transparent;
(track is hidden)
|
Only the thumb is visible; the track
remains
transparent.
|
The Scroll Handle (Thumb) ::-webkit-scrollbar-thumb
This is the heart of the scrollbar system. It is the moving part that the user grabs with the mouse and drags up or down to indicate their position on the page.
While the Track (foundation) is a passive guide, the ::-webkit-scrollbar-thumb is the active component that provides direct control.
Consequently, the thumb is the primary interaction point between the user and the scrollbar, requiring the most design attention.
From a design perspective, this is your area of greatest freedom; it is an interactive element that communicates directly with the user.
Therefore, thumb design should prioritize not only aesthetics but also accessibility and usability.
Even if you set the scrollbar width to width: 12px, you might want the handle to appear slimmer (e.g., 8px).
The cleanest way to achieve this is to give the thumb a transparent border and apply background-clip: content-box.
With this technique, the visual area is minimized while the physical hit area is preserved, resulting in a more elegant look.
Simultaneously, this method provides a wider clickable area without the user even noticing.
On very long pages, the thumb can shrink to a tiny dot, making it impossible to grab.
This creates a serious accessibility problem, especially for desktop users.
To prevent this, the min-height property should always be used (e.g., min-height: 40px;).
This value guarantees a grabbable area for the user at all times and ensures a stable experience.
Auto-Sizing: Viewport/Content RatioThe height (or width in horizontal) of the thumb is calculated automatically:
Formula: Thumb size = (Visible area / Total content) × Scrollbar length
This calculation serves as a visual ratio indicator that tells the user their current position within the page.
The longer the content, the smaller the thumb becomes; the shorter the content, the larger it grows.
This means you cannot define a fixed height or width via CSS; you can only guarantee a minimum size using min-height / min-width.
In short; thumb design is not just a matter of style—it is a factor that directly impacts the user's navigation experience.
|
Styling Tool
|
Usage and Purpose
|
|---|---|
|
background-color
|
Sets the handle color.
|
|
background-image
|
Used for gradients or patterns.
|
|
border-radius
|
The most critical property.
|
border
|
Pro Tip: By using a
transparent
border, you can make the handle appear
"shrunken" from the edges
|
|
box-shadow
|
Shadow effects.
|
|
transform (limited)
|
scale() can
be used for expansion effects on hover.
|
|
State
|
Code Example
|
Visual Effect
|
|---|---|---|
|
Default
|
background: #ccc;
|
A calm, non-distracting gray tone. |
|
:hover
|
background: #888;
|
Darkens when hovered, signaling a "grabbable" state. |
|
:hover + transform
|
transform: scale(1.05);
|
Slight expansion effect. Adds a sense of "liveliness" to the UI. |
|
:active
|
background: #555;
|
Becomes darkest while clicking and
dragging.
|
|
:active + shadow
|
box-shadow: 0 0 8px rgba(0,0,0,0.3);
|
Shadow appears upon clicking.
|
Directional Buttons ::-webkit-scrollbar-button
These are the buttons located at the start and end points of the scrollbar (Track) that scroll the content in small increments when clicked.
While the track defines direction and the thumb indicates position, these buttons provide step-by-step navigation.
This structure offers extra control to the user, especially in scenarios requiring precision scrolling ( long lists, code editors, data tables ).
By default, they appear as "Up/Down Arrows" in Windows, whereas they are typically hidden in macOS and modern mobile designs.
This is because modern UI philosophy often views these buttons as unnecessary visual noise, since users have grown accustomed to direct scrolling methods.
( wheel / touch ).
This selector allows you to bring those arrows back, change their shape, or remove them entirely.
However, in practice, most modern projects either hide these buttons completely or style them very minimally.
Critical Logic: Increment and DecrementThe most common point of confusion when styling these buttons is their directionality.
CSS does not use terms like "Up/Down"; it employs mathematical logic:
Decreasing ( :decrement ) This is the direction that decreases the scroll value, representing "Up" vertically and "Left" horizontally. ( The Starting Point ).
Increasing ( :increment ) This is the direction that increases the scroll value, representing "Down" vertically and "Right" horizontally. ( The Ending Point ).
This logic provides a consistent system independent of the scrollbar's orientation, functioning identically for both horizontal and vertical usage.
The Invisibility Issue and Its SolutionWhen you write the ::-webkit-scrollbar-button selector, you might not see anything on the screen initially.
This is because these buttons are empty boxes by default; the browser only reserves the space and expects you to provide the content.
To make them visible, you must either assign a background-color or a
background-image (usually an arrow icon).
Furthermore, the icon's alignment must be handled correctly using properties like background-size and background-position.
In short; while these buttons are powerful control tools, they have become optional and rarely used components in modern interfaces.
|
Selector Combination
|
Targeted Button
|
|---|---|
|
::-webkit-scrollbar-button
|
Selects all directional buttons ( up, down, right, left ) simultaneously. |
|
:vertical:decrement
|
The button at the Very Top of the vertical bar ( Up Arrow ). |
|
:vertical:increment
|
The button at the Very Bottom of the vertical bar ( Down Arrow ). |
|
:horizontal:decrement
|
The button on the Far Left of the horizontal bar ( Left Arrow ). |
|
:horizontal:increment
|
The button on the Far Right of the horizontal bar ( Right Arrow ). |
|
Technique
|
Description
|
|---|---|
|
Block Coloring
|
Simply applying background-color: red; colors the button
area entirely.
|
|
Image/SVG Usage
|
The most common method.
|
|
Hiding (Modern Style)
|
Modern interfaces often prefer no
buttons.
|
The Corner Area ::-webkit-scrollbar-corner
When an element has both vertical and horizontal scrollbars active, this is the small square area formed at the bottom-right corner where the two bars intersect.
This area is the smallest yet most critical junction point of the scrollbar system, as it visually bridges two different axes.
While the track defines direction, the thumb handles movement, and the buttons provide navigation, the corner fills the complementary void of this system.
By default, it is usually gray or matches the page background color, often going unnoticed by most designers.
However, in custom scrollbar designs, this corner gap can result in drafting errors or color inconsistency.
This becomes particularly evident in interfaces utilizing dark themes, gradient backgrounds, or glassmorphism effects.
Critical Note: Dual Scrollbar RequirementThis selector is effective only and only if both vertical and horizontal scrollbars are visible at the same time.
This scenario typically arises in large data tables, code editors, or fixed-dimension containers.
If your element has only vertical or only horizontal scrolling, this corner area does not exist, and any styling applied to it will have no effect.
Therefore, this pseudo-element gains significance in specific scenarios rather than in every project.
Design Pitfall: Color InconsistencyThe most common issue is that when you customize the track and thumb, the corner remains in its default gray color.
This creates a visual disconnect from the rest of the scrollbar.
For example, if you've designed a light gray scrollbar for a dark theme application (color #222), the gray corner remains as an eyesore.
This minor detail directly impacts the perception of professionalism in an interface, even if the user doesn't consciously notice it.
Final Word: Unnecessary or Essential?::-webkit-scrollbar-corner is often viewed as "over-engineering."
However, this is a superficial perspective; in modern UI, details define the overall perception.
In premium UI designs and dark/light theme transitions, this small detail creates the difference between professionalism and amateurism.
Rule: If you have given the track a custom background, always style the corner as well or make it transparent.
In short; the corner is a minor detail, but when used correctly, it ensures the interface looks polished and consistent.
|
Scenario
|
Appearance and State
|
|---|---|
|
Wide Tables
|
In wide Excel-like tables, both
horizontal and
vertical scrolling become active.
|
|
Code Editors
|
Long code lines require horizontal
scrolling,
while numerous lines necessitate vertical scrolling.
|
|
Responsive Design
|
Dual scrollbars appear on small
screens when
content overflows both axes.
|
|
Image Galleries
|
The corner becomes prominent when there is a horizontal image sequence and a vertical category list. |
|
Technique
|
CSS Code
|
Effect
|
|---|---|---|
|
Color Matching
|
background: #f0f0f0; |
Matches the corner with the track
color.
|
|
Transparency
(transparent)
|
background: transparent; |
Completely hides the corner.
|
|
Gradient Corner
|
background: linear-gradient(135deg, #f0f0f0, #e0e0e0); |
Provides a sense of depth.
|
|
Corner Icon
(advanced)
|
background: url('corner-icon.svg') center no-repeat; |
Used very rarely.
|
Track Pieces ::-webkit-scrollbar-track-piece
This selector allows you to divide the scroll path ( track ) into two primary segments: the areas above and beyond the thumb.
While the standard ::-webkit-scrollbar-track treats the entire path as a single unit, this selector enables you to style the before and after areas of the thumb independently.
This approach transforms the scrollbar from a passive component into an active visual tool that conveys state information.
It is ideal for creating "progress bar" effects or "reader mode" indicators.
For Example: If a user is 60% through a page, the track piece above the thumb can be colored differently to visualize reading progress.
This technique is highly effective for user guidance on blogs, documentation sites, and long-form content pages.
Critical Concept: start and endThe two most important pseudo-classes for the track-piece are:
- :start The piece in the starting direction of the scroll. Vertically, this is above the thumb; horizontally, it is to the left.
- :end The piece in the ending direction of the scroll. Vertically, this is below the thumb; horizontally, it is to the right.
This structure allows you to create a two-way state distinction centered around the thumb.
Consequently, the track is no longer just a monochromatic area but is divided into past and future segments.
Warning: Browser InconsistencyThe :start and :end pseudo-classes do not always function consistently across all WebKit browsers.
Therefore, this feature should be used primarily for visual enhancement rather than for critical UI components.
- Good Support: Chrome 90+, Edge 90+, Safari 14+
- Limited Support: Older Chrome/Edge versions
- No Support: Firefox (always)
Thus, a fallback strategy must be considered in production environments.
Performance NoteTrack-piece styles are constantly recalculated and redrawn as the thumb moves.
This essentially turns the scrollbar into a small animation system.
- Use: Simple colors and linear gradients.
- Avoid: Complex patterns, high-resolution images, and blur effects.
Heavy styling can degrade scroll smoothness, particularly on lower-performance devices.
Best Practice: Use this only when truly necessary. For most UIs, a standard track is sufficient.
::-webkit-scrollbar-track-piece is a powerful tool for those who wish to transform the scrollbar into an informative visual layer, though it must be used with care.
|
State
|
CSS Targeting
|
Visual Impact
|
|---|---|---|
|
At Page Top
|
:start { background: green; }
:end { background: gray; }
|
Thumb is at the very top.
|
|
In Mid-Page
|
:start { background: green; }
:end { background: orange; }
|
Thumb is centered.
|
|
At Page Bottom
|
:start { background: green; }
:end { background: transparent; }
|
Thumb is at the very bottom.
|
|
Combination
|
Full Selector
|
Targeted Area
|
|---|---|---|
|
Base Segment
|
::-webkit-scrollbar-track-piece
|
ALL track segments except the thumb.
|
|
Direction + Piece
|
:vertical::-webkit-scrollbar-track-piece:start
|
Only the segment above the thumb on a vertical scrollbar. |
|
Dual-Axis Piece
|
:horizontal::-webkit-scrollbar-track-piece:end
|
Only the segment to the right of the thumb on a horizontal scrollbar. |
|
No Button State
|
:no-button::-webkit-scrollbar-track-piece
|
Track segments when buttons are not present (common in modern UI designs). |
|
Application
|
:start Style
|
:end Style
|
|---|---|---|
|
Video Player
(timeline)
|
background: #3b82f6;
|
background: #94a3b8;
|
|
Form Wizard
(step indicator)
|
background: linear-gradient(...);
|
background: #e2e8f0;
|
|
Music Player
(playlist)
|
background: #ef4444;
|
background: #f3f4f6;
|
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>::-webkit-scrollbar - Scrollbar Özelleştirme Demosu</title>
<link rel="stylesheet" href="style.css?v=1.0.150">
</head>
<body>
<div class="container">
<h1>🎨 <code>::-webkit-scrollbar</code> Demo</h1>
<div class="scroll-demo">
<div class="horizontal-content">
<!-- BİLGİ KUTUSU - Scrollbar Anatomisi -->
<div class="info-box">
<strong>📐 Scrollbar Anatomisi</strong><br>
Bu sayfada aşağıdaki tüm pseudo-elementler özelleştirilmiştir:
<div class="anatomy">
<span class="anatomy-item">::-webkit-scrollbar</span>
<span class="anatomy-item">::-webkit-scrollbar-track</span>
<span class="anatomy-item">::-webkit-scrollbar-thumb</span>
<span class="anatomy-item">::-webkit-scrollbar-button</span>
<span class="anatomy-item">::-webkit-scrollbar-corner</span>
<span class="anatomy-item">::-webkit-scrollbar-track-piece:start</span>
<span class="anatomy-item">::-webkit-scrollbar-track-piece:end</span>
</div>
</div>
<!-- KART 1 -->
<div class="card">
<h3>🎯 <code>::-webkit-scrollbar-track-piece</code></h3>
<p><span class="badge badge-green">İlerleme Göstergesi</span> <span class="badge">:start /
:end</span></p>
<p>Scrollbar'da thumb'un <strong>üstündeki alan yeşil</strong>,
<strong>altındaki alan gri</strong> olarak renklendirilmiştir.
</p>
<p>⬇️ <strong>Aşağı kaydırdıkça</strong> yeşil alan büyür, gri alan küçülür!</p>
</div>
<!-- KART 2 -->
<div class="card">
<h3>🖱️ <code>::-webkit-scrollbar-thumb</code></h3>
<p><span class="badge">Gradient Renk</span> <span class="badge">Hover + Scale</span> <span
class="badge">min-height:40px</span></p>
<p>Tutamaç mor-mavi gradient renge sahiptir. Üzerine gelince büyür
(<code>transform: scale()</code>),
tıklayınca koyulaşır. <strong>min-height:40px</strong> ile her zaman tutulabilir boyuttadır.</p>
</div>
<!-- KART 3 -->
<div class="card">
<h3>🔘 <code>::-webkit-scrollbar-button</code> & <code>::-webkit-scrollbar-corner</code></h3>
<p><span class="badge">:decrement</span> <span class="badge">:increment</span> <span
class="badge">SVG Icons</span></p>
<p>Scrollbar yapısnın uçlarına SVG ok ikonları eklenmiştir. Ayrıca yatay + dikey scrollbar aynı anda aktif
olduğunda
sağ-alt köşede <strong>gradient renkli corner</strong> görünecektir.</p>
</div>
<!-- KART 4 -->
<div class="card">
<h3>♿ Erişilebilirlik Notu</h3>
<p><span class="badge">min-height:40px</span> <span class="badge">Hover Feedback</span> <span
class="badge">Active State</span></p>
<p>Thumb minimum 40px yükseklikte tutulmuştur (WCAG önerisi). Hover ve aktif durumlarda görsel geri
bildirim
sağlanmıştır.</p>
</div>
<h3>📊 Yatay Scrollbar Örneği (Geniş Tablo)</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>Kullanıcı Adı</th>
<th>E-posta</th>
<th>Şehir</th>
<th>Ülke</th>
<th>Kayıt Tarihi</th>
<th>Son Giriş</th>
<th>Durum</th>
</tr>
</thead>
<tbody>
<tr><td>1</td><td>ahmet_yilmaz</td><td>ahmet@example.com</td><td>İstanbul</td><td>Türkiye</td><td>2024-01-15</td><td>2024-03-20</td><td>Aktif</td></tr>
<tr><td>2</td><td>ayse_demir</td><td>ayse@example.com</td><td>Ankara</td><td>Türkiye</td><td>2024-01-20</td><td>2024-03-19</td><td>Aktif</td></tr>
<tr><td>3</td><td>mehmet_kaya</td><td>mehmet@example.com</td><td>İzmir</td><td>Türkiye</td><td>2024-02-01</td><td>2024-03-18</td><td>Pasif</td></tr>
</tbody>
</table>
<div class="card">
<h3>⚠️ Önemli Uyarılar</h3>
<ul>
<li><strong>Standart Değildir:</strong> <code>::-webkit-scrollbar</code> W3C standardı değildir.</li>
<li><strong>Firefox'ta Çalışmaz</strong></li>
<li><strong>Mobil Uyarısı</strong></li>
</ul>
</div>
</div>
</div>
<div style="padding:16px; text-align:center;">
⚡ <strong>::-webkit-scrollbar</strong> Demo
</div>
</div>
</body>
</html>
/* ============================================ */
/* 1. SCROLLBAR'IN ANA KONTEYNERİ (Görünürlük için zorunlu) */
/* ============================================ */
::-webkit-scrollbar {
width: 14px;
/* Dikey scrollbar kalınlığı */
height: 14px;
/* Yatay scrollbar kalınlığı */
background: #f1f5f9;
/* Fallback renk */
}
/* ============================================ */
/* 2. TRACK (RAY) - Pasif arka plan */
/* ============================================ */
::-webkit-scrollbar-track {
background: linear-gradient(145deg, #e2e8f0 0%, #cbd5e1 100%);
border-radius: 20px;
margin: 4px 0;
/* Dikeyde üst/alt boşluk */
box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.1);
}
/* Yatay track için özel margin */
::-webkit-scrollbar-track:horizontal {
margin: 0 4px;
/* Yatayda sol/sağ boşluk */
}
/* ============================================ */
/* 3. THUMB (TUTAMAÇ) - Hareketli kontrol */
/* ============================================ */
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20px;
border: 3px solid transparent;
background-clip: padding-box;
/* İnce görünüm */
min-height: 40px;
/* Minimum boyut - ERİŞİLEBİLİRLİK */
min-width: 40px;
}
/* Hover efekti - Kullanıcıya "beni tutabilirsin" mesajı */
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #5a67d8 0%, #6b46a0 100%);
transform: scale(1.02);
}
/* Aktif (sürükleme) durumu */
::-webkit-scrollbar-thumb:active {
background: linear-gradient(135deg, #4c51bf 0%, #553c9a 100%);
cursor: grabbing;
}
/* ============================================ */
/* 4. BUTTONS (OKLAR) - Navigasyon butonları */
/* ============================================ */
::-webkit-scrollbar-button {
background: #94a3b8;
border-radius: 12px;
width: 14px;
height: 14px;
background-size: 8px 8px;
background-repeat: no-repeat;
background-position: center;
}
/* Dikey - Yukarı Ok (decrement) */
::-webkit-scrollbar-button:vertical:decrement {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 24 24'%3E%3Cpath d='M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z'/%3E%3C/svg%3E");
margin-bottom: 4px;
}
/* Dikey - Aşağı Ok (increment) */
::-webkit-scrollbar-button:vertical:increment {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 24 24'%3E%3Cpath d='M12 16l-6-6 1.41-1.41L12 13.17l4.59-4.58L18 10z'/%3E%3C/svg%3E");
margin-top: 4px;
}
/* Yatay - Sol Ok (decrement) */
::-webkit-scrollbar-button:horizontal:decrement {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 24 24'%3E%3Cpath d='M16 18l-6-6 6-6-1.41-1.41L7.17 12l7.42 7.41z'/%3E%3C/svg%3E");
margin-right: 4px;
}
/* Yatay - Sağ Ok (increment) */
::-webkit-scrollbar-button:horizontal:increment {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 24 24'%3E%3Cpath d='M8 18l6-6-6-6 1.41-1.41 7.42 7.41-7.42 7.41z'/%3E%3C/svg%3E");
margin-left: 4px;
}
/* Buton hover durumu */
::-webkit-scrollbar-button:hover {
background-color: #64748b;
transform: scale(1.05);
}
/* ============================================ */
/* 5. CORNER (KÖŞE) - Çift scrollbar'da kesişim */
/* ============================================ */
::-webkit-scrollbar-corner {
background: linear-gradient(135deg, #667eea20, #764ba220);
border-radius: 4px;
}
/* ============================================ */
/* 6. TRACK-PIECE (YOL PARÇALARI) - İlerleme göstergesi */
/* ============================================ */
/* Thumb'un ÜSTÜNDEKİ parça (başlangıç yönü) - OKUNAN kısım */
::-webkit-scrollbar-track-piece:start {
background: linear-gradient(145deg, #22c55e, #16a34a);
border-radius: 20px 20px 4px 4px;
}
/* Thumb'un ALTINDAKİ parça (bitiş yönü) - KALAN kısım */
::-webkit-scrollbar-track-piece:end {
background: linear-gradient(145deg, #cbd5e1, #94a3b8);
border-radius: 4px 4px 20px 20px;
}
/* Yatay scrollbar için track-piece özelleştirmesi */
::-webkit-scrollbar-track-piece:horizontal:start {
border-radius: 20px 4px 4px 20px;
}
::-webkit-scrollbar-track-piece:horizontal:end {
border-radius: 4px 20px 20px 4px;
}
/* ============================================ */
/* SAYFA İÇERİĞİ STİLLERİ (Sadece demo amaçlı) */
/* ============================================ */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 24px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
overflow: hidden;
}
/* Scrollbar'ın görünmesi için içerik alanı */
.scroll-demo {
height: 500px;
overflow-y: scroll;
overflow-x: auto;
padding: 24px;
background: #f8fafc;
}
/* Yatay scrollbar'ı tetiklemek için geniş içerik */
.horizontal-content {
width: 1200px;
}
.card {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-left: 4px solid #667eea;
}
h1 {
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
padding: 24px 24px 0 24px;
font-size: 1.8rem;
}
.badge {
display: inline-block;
background: #e2e8f0;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
margin-right: 8px;
}
.badge-green {
background: #22c55e20;
color: #16a34a;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th,
td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
th {
background: #f1f5f9;
font-weight: 600;
}
.info-box {
background: #fef3c7;
border-left: 4px solid #f59e0b;
padding: 16px;
border-radius: 12px;
margin: 20px 0;
font-size: 0.9rem;
}
.anatomy {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 20px 0;
padding: 16px;
background: #1e293b;
border-radius: 16px;
color: white;
}
.anatomy-item {
background: #334155;
padding: 6px 12px;
border-radius: 20px;
font-family: monospace;
font-size: 0.75rem;
}
hr {
margin: 24px 0;
border: none;
border-top: 2px solid #e2e8f0;
}