Skip to content
ToolShedby Wasim Shaikh

Creating Your First Client Extension: A Step-by-Step Tutorial (globalCSS)

Wasim · 8 Jul 2026

What You'll Build

In this tutorial, you'll create a globalCSS client extension — the simplest type of frontend client extension in Liferay. By the end, you'll have a custom stylesheet applied site-wide, deployed entirely outside Liferay's core, with no theme recompilation required.

This is the standard entry point for learning client extensions, because it requires:

  • No React, Vue, or build tooling
  • No Node.js dependency management
  • Just a .css file and a YAML configuration file

Everything else — themes, custom elements, JavaScript extensions — builds on the same core pattern you'll learn here.

Reading time: 10-12 minutes · Difficulty: Beginner · Audience: Developers, Frontend Engineers, Site Administrators


What Is a Client Extension?

A client extension is a self-contained unit of code that lives outside the Liferay DXP container and interacts with the platform through defined extension points, rather than modifying Liferay's core code. Because it's decoupled, it survives platform upgrades without needing to be re-certified or rewritten.

A globalCSS client extension is the frontend variant used to inject custom CSS across an entire Liferay instance — without touching a theme's source files.


Prerequisites

  • Liferay Workspace set up locally (or access to a Liferay Cloud project)
  • Java 21 installed
  • Blade CLI installed (or the Liferay Workspace Gradle plugin)
  • A running Liferay DXP instance (local or cloud)
  • Basic familiarity with YAML syntax

If you don't yet have a workspace, initialize one:

blade init my-workspace
cd my-workspace

Step 1: Create the Client Extension Project Folder

Inside your Liferay Workspace, client extensions live in the client-extensions/ directory.

cd client-extensions
mkdir my-global-css
cd my-global-css

Your folder structure so far:

liferay-workspace/
└── client-extensions/
    └── my-global-css/

Step 2: Write the CSS File

Create a src folder and add your stylesheet:

mkdir src

Create src/main.css:

/* src/main.css */

/* Example: Override the primary button color site-wide */
.btn-primary {
  background-color: #1a5f7a;
  border-color: #144a5f;
}

.btn-primary:hover {
  background-color: #144a5f;
  border-color: #0f3a4a;
}

/* Example: Adjust global heading typography */
h1, h2, h3 {
  font-family: 'Helvetica Neue', Arial, sans-serif;
  letter-spacing: -0.02em;
}

/* Example: Custom footer styling */
.footer-custom {
  background-color: #0d1b2a;
  color: #ffffff;
  padding: 40px 0;
}

Keep the first version small and focused. It's easier to verify a handful of rules applied correctly than to debug fifty at once.


Step 3: Author the client-extension.yaml File

This file tells Liferay what the extension is, what type it is, and where to find its assets. Create it in the root of your extension folder:

touch client-extension.yaml

Contents:

# client-extension.yaml
assemble:
  - from: src
    into: static

my-global-css:
  name: My Global CSS
  description: Site-wide CSS overrides for buttons, headings, and footer
  type: globalCSS
  cssURLs:
    - static/main.css

Field breakdown:

Field Purpose
assemble Copies files from src/ into the deployable package under static/
name Human-readable label shown in the Liferay admin UI
description Optional, but useful for future maintainers
type Must be exactly globalCSS for this extension type
cssURLs List of CSS file paths (relative to the assembled package) to load

Common mistake: YAML is indentation-sensitive. cssURLs must be nested under your extension's key (my-global-css), not at the root level, and the list items need a consistent two-space indent.


Step 4: Review the Final Project Structure

At this point, your extension folder should look like this:

client-extensions/
└── my-global-css/
    ├── client-extension.yaml
    └── src/
        └── main.css

That's the entire extension. No package.json, no build config, no compiler.


Step 5: Build and Deploy

Option A: Using Blade CLI

From the root of your Liferay Workspace:

blade gw deploy

This builds all client extensions in the workspace and deploys them to your running local Liferay instance.

Option B: Using Gradle Directly

./gradlew deploy

Option C: Manual Package Upload (Cloud/Production)

If you're not deploying to a local instance, build the extension into a .zip:

./gradlew build

The resulting archive appears under:

client-extensions/my-global-css/build/distributions/

Upload this .zip through your Liferay Cloud project's console, or via your CI/CD pipeline if one is configured.


Step 6: Verify the CSS Is Applied

  1. Open your Liferay instance in a browser.
  2. Navigate to any page with a primary button (e.g., a login form, "Add" button, or CTA fragment).
  3. Confirm the button now reflects your custom #1a5f7a background color.
  4. Open browser DevTools → Elements tab → Inspect the button. You should see your stylesheet listed under Styles, sourced from a path resembling:
/o/client-extension/my-global-css/static/main.css

If the styles aren't appearing, work through this checklist:

  • Confirm the extension deployed without errors (check server logs)
  • Hard-refresh the browser (Cmd/Ctrl + Shift + R) to bypass cache
  • Confirm type: globalCSS is spelled exactly as shown
  • Confirm the cssURLs path matches the assembled output location

Step 7: Confirm It's Registered in the Admin UI

  1. Go to Global Menu → Control Panel → Client Extensions (exact location may vary slightly by version).
  2. Look for My Global CSS in the list.
  3. Its status should show as Deployed or Active.

This confirms Liferay has registered the extension and is serving the CSS file to every page in the instance.


How globalCSS Differs from themeCSS

It's worth understanding this distinction early, since both extension types affect visual styling but behave differently:

Aspect globalCSS themeCSS
Scope Applies instance-wide, regardless of active theme Tied to a specific theme
Use case Quick overrides, branding tweaks, utility classes Deep theme customization, SCSS-based systems
Complexity Single CSS file, no build step Often uses SCSS, styled/unstyled base themes
Best for Small, targeted style changes Full visual redesigns tied to a theme

If you only need to override a handful of styles across the whole site — regardless of which theme is active — globalCSS is the right tool. If you're building a comprehensive visual identity tied to one theme, themeCSS is more appropriate. If you're currently scaffolding a theme from scratch, the Liferay Theme Builder Wizard can generate the base _clay_variables.scss and layout files a themeCSS extension would build on top of.


Common Beginner Mistakes

1. Wrong type Value

# Incorrect
type: css

# Correct
type: globalCSS

The type value is case-sensitive and must match Liferay's defined extension types exactly.

2. Mismatched File Paths

If your assemble block copies src into static, your cssURLs entry must reference the static/ path — not src/:

# Incorrect — references source path
cssURLs:
  - src/main.css

# Correct — references assembled output path
cssURLs:
  - static/main.css

3. Forgetting to Redeploy After Changes

CSS edits inside src/main.css won't appear until you rebuild and redeploy the extension. Unlike editing a theme's CSS directly in some workflows, client extensions require an explicit deploy step each time.

4. Browser Caching Masking Real Issues

Before assuming a deployment failed, always hard-refresh or test in an incognito window. Stale CSS caching is one of the most common false alarms when testing style changes.

5. Overwriting Instead of Overriding

Because globalCSS applies broadly, overly aggressive selectors (like styling button instead of .btn-primary) can unintentionally affect elements you didn't mean to touch. Scope your selectors as specifically as reasonably possible.


What Gets Created Behind the Scenes

Component Result
Deployable package .zip archive containing your assembled CSS
Registration Extension appears in Client Extensions admin list
Delivery CSS served via a Liferay-managed static URL
Application Stylesheet loaded on every page, instance-wide
Upgrade safety Extension is untouched by Liferay version upgrades

Next Steps

Once you're comfortable with globalCSS, the same YAML-driven pattern extends to other frontend client extension types:

  • globalJS — Inject custom JavaScript site-wide, following an almost identical YAML structure (jsURLs instead of cssURLs); see the globalJS tutorial for a full walkthrough
  • themeCSS — Scope styling to a specific theme, with support for SCSS and the styled/unstyled base theme system; see the themeCSS deep dive for how it actually works under the hood
  • themeFavicon — Replace the default favicon per theme
  • themeSpritemap — Customize the icon spritemap used across a theme
  • Custom Elements — Build interactive React or Vue components deployed the same way, using type: customElement

Each of these follows the same core workflow you just completed: define assets, describe them in client-extension.yaml, build, and deploy. For the official reference on every extension type and its full YAML schema, see Liferay's Client Extensions documentation and the CSS YAML Configuration Reference.


Summary

In this tutorial, you:

  1. Created a client extension project folder inside client-extensions/
  2. Wrote a custom CSS file with targeted style overrides
  3. Authored a client-extension.yaml file with type: globalCSS
  4. Built and deployed the extension using Blade or Gradle
  5. Verified the CSS was applied site-wide
  6. Confirmed the extension appeared as Active in the admin UI

This is the foundational pattern for every client extension type in Liferay — only the type value and the code inside src/ change. Once this workflow feels familiar, extending it to JavaScript, theming, or full custom elements is a matter of substitution, not relearning.


Wasim Shaikh

About the author

Wasim Shaikh is a UI/UX developer and front-end engineer with 15+ years of experience, based in Ahmedabad, India. He specializes in Liferay, React, Angular, Next.js and Tailwind CSS.

Keep reading

Web dev · 8 Jul 2026Creating Your First Client Extension: A Step-by-Step Tutorial (globalJS)Web dev · 8 Jul 2026Creating Your First Custom Element: A Step-by-Step Tutorial (React Weather Card)Web dev · 8 Jul 2026Sharing JavaScript Libraries Across Pages: The JS Import Map Entry Client Extension (jsImportMapsEntry)