TechEarl

Inline CSS in WordPress Instead of Enqueuing a Stylesheet

Two ways to inline CSS in WordPress: attach it to an enqueued handle with wp_add_inline_style, or echo a minified <style> block on wp_head. When inlining critical CSS helps, and when it just bloats every page.

Ishan Karunaratne⏱️ 12 min readUpdated
Share thisCopied
How to inline CSS in WordPress instead of enqueuing a stylesheet: use wp_add_inline_style on an enqueued handle, or echo a minified style block on wp_head to drop a render-blocking request for small critical CSS.

If you have a small block of CSS that needs to load before the browser paints, the right move is often to print it inline in the <head> rather than wp_enqueue_style() it as a separate file. A separate stylesheet is a render-blocking HTTP request; for a few KB of critical CSS, that round trip costs you more than the bytes themselves. Here is the minimal version that reads a small CSS file from your plugin, strips the whitespace, and echoes it on wp_head:

php
function te_inline_css() {
    $file = __DIR__ . '/assets/critical.css';
    if ( ! is_readable( $file ) ) {
        return;
    }
    $css = file_get_contents( $file );
    $css = preg_replace( '#\s+#', ' ', trim( $css ) ); // crude minify
    echo '<style id="te-critical-css">' . $css . '</style>' . "\n";
}
add_action( 'wp_head', 'te_inline_css', 8 );

That is the whole pattern. No extra request, the CSS arrives with the HTML, and the browser can paint without waiting on a stylesheet fetch. Whether you should do this is the part worth thinking about, so let me cover both ways to inline and the tradeoff that decides which one fits.

Looking for the no-code way to add CSS? If you just want to drop a few style rules into your site without touching PHP, go to Appearance, Customize, Additional CSS in the WordPress admin. That Customizer panel saves your CSS to the database and prints it in the head for you, no file editing required. This article is about doing it in code, programmatically: attaching dynamic CSS to an enqueued handle, and inlining critical CSS from your own plugin. If the Additional CSS box covers your need, stop there; what follows is for the developer route.

Two ways to inline CSS in WordPress

There are two clean approaches, and they solve different problems.

1. wp_add_inline_style(), attached to an enqueued handle. This is the WordPress-blessed way when the inline CSS belongs to a stylesheet you are already loading. It appends your CSS into a <style> block right after that handle's <link> tag:

php
add_action( 'wp_enqueue_scripts', function () {
    wp_enqueue_style( 'te-theme', get_stylesheet_uri() );

    $accent = get_theme_mod( 'accent_color', '#0b5fff' );
    $css    = ":root { --accent: {$accent}; } a { color: var(--accent); }";

    wp_add_inline_style( 'te-theme', $css );
} );

The catch that trips everyone: wp_add_inline_style() does nothing on its own. It only outputs if the handle you passed (te-theme here) is actually enqueued. Attach inline CSS to a handle that is merely registered, or never enqueued on that page, and your CSS silently vanishes. This is the right tool for dynamic CSS that depends on a stylesheet already in the queue, like a Customizer accent color or a per-page override.

2. Echo a <style> block on the wp_head action. When the CSS has no associated handle, it is small critical CSS you want in the <head> with nothing else attached to it, you skip the queue entirely and print it yourself. That is the te_inline_css() snippet at the top. You can read a small .css file from your plugin and minify it on the way out, which keeps the source maintainable as a real file while shipping it inline:

php
function te_inline_css() {
    $file = __DIR__ . '/assets/critical.css';
    if ( ! is_readable( $file ) ) {
        return;
    }
    // Collapse runs of whitespace to a single space; trim the ends.
    $css = preg_replace( '#\s+#', ' ', trim( file_get_contents( $file ) ) );
    echo '<style id="te-critical-css">' . $css . '</style>' . "\n";
}
add_action( 'wp_head', 'te_inline_css', 8 );

A priority below the default 10 prints your critical CSS early in the head, before the theme's enqueued styles. The minify here is deliberately crude (collapse whitespace, trim). For genuine critical CSS you would generate the file with a proper extractor that determines which rules actually apply above the fold: the critical and criticalcss npm packages, Penthouse, or the hosted criticalcss.com service all do this. The inlining mechanism is exactly this regardless of which extractor produced the file.

If you would rather not hand-roll any of it, the performance plugins do this automatically: WP Rocket, Jetpack Boost, Autoptimize, and LiteSpeed Cache can all generate and inline critical CSS for you. That is the right call for many sites. The hand-rolled approach in this article is for when you want explicit control over exactly what gets inlined, on which pages, with no plugin overhead or markup you did not write.

Side by side, the two code routes differ on one thing that decides everything else, whether the CSS rides along with a stylesheet you are already enqueuing:

ApproachNeeds an enqueued handle?Where it hooksBest for
wp_add_inline_style()Yes (no-ops without one)wp_enqueue_scriptsDynamic CSS tied to a loaded stylesheet (Customizer color, per-page override)
wp_head <style> echoNowp_headStandalone critical CSS with no parent stylesheet

When inlining helps and when it hurts

This is the part most "speed up WordPress" posts skip, so I will be blunt about it.

Inlining a stylesheet removes a render-blocking HTTP request. For a small block of critical, above-the-fold CSS (a few KB), that is a genuine win: the browser gets the styles it needs to paint the first screen in the same response as the HTML, with no second round trip to block on. This is the whole reason "inline your critical CSS" is standard performance advice.

But inlined CSS is not cached separately by the browser. An enqueued style.css is fetched once and then served from cache on every subsequent page; inlined CSS is re-sent inside the HTML on every single page load, because it is part of the document, not a cacheable asset. So every byte you inline is a byte added to every response, forever, with no caching to amortize it.

That makes the rule simple:

  • Inline only small critical CSS. A few KB of above-the-fold styles that has to arrive before paint. The render-blocking request you save is worth more than the repeated bytes.
  • Keep large stylesheets enqueued. Your full theme CSS, a framework, a 60 KB design system, those belong in a cached file. Inline a large stylesheet and you bloat every HTML response, defeat the browser cache, and usually make the site slower overall. Defer or preload them instead of inlining.

The honest version: inlining is a scalpel for the critical-CSS problem, not a general "fewer requests is always better" rule.

A note on HTTP/2 and HTTP/3

The "fewer requests" argument for inlining was a much bigger deal on HTTP/1.1, where every stylesheet meant a fresh connection-limited request and head-of-line blocking. On HTTP/2 and HTTP/3, many small requests are multiplexed cheaply over a single connection, so cutting request count purely to reduce overhead buys you far less than it used to.

What inlining still wins on modern protocols is eliminating the render-blocking round trip for critical CSS: even multiplexed, an external stylesheet is a separate fetch the browser must complete before it paints. Inline it and the bytes are already in the HTML. So inline for the critical-path latency, not for the request count, and check whether your host already serves over HTTP/2 or HTTP/3 before assuming the old request-count math applies.

Put it in a plugin, not the theme

I do not keep this in functions.php. Theme code disappears the moment you switch themes, and "why did my critical CSS stop loading?" is an annoying thing to rediscover later. Drop it into a must-use plugin at wp-content/mu-plugins/te-inline-css.php instead. Files in mu-plugins load automatically, before regular plugins, and survive theme switches:

php
<?php
/**
 * Plugin Name: TE Inline CSS
 * Plugin URI:  https://techearl.com/inline-css-wordpress
 * Description: Inlines a small critical CSS file on wp_head instead of enqueuing a separate render-blocking stylesheet.
 * Version:     1.0.0
 * Author:      Ishan Karunaratne
 * Author URI:  https://techearl.com
 * License:     GPL-2.0-or-later
 * Text Domain: te-inline-css
 */

function te_inline_css() {
    $file = __DIR__ . '/assets/critical.css';
    if ( ! is_readable( $file ) ) {
        return;
    }
    $css = preg_replace( '#\s+#', ' ', trim( file_get_contents( $file ) ) );
    echo '<style id="te-critical-css">' . $css . '</style>' . "\n";
}
add_action( 'wp_head', 'te_inline_css', 8 );

Put your hand-written, readable critical CSS in wp-content/mu-plugins/assets/critical.css. The plugin reads and minifies it at request time, so you edit a real stylesheet and ship it inline. On a high-traffic site, cache the minified string in a transient rather than running preg_replace over the file on every request; the file rarely changes and the regex on every page load is pointless work.

The gotchas that bite

A few things go wrong with inline CSS that are worth knowing before you ship it.

wp_add_inline_style() silently does nothing if the handle is not enqueued. This is the number-one inline-CSS confusion. You call it, you see no CSS, and there is no error. The handle has to be actually enqueued on the page where you expect the output, not just registered. If your inline styles vanish, check that the parent handle is enqueued there first.

Run te_inline_css() on the right hook, at the right time. wp_add_inline_style() belongs on wp_enqueue_scripts (it works through the queue). The raw <style> echo belongs on wp_head. Mixing them up, like trying to echo during wp_enqueue_scripts, prints your CSS in the wrong place or not at all.

Escaping and untrusted input. The snippet above inlines a static file you control, so escaping is not the concern. The moment any inlined value comes from user input or the database, sanitize it. A Customizer color should go through sanitize_hex_color() before it lands inside a <style> block; never drop raw request data into inline CSS.

Do not inline your whole stylesheet. Worth repeating as a gotcha because people do it: inlining a large CSS file feels like an optimization and is the opposite. You lose the browser cache and pay the bytes on every page. Keep it small and critical, or keep it enqueued.

A page cache will serve stale inline CSS. Because the CSS is baked into the HTML, a full-page cache (or a CDN HTML cache) serves the old inlined styles until you purge it. Edit critical.css and nothing changes? Purge the page cache before you go debugging the PHP.

Verify it worked

Hard-refresh the page in a logged-out window and check the source. You are looking for your <style id="te-critical-css"> block in the <head>, and you are confirming there is no separate request for the file you inlined:

text
$ curl -s https://example.com/ | grep -o '<style id="te-critical-css">'
<style id="te-critical-css">

Then confirm the file is not also being fetched as a separate stylesheet (you do not want both):

text
$ curl -s https://example.com/ | grep -i 'critical.css'
(no output)

In the browser, open DevTools, reload, and check the Network tab: the inlined CSS should not appear as its own request, and the styles should be applied with no flash of unstyled content on the first paint. For a wp_add_inline_style() block, view-source and confirm the <style> sits right after its parent handle's <link rel="stylesheet"> tag; if it is missing, the parent handle was not enqueued on that page.

See also

Sources

Authoritative references this article was fact-checked against.

TagsWordPressPerformancePHPCSSPage SpeedCritical CSS

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

Bluehost for WordPress: Honest Take for Small Sites

Bluehost has the WordPress.org recommendation and the lowest entry-tier price in mainstream hosting. The honest take on where Bluehost legitimately fits in 2026, the real operational ceiling, and the migration path for sites that outgrow it.

Turning Figma Designs Into WordPress Components Using AI

The Figma to ACF Flexible Content pipeline used to be the slow part of every agency build. With AI in the loop, you describe the design intent, hand over the Figma JSON or screenshots, and get back working ACF registration plus template partials. Here is the realistic workflow.