Skip to main content

8 posts tagged with "CSS"

View all tags

Fixing 4 CSS Conflicts Between WooCommerce and FSE Block Themes

· 5 min read

While developing a WordPress FSE Block Theme with WooCommerce integration, I encountered 4 hidden traps where WooCommerce silently overrides theme styles. Each one was difficult to diagnose. Here are the root causes and solutions to help other FSE + WooCommerce theme developers.

TL;DR

  1. Font size preset override: WooCommerce registers small/medium/large/x-large presets that override theme.json. Use custom slugs like h-1~h-6 to avoid conflicts.
  2. Font-size class hyphenation: has-h-1-font-size(48px) vs has-h1-font-size(20px). Hand-written HTML must use the hyphenated version.
  3. Contrast color inversion: --wp--preset--color--contrast is set to a light color (#f8fafc) by WooCommerce, making white text invisible.
  4. ul.products pseudo-elements: WooCommerce injects ::before/::after that break CSS Grid layout. Fix with display:none.

Trap 1: Font Size Presets Overridden

Symptom

Defined font size presets in theme.json:

{
"settings": {
"typography": {
"fontSizes": [
{ "slug": "small", "size": "0.875rem" },
{ "slug": "large", "size": "1.125rem" }
]
}
}
}

After installing WooCommerce, paragraphs using has-large-font-size render at 36px instead of the expected 18px. H1 headings may also shrink from 48px to 20px.

Root Cause

WooCommerce registers its own font size presets via WP_Theme_JSON_API:

slugtheme.json originalWooCommerce override
small0.875rem (14px)13px
medium1rem (16px)20px
large1.125rem (18px)36px
x-large1.25rem (20px)42px

Presets with the same slug get overridden. Custom slugs (like h-1~h-6, base) are unaffected.

Solution

Use custom slugs in theme.json to avoid WooCommerce conflicts:

{
"fontSizes": [
{ "slug": "h-1", "size": "3rem" },
{ "slug": "h-2", "size": "2.25rem" },
{ "slug": "h-3", "size": "1.75rem" },
{ "slug": "base", "size": "16px" }
]
}

Use has-h-5-font-size instead of has-large-font-size in templates.

Trap 2: Font-Size Class Hyphenation

Symptom

When hand-writing FSE template HTML, H1 headings render at 20px instead of the theme.json value of 48px:

<!-- Wrong: renders at 20px -->
<h1 class="has-h1-font-size">Heading</h1>

<!-- Correct: renders at 48px -->
<h1 class="has-h-1-font-size">Heading</h1>

Two classes differ by one hyphen, but the rendered size differs by 2.4x.

Root Cause

Gutenberg renders fontSize: "h1" slug as has-h-1-font-size (hyphen added before the number). This is correct behavior.

But when WooCommerce overrides the font size system, has-h1-font-size (no hyphen) matches WooCommerce's override value (20px), while has-h-1-font-size (with hyphen) matches the theme.json original (48px).

Verify in browser console:

const el = document.createElement('div');
document.body.appendChild(el);

el.className = 'has-h1-font-size';
console.log('No hyphen:', getComputedStyle(el).fontSize); // 20px (wrong)

el.className = 'has-h-1-font-size';
console.log('With hyphen:', getComputedStyle(el).fontSize); // 48px (correct)

document.body.removeChild(el);

Solution

Always use the hyphenated format in hand-written FSE template HTML:

<!-- wp:heading {"fontSize":"h1"} -->
<h1 class="has-h-1-font-size">Heading</h1>
<!-- /wp:heading -->

<!-- wp:heading {"fontSize":"h3"} -->
<h2 class="has-h-3-font-size">Section Title</h2>
<!-- /wp:heading -->

Rule: has-h-{N}-font-size (with hyphen), applies to all h-* presets.

Trap 3: Contrast Color Inversion

Symptom

White text on a contrast background is invisible:

<div class="has-contrast-background-color">
<h2 class="has-base-color">White text on dark background</h2>
</div>

Expected contrast to be dark, but it renders as a very light background.

Root Cause

WooCommerce registers --wp--preset--color--contrast as #f8fafc (very light gray). This is WooCommerce's design intent (their default theme uses contrast as a light background section), but conflicts with most FSE themes where contrast means dark.

Solution

Use primary (dark) as text color on contrast backgrounds:

<div class="has-contrast-background-color">
<h2 class="has-primary-color has-text-color">Dark text on light background</h2>
</div>

Or define your own dark preset in theme.json (e.g. surface) instead of relying on contrast:

{
"settings": {
"color": {
"palette": [
{ "slug": "surface", "color": "#0f172a", "name": "Surface" }
]
}
}
}

Trap 4: ul.products Pseudo-Elements Break CSS Grid

Symptom

WooCommerce product listings using CSS Grid show misaligned cards with empty gaps:

ul.products {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}

Expected 3 equal columns, but empty slots appear and cards wrap to the next row.

Root Cause

WooCommerce injects ::before and ::after pseudo-elements on ul.products:

ul.products::before,
ul.products::after {
display: table;
content: '';
}

ul.products::after {
clear: both;
}

These pseudo-elements are designed as clearfix for traditional float layouts. But CSS Grid treats them as grid items, occupying two implicit grid cells.

Solution

Hide pseudo-elements for Grid layouts on ul.products:

ul.products {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}

ul.products::before,
ul.products::after {
display: none;
}

Important Notes

  • Each trap relates to WooCommerce's WP_Theme_JSON_API registration or CSS injection. Disabling WooCommerce removes these overrides.
  • Traps 1 and 2 are linked: after WooCommerce overrides font size presets, hyphenated and non-hyphenated classes map to different values.
  • Trap 3's contrast value may change across WooCommerce versions. Use a custom slug (e.g. surface) for stability.
  • Before hand-writing FSE templates, inspect Gutenberg's auto-rendered class names in DevTools. Don't guess class name formats.

Deploying a WooCommerce Site?

Recommended cloud hosting providers


Fix Hover Selector Penetration in Nested FSE Group Blocks

· 3 min read

Encountered this issue while developing a WordPress FSE Block Theme for a client: blog listing cards show text shifting on hover while the card border stays still. Here's the root cause and solution.

TL;DR

Generic card hover selector .wp-block-columns .wp-block-column > .wp-block-group:hover matches the inner nested text group inside cards, causing text-only displacement. Fix: use .wp-block-post-template prefix to reset hover effects on inner groups.

Symptom

Blog listing uses card-style layout -- outer bordered group wrapping an image + text group. On hover:

  • Inner title and excerpt text shifts by translateY(-4px)
  • Outer card border and shadow remain unchanged
  • Looks like text "floats out" of the card

Root Cause

FSE card patterns use nested HTML structure:

<!-- Outer card group (has border) -->
<div class="wp-block-group has-border-color ...">
<img ... /> <!-- Featured image -->

<!-- Inner text group -->
<div class="wp-block-group" style="padding:...">
<h2>Post Title</h2>
<p>Excerpt text...</p>
</div>
</div>

The theme defines a generic hover effect:

.wp-block-columns .wp-block-column > .wp-block-group:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1);
}

This selector is intended to lift the entire card. But in blog listing pages, the post-template block wraps cards inside wp-block-columns, so the selector also matches the inner text group (which is also .wp-block-column > .wp-block-group).

Since the inner group has no border or shadow, only text displacement is visible, while the outer card remains static.

Solution

Two-step approach: reset inner layer + add separate hover to outer layer.

1. Reset hover on inner groups

Use .wp-block-post-template prefix for higher specificity, zeroing out all hover effects on inner groups:

/* Reset hover for inner groups inside post template cards */
.wp-block-post-template .wp-block-columns .wp-block-column > .wp-block-group {
transition: none;
}
.wp-block-post-template .wp-block-columns .wp-block-column > .wp-block-group:hover {
transform: none;
box-shadow: none;
}

2. Add dedicated hover to outer card

Outer cards have .has-border-color class -- use it for precise targeting:

/* Blog card (outer bordered group) -- hover lift + shadow */
.wp-block-post-template .wp-block-group.has-border-color {
transition:
transform 0.3s ease-out,
box-shadow 0.3s ease-out;
}
.wp-block-post-template .wp-block-group.has-border-color:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.08);
}

Why not narrow the generic selector?

The generic selector is shared across multiple patterns (features-grid, pricing, testimonial, etc.) where groups are single-level -- no nesting penetration issue. Narrowing the generic selector would break hover effects on those patterns.

Resetting inner layers is more reliable because:

  • It doesn't affect other patterns using the generic hover
  • It precisely targets the problematic nesting scenario
  • Complete CSS-level isolation without any HTML/PHP changes

Takeaway

Before adding hover effects in FSE Block Themes:

  1. Inspect the actual HTML nesting structure of your patterns
  2. Verify selectors only target the intended element (outer container), not nested groups
  3. When multiple patterns share a selector, prefer "reset inner" over "restrict outer"

Interested in similar solutions? Get in touch

Fix CSS ::before Pseudo-element Decorative Patterns Covering Buttons

· 3 min read

TL;DR

When using ::before pseudo-elements for container background decorations (dots/grid), you must set opacity, pointer-events: none, and z-index: -1 together. Missing opacity causes 100% opaque patterns; missing z-index causes patterns to cover buttons and other child elements.

Problem

An FSE theme's CTA banner section used a ::before pseudo-element to render decorative dot patterns. The expected effect was a subtle background texture, but the actual result was fully opaque dots covering the button surface:

/* Broken code — pattern is fully opaque and covers children */
.has-dots-pattern::before {
content: "";
position: absolute;
inset: 0;
background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
background-size: 20px 20px;
pointer-events: none;
/* Missing opacity — pattern renders at 100% opacity */
/* Missing z-index — pattern overlays child content */
}

Dense white dots appeared on button surfaces, and obvious grid lines covered the gradient background of the CTA section.

Root Cause

Three critical properties are required for ::before decorative patterns. Missing any one causes issues:

1. opacity — controls pattern transparency

radial-gradient generates solid dots. currentColor inherits the text color. On dark backgrounds, white solid dots are very prominent. Without opacity, the default value is 1, making the pattern fully opaque.

2. z-index: -1 — pushes the pattern behind child elements

::before is set to position: absolute. In the default stacking context, positioned elements paint after normal flow elements. When the container is position: relative, z-index: -1 pushes the pseudo-element behind child content while keeping it visible above the container's own background.

3. pointer-events: none — prevents click interception

This is the easiest to remember because it directly affects interaction. Without it, the pattern layer intercepts click events.

The same project had a correctly implemented reference using a standalone element approach:

/* Correct implementation — standalone element approach */
.cclee-dots-pattern {
position: absolute;
inset: 0;
background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
background-size: 24px 24px;
opacity: 0.08; /* Present */
pointer-events: none;
/* Standalone element controlled by HTML order, no z-index needed */
}

Solution

Add opacity and z-index: -1 to the ::before pseudo-element:

/* Fixed */
.has-dots-pattern {
position: relative;
}
.has-dots-pattern::before {
content: "";
position: absolute;
inset: 0;
background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
background-size: 20px 20px;
opacity: 0.08; /* Added: 8% opacity */
pointer-events: none;
z-index: -1; /* Added: behind child content */
}

Same fix for grid patterns:

.has-grid-pattern {
position: relative;
}
.has-grid-pattern::before {
content: "";
position: absolute;
inset: 0;
background-image:
linear-gradient(currentColor 1px, transparent 1px),
linear-gradient(90deg, currentColor 1px, transparent 1px);
background-size: 40px 40px;
opacity: 0.05; /* Grid is more subtle, 5% opacity */
pointer-events: none;
z-index: -1;
}

Checklist for decorative pattern pseudo-elements:

PropertyPurposeMissing consequence
opacity: 0.05~0.1Controls pattern transparencyPattern is fully opaque, overwhelming
z-index: -1Stacks behind child elementsPattern covers buttons and other children
pointer-events: nonePrevents mouse event interceptionClick-through fails

All three properties must be present together. This is the fixed pattern for ::before decorative overlays.


Interested in similar solutions? Get in touch

Fix FSE Group Block Layout Property Overriding Custom CSS

· 3 min read

TL;DR

WordPress FSE Group blocks with layout property automatically generate is-layout-* CSS classes that have higher specificity than custom CSS, causing size settings to be ignored. Solution: 1) Use "layout":{"type":"default"} in block annotation to avoid extra layout classes; 2) Use !important in CSS to force override; 3) Key: Add padding: 0 !important to clear the Group block's default padding.

Problem

Timeline component year dots should display as 80px circles, but appear as ellipses:

<!-- Size settings in block annotation -->
<!-- wp:group {"style":{"dimensions":{"width":"80px","height":"80px"}},"layout":{"type":"flex",...}} -->
/* Custom CSS */
.cclee-timeline-dot {
width: 80px;
height: 80px;
border-radius: 50%;
}

Neither adjusting CSS nor block attributes fixed the stretched dots.

Root Cause

WordPress FSE Group blocks automatically add layout-related CSS classes based on the layout property:

<div class="wp-block-group cclee-timeline-dot is-layout-flow">

These is-layout-* classes come from WordPress core stylesheets and override custom CSS. Additionally, Group blocks have default padding that increases element dimensions.

Key issues:

  1. layout: {"type": "flex"} generates is-layout-flex class, causing children to be stretched by flexbox
  2. style.dimensions in block annotation becomes inline style, but gets overridden by layout class styles
  3. Group block default padding adds to actual element size

Solution

1. Modify Block Annotation with Default Layout

<!-- wp:group {"className":"cclee-timeline-dot","style":{"border":{"radius":"50%"}},"backgroundColor":"accent","textColor":"base","layout":{"type":"default"}} -->
<div class="wp-block-group cclee-timeline-dot has-base-color has-accent-background-color has-text-color has-background" style="border-radius:50%">

Remove style.dimensions and complex flex layout, use "layout":{"type":"default"} instead.

2. CSS Override + Clear Default Padding

/* Timeline: Fixed circle dot */
.wp-block-group.cclee-timeline-dot {
width: 80px !important;
height: 80px !important;
min-width: 80px !important;
min-height: 80px !important;
flex-shrink: 0 !important;
aspect-ratio: unset !important;
border-radius: 50% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
align-self: center !important;
box-sizing: border-box !important;
text-align: center !important;
padding: 0 !important; /* Key: clear default padding */
}

.wp-block-group.cclee-timeline-dot p {
margin: 0 !important;
white-space: nowrap !important;
line-height: 1 !important;
overflow: visible !important;
}

3. Use :has() to Control Parent Container

Prevent parent Column from being stretched by flexbox:

.wp-block-columns .wp-block-column:has(.cclee-timeline-dot) {
flex-shrink: 0 !important;
flex-basis: 100px !important;
width: 100px !important;
}

Key Finding

padding: 0 !important is the final solution. Group block's default padding stretches the element—even with width/height set, the actual rendered size exceeds expectations.


Interested in similar solutions? Contact us

Align Heading Icons in Docusaurus Docs with Flexbox

· 3 min read

TL;DR

To align SVG icons with text in Docusaurus document headings, use display: flex + align-items: center + gap, combined with the .theme-doc-markdown selector to target docs pages only without affecting blog.

Problem

When using inline SVG icons as heading decorations in Docusaurus docs:

## 🚀 Quick Start

Or via MDX components:

## <RocketIcon /> Quick Start

By default, SVG icons align to the text baseline, appearing visually offset upward:

🚀 Quick Start     ← Icon sits high, aligned to top of text

The traditional approach uses vertical-align: middle + margin-right, but has issues:

  1. Margin needs adjustment when icon size changes
  2. Alignment may break with different line-heights
  3. Multi-line headings have inconsistent alignment

Root Cause

SVG elements are inline by default, participating in inline layout. vertical-align: middle is calculated based on the x-height of the current line, affected by font, line-height, and icon size—making precise control difficult.

A deeper issue is selector scope. Docusaurus applies the .markdown class to both docs and blog pages, so direct modifications affect everything globally.

Solution

1. Use Flexbox Layout

Flexbox align-items: center calculates based on container height, independent of font metrics, providing more stable alignment:

/* Docs page heading icon alignment */
.theme-doc-markdown h1,
.theme-doc-markdown h2,
.theme-doc-markdown h3,
.theme-doc-markdown h4 {
display: flex;
align-items: center;
gap: 0.75rem;
}

2. Reset SVG Original Styles

Override global .markdown styles for margin-right and vertical-align:

.theme-doc-markdown h1 svg,
.theme-doc-markdown h2 svg,
.theme-doc-markdown h3 svg,
.theme-doc-markdown h4 svg {
margin-right: 0;
vertical-align: baseline;
flex-shrink: 0; /* Prevent icon compression */
}

3. Selector Scoping

Docusaurus provides page-specific class names:

SelectorScope
.markdowndocs + blog globally
.theme-doc-markdowndocs pages only
articleblog post pages only

Use .theme-doc-markdown to precisely target docs pages, leaving blog styling untouched.

Complete Code

/* ========== Docs Page Styles ========== */

/* Docs heading icon alignment */
.theme-doc-markdown h1,
.theme-doc-markdown h2,
.theme-doc-markdown h3,
.theme-doc-markdown h4 {
display: flex;
align-items: center;
gap: 0.75rem;
}

.theme-doc-markdown h1 svg,
.theme-doc-markdown h2 svg,
.theme-doc-markdown h3 svg,
.theme-doc-markdown h4 svg {
margin-right: 0;
vertical-align: baseline;
flex-shrink: 0;
}

FAQ

Q: What's the difference between .markdown and .theme-doc-markdown in Docusaurus?

.markdown is Docusaurus's global content styling class, applied to both docs and blog pages. .theme-doc-markdown is a docs-page-specific container class that only affects pages under /docs/* paths, ideal for docs-only styling.

Q: Why use gap instead of margin-right?

gap is a Flexbox/Grid spacing property that works naturally with align-items: center without depending on element margins. When an icon is hidden or absent, gap produces no extra whitespace, whereas margin-right would.

Q: What does flex-shrink: 0 do?

It prevents flex children from shrinking when container space is insufficient. SVG icons typically have fixed dimensions—shrinking would cause distortion and blurriness. Setting flex-shrink: 0 ensures icons maintain their original size.

Fix Tailwind Preflight Resetting Docusaurus Breadcrumbs Styles

· 2 min read

TL;DR

After adding Tailwind CSS to a Docusaurus project, Preflight's CSS Reset strips <ul> elements of their list-style, margin, and padding, breaking the breadcrumbs navigation. Fix by adding explicit override styles in custom.css.

Problem

After integrating Tailwind CSS into Docusaurus, the breadcrumbs navigation on doc pages displays incorrectly:

  • List styles are lost (list-style reset to none)
  • Spacing disappears (margin, padding reset to 0)
  • Layout may break (display may be affected)

Checking browser DevTools, the .breadcrumbs computed styles show these properties are reset by Preflight:

/* Tailwind Preflight reset */
ul, ol {
list-style: none;
margin: 0;
padding: 0;
}

Root Cause

Tailwind Preflight is a CSS Reset based on modern-normalize, injected during the @tailwind base stage. It provides a consistent cross-browser baseline.

The problem: Docusaurus's .breadcrumbs component uses a <ul> element, relying on browser-default flex layout and spacing. Preflight's reset rules have higher specificity and override Docusaurus's default styles.

Since Preflight is injected globally, any third-party component using <ul>/<ol> may be affected.

Solution

Add explicit override styles in src/css/custom.css, using !important for specificity:

/* ========== Breadcrumbs ========== */
.theme-doc-breadcrumbs {
margin-bottom: 1.5rem;
}

.breadcrumbs {
display: flex !important;
flex-wrap: wrap;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
}

.breadcrumbs__item {
display: flex !important;
align-items: center;
gap: 0.5rem;
}

Key points:

  1. .breadcrumbs uses display: flex !important to ensure horizontal layout
  2. list-style: none is expected behavior (breadcrumbs don't need bullets)
  3. .breadcrumbs__item adds gap: 0.5rem for element spacing

FAQ

Q: Why is !important needed?

Tailwind Preflight is injected during @tailwind base, and its selector specificity may match Docusaurus default styles. Using !important ensures custom styles take effect, avoiding specificity wars.

Q: What other components might be affected?

Any component using <ul>/<ol> may be affected, such as:

  • Navigation menus
  • Pagination components
  • Custom lists

How to check: In browser DevTools, search for list-style: none sources and confirm if it comes from Preflight.

Q: Can I disable Preflight?

Yes, but not recommended. In tailwind.config.js:

module.exports = {
corePlugins: {
preflight: false,
},
}

Disabling it means you'll need to handle cross-browser consistency yourself, which may cause more issues.