(WIP)
The Common-Styling module provides a reusable design system for platformOS to make your projects look great out of the box and gives you simple tools to customize them.
It includes reusable CSS and JavaScript that are (or will be) leveraged by platformOS modules, and which you can also use directly in your own projects. The idea is to provide a consistent, documented way of building modules that look good from the start and which you can easily customize to fit your needs.
Common-styling follows the platformOS DevKit best practices.
- ⚙️ Customizable through CSS variables defined in
pos-config.css - 📖 Built-in
/style-guidepage to preview components and check available variables - 🧩 Provides shared CSS and JavaScript utilities for use in platformOS modules and projects
- 📝 Includes base styles for forms, buttons, typography, and other common components
- 🛡️ Scoped with
pos-prefixes and CSS layers to avoid conflicts with project code - 🌙 Supports both light and dark themes (automatic or manual)
- 📱 Responsive styles included by default
- ✅ Follows platformOS DevKit best practices
The platformOS Common Styling Module is available on the Partner Portal Modules Marketplace.
After installation, visit <your instance url>/style-guide to preview all available components, see examples of forms, buttons, typography and the complete list of CSS variables you can override.
Before installing the module, ensure that you have pos-cli installed. This tool is essential for managing and deploying platformOS projects.
The platformOS Common Styling is fully compatible with platformOS Check, a linter and language server that supports any IDE with Language Server Protocol (LSP) integration. For Visual Studio Code users, you can enhance your development experience by installing the VSCode platformOS Check Extension.
-
Navigate to your project directory where you want to install the Common Styling Module.
-
Run the installation command:
pos-cli modules install common-stylingThis command installs the Common Styling Module and updates or creates the app/pos-modules.json file in your project directory to track module configurations.
- Download the source code into your local environment:
pos-cli modules download common-styling- Explore the built-in Style Guide.
After installation, visit
/style-guideon your instance. (Make sure you deploy the module usingpos-cli deploycommand)
- Preview all available components.
- See examples of forms, buttons, and typography.
- Find the complete list of CSS variables you can override.
-
Install the module using the pos-cli.
-
Include the following partial into your layout's
<head>section:
{% render 'modules/common-styling/init' %}👉 If you do not have a layout, check the style-guide's layout for an example of a minimal layout.
- Scope the styling with
pos-app:
- To apply common-styling globally, add
class="pos-app"to your application’s<html>tag:
<html lang="en" class="pos-app">- To apply styles only in certain parts of your app, wrap content in a container:
<div class="pos-app">
</div>- Ensure you configured your Instance to escape output instead of sanitizing through app/config.yml
---
escape_output_instead_of_sanitize: true
---- Optionally enable the CSS reset. It resets default browser styling and fixes some browser-specific issues
- It’s safe to use in a fresh app.
- In an existing app, enabling it might cause unexpected changes.
To use it, pass the reset: true parameter to the render tag mentioned above and use a pos-app class anywhere on your main content container:
{% render 'modules/common-styling/init', reset: true %}Note: If you plan to add your own CSS overrides, always load them after
common-stylingin your layout. This way, your styles take precedence over the defaults.
When using the common-styling module, you can configure the look of components by overriding the CSS variables defined in pos-config.css.
Define overrides inside :root {} so they apply globally across your app.
Instead of editing the module directly, you can override only the variables you need in your own stylesheet.
👉 Copy only the variables you want to change into your app’s CSS file, and redefine them there.
👉 Use /style-guide as your main reference for available variables and component classes. This page lists all color variables, form components, buttons, and more. It’s the recommended way to discover what you can override.
👉 Common-styling is responsive out of the box. To test layouts, use your browser’s responsive design mode (DevTools → device toolbar).
Add a file to your project, for example:
app/assets/<my-app-name>-config.css
This is where you’ll put your overrides.
In your layout (for example: application.liquid), include your stylesheet below the common-styling init. This ensures your overrides take precedence.
{% render 'modules/common-styling/init' %}
<link rel="stylesheet" href="{{ 'variables.css' | asset_url }}">Copy only the variables you want to change from pos-config.css into your stylesheet, and redefine them under :root.
Example — giving primary buttons a green theme:
:root {
--pos-color-button-primary-background: #008000;
--pos-color-button-primary-hover-background: #00a000;
}- Don’t hardcode values like colors or sizes (with very few exceptions). Always rely on the provided CSS variables.
- Override only what you need — don’t copy the full config.
- You can use CSS functions like
calc(),from-color(), orcolor-mix()if you need to adjust variables dynamically. - Stick to the provided variable system, which maps to our Figma design kit.
- If your app supports dark mode, remember to override both light and dark variables.
- Use
/style-guideto preview available variables and confirm your overrides.
The common-styling module includes two base themes by default: a light and a dark one.
To enable automatic dark mode (switches based on the user’s system preference), add the following class to the root <html> tag in your layout:
<html class="pos-app pos-theme-darkEnabled">If you want to control the theme manually, use:
<html class="pos-app pos-theme-dark">This forces the dark theme regardless of system settings.
- Both light and dark themes use the same set of CSS variables. If you override variables, make sure to provide values for both themes if needed.
- You can preview dark mode in your browser using dev tools, toggling system preferences, or manually applying
.pos-theme-dark. - Example — overriding variables just for dark mode:
:root {
--pos-color-dark-button-primary-background: #004d00;
}To avoid conflicts and keep styles predictable, all CSS in this module follows a scoping convention.
When naming your module CSS files, please prefix them with pos- for consistency.
- Files: Prefix module CSS files with
pos-(e.g.,pos-form.css,pos-button.css) for consistency. - Classes: Prefix CSS classes with
pos-(e.g.,.pos-form,.pos-button). This ensures styles fromcommon-stylingwon’t interfere with unrelated CSS in your project.
👉 Keep in mind that the module can be used in various contexts, so any styling needs to be scoped just to the module code.
All styles from common-styling are placed inside a dedicated CSS layer.
This lowers specificity so you can override them easily in your own stylesheet, without having to worry about the selectors used.
Some CSS rules will be inherited when the parent container has a specific class. For example, the .pos-form scopes styling so that inputs, buttons, and other form-related elements inside the container adopt the design system styles.
Each component should have its own separate CSS file. This keeps the codebase modular, easier to maintain, and consistent across projects.
For example:
pos-form.cssfor formspos-button.cssfor buttonspos-card.cssfor cards
Use ESM Modules to build JavaScript.
The modules should not pollute the global JavaScript namespace. For this purpose, we are using the pos namespace attached to the global window object. Extending the namespace is the preferred way to store all the needed data, translations, and module public interface.
There are several basic objects used across the modules that could be extended for consistency. Those are shared across many modules, so remember not to overwrite them in your code and extend the objects instead.
| object | description |
|---|---|
| window.pos | Global namespace for all module-related data. |
| window.pos.modules | If your module provides a public API or a constructor, then you should attach it to this object,t namespacing your module accordingly window.pos.module.myModule = {} |
| window.pos.modules.active | If your module creates a separate instance and provides a public API, it can be attached to this object. |
| window.pos.profile | Stores all the profile-related data for the currently logged-in user. |
| window.pos.translations | If your JavaScript code needs access to any translations, you should append them to this object. |
As an example starting point for defining JavaScript for your module, you can use the following code:
<script>
/* namespace */
if(typeof window.pos !== 'object'){
window.pos = {};
window.pos.modules = {};
window.pos.translations = {};
}
/* profile namespace */
if(typeof window.pos.profile !== 'object'){
window.pos.profile = {};
}
window.pos.profile.myProfileVariable = 'foo';
/* translations used in module */
window.pos.translations = {
...window.pos.translations,
myTranslation: 'bar'
}
</script>To enable debug mode, you can set the pos.debug to true in the JS Console. This will log events from the default provided modules.
When building your module, please use the following method to log debug data:
pos.modules.debug([true || module.settings.debug], [module id (string)], [message (string)]);To provide a way of reacting to your module changes, please use JavaScript events when appropriate, prefixing the event with pos- as follows:
document.dispatchEvent(new CustomEvent('pos-somethingHappenedInMyModule', { bubbles: true, detail: { something: 'new value' } }));
pos.modules.debug(module.settings.debug, 'event', `pos-somethingHappenedInMyModule`, { something: new value });Using pos.modules.debug() to add information about the event provides an easy way for the developers to react to changes provided by your module without the need to check the code or browse through documentation.
Rollup is used to build dependencies from NPM registry. To add a new dependency:
- Use
npm install [package]as you would normally do - Create new JS file in
src/js/nameddependency-[package name].js - In that file use the needed
importstatements - Use
npm run buildto generate the build files for each dependency inmodules/common-styling/public/assets/js - Use
modules/common-styling/public/views/partials/init.liquidto load your dependency using asyncimport()function
After adding the code for your module in modules/common-styling/public/assets/js/ use the async import() function to load them and assign their instances to pos.modules.active namespace depending on if they are needed on the page. For example let's load a collapsible list module only when there is such list on the page:
/* collapsible lists */
if(document.querySelector('.pos-collapsible')){
import('{{ 'modules/common-styling/js/pos-collapsible.js' | asset_url }}').then(module => {
document.querySelectorAll('.pos-collapsible').forEach((element, index) => {
window.pos.modules.active[element.id || `pos-collapsible-${index}`] = new window.pos.modules.collapsible({
container: element,
id: element.id || `pos-collapsible-${index}`
});
});
});
};
When using the import statement in your JavaScript files, you will request a JS file from the CDN that could already be cached by the browser. platformOS handles breaking the cache for assets by using the asset_url filter. You cannot use it in the JS files, though, but the browsers allow you to map any filename to any other URL using Import Maps. Currently, only a single import map on a page can be used, and it needs to be defined before any other JS script. (This will change soon as multiple import maps are in the works for all the browsers.)
An example import map looks like this:
<script type="importmap">
{
"imports": {
"/": "{{ 'modules/chat/js/' | asset_url }}",
"chat.js": "{{ 'modules/chat/js/chat.js' | asset_url }}",
"consumer.js": "{{ 'modules/chat/js/consumer.js' | asset_url }}",
"csrfToken.js": "{{ 'modules/chat/js/csrfToken.js' | asset_url }}",
"notifications.js": "{{ 'modules/chat/js/notifications.js' | asset_url }}",
"./": "./"
}
}
</script>The first line allows you to use relative import statements inside your JS files, the last line resets it back to the default.
- Render the partial in your application layout (preferably at the very bottom)
{% liquid
function flash = 'modules/core/commands/session/get', key: 'sflash'
if context.location.pathname != flash.from or flash.force_clear
function _ = 'modules/core/commands/session/clear', key: 'sflash'
endif
render 'modules/common-styling/toasts', params: flash
%}From JavaScript, you can use:
new pos.modules.toast('[severity]', '[message]') to show new notificationOn the server-side: [TO DO]
A pre-defined method of loading HTML content into a container:
const { load } = await import('modules/common-styling/js/pos-load.js');
pos.modules.load = load;await pos.modules.load({
endpoint: [string],
target: [string],
method: [string],
trigger: [dom node],
triggerType: [string]
});| parameter | type | description |
|---|---|---|
| endpoint | string | URL of the endpoint that returns the HTML to be applied to a container |
| target | string | Selector for the target container that the HTML will be applied to |
| method | string | Either replace or append. The returned HTML will replace the content of the container or will be appended after the last node of the container |
| trigger | dom node | The HTML element that triggers loading the endpoint |
| triggerType | string | Either click or hover. Defines whether loading starts on click or hover trigger |
You can use the load method directly or use the simpler method by adding some custom attributes to the trigger element that will initialize loading the endpoint when interacted with:
Clicking the following button will load the HTML from /test/example_endpoint to the container with ID example_container:
<button type="button" data-load-content="/test/example_endpoint" data-load-target="#example_container">
<div id="example_container">Loading…</div>
| attribute | description |
|---|---|
| data-load-content | URL of the endpoint that returns the HTML to be applied to a container |
| data-load-target | Selector for the target container that the HTML will be applied to |
| data-load-method | replace or append – the returned HTML will either replace the container’s content or be appended after the last node of the container |
| data-load-trigger-type | Defines whether the loading process is triggered by a click or mouseenter event |