Creating Your First Client Extension: A Step-by-Step Tutorial (globalCSS)
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
.cssfile 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
- Open your Liferay instance in a browser.
- Navigate to any page with a primary button (e.g., a login form, "Add" button, or CTA fragment).
- Confirm the button now reflects your custom
#1a5f7abackground color. - 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: globalCSSis spelled exactly as shown - Confirm the
cssURLspath matches the assembled output location
Step 7: Confirm It's Registered in the Admin UI
- Go to Global Menu → Control Panel → Client Extensions (exact location may vary slightly by version).
- Look for My Global CSS in the list.
- 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 (jsURLsinstead ofcssURLs); see the globalJS tutorial for a full walkthroughthemeCSS— Scope styling to a specific theme, with support for SCSS and thestyled/unstyledbase theme system; see the themeCSS deep dive for how it actually works under the hoodthemeFavicon— Replace the default favicon per themethemeSpritemap— 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:
- Created a client extension project folder inside
client-extensions/ - Wrote a custom CSS file with targeted style overrides
- Authored a
client-extension.yamlfile withtype: globalCSS - Built and deployed the extension using Blade or Gradle
- Verified the CSS was applied site-wide
- 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.
Related Reading
- Creating Your First Client Extension: A Step-by-Step Tutorial (globalJS) — the JavaScript counterpart to this tutorial.
- Creating Your First Liferay Object: A Step-by-Step Tutorial — the equivalent no-code, step-by-step walkthrough for Liferay Objects.
- Liferay Hosting Explained: SaaS vs. PaaS vs. Self-Hosted — SaaS plans limit customization to Client Extensions only; worth knowing before you plan a deployment.
- Liferay Theme Builder Wizard — scaffold a full 7.4/DXP theme if your next step is a
themeCSSextension. - Browse all Liferay tools — generators and reference utilities for Liferay developers.
