Styling
Palinx links your stylesheets for you. Put plain CSS in src/styles/global.css, or compile Sass or Tailwind to public/styles.css, and every page gets them, in development and in production, in the same order.
Where styles live
Palinx looks for two files and links each one that exists, on every page:
| File | What it's for |
|---|---|
src/styles/global.css |
Plain CSS you write by hand. Served as it is; there is no build step. |
public/styles.css |
The output of a CSS tool such as Sass or Tailwind. Your tool writes it; palinx serves it like any other file in public/. |
You don't add <link> tags yourself. A project can use either file, both, or neither.
Link order
Pages link stylesheets in this order, under both px dev and the built app:
src/styles/global.csspublic/styles.css- Component styles from
css`...`templates (below)
Later stylesheets win when two rules have the same specificity, so component styles override global ones.
Component styles
A component can carry its own styles with the css`...` tag from @palinx/core. The rules are scoped: palinx prefixes each selector with a generated class, and you put that class on the component's root element.
import { Component, html, css } from "@palinx/core";
const styles = css`
.badge { color: rgb(109, 92, 255); }
`;
export class Badge extends Component {
render() {
return html`<span class=${styles.scope}><b class="badge">New</b></span>`;
}
}The rule becomes .ov-… .badge { … }, so it only matches inside elements carrying styles.scope. Wrap a selector in :global(...) to leave it unscoped.
When a template has no ${} expressions, palinx extracts it ahead of time, in px dev as well as px build, into one component stylesheet that is linked with the others, and the server-rendered HTML already carries the scope class. The component is styled at first paint, before any JavaScript runs, on prerendered pages, on server-only pages and on every page under px dev. In production, pages with route parameters (such as /items/[id]) aren't server-rendered, so their components are styled as soon as the client renders them.
A template that does use ${} stays a runtime call: it adds its own <style> tag when the component's module loads, so its rules apply from hydration.
Which tags palinx compiles
palinx only compiles a template when it can tell the tag is its own.
cssis extracted when it is imported from@palinx/corein the same file, under its own name or an alias (import { css as style } from "@palinx/core"). Acssreached any other way, such as throughimport * as core, through a module of yours that re-exports it, or under a name the file also uses for something else (a parameter calledcss, say), isn't extracted. It still works: it runs as a runtime call and its rules apply from hydration.htmlis compiled when its tag is imported from@palinx/coreby name, by alias or through a namespace (core.html), or from a module of yours that re-exports it. If palinx can't confirm a tag is its own and the template has@eventhandlers, the build fails with the file, the line and the reason, for example thathtmlis passed as a value at line 12. Uncompiled, those handlers would run while the page renders instead of on the event. Rename the other use, or importhtmldirectly from@palinx/core.
Live reload
Under px dev, saving a style change applies it without reloading the page:
- a
css`...`template in a component; src/styles/global.css;public/styles.csswhen your Sass or Tailwind watcher rewrites it.
Each changed stylesheet is swapped in once the new copy has loaded, so the page doesn't flash unstyled.
Choosing styling when you scaffold
webapp and frontend ask which styling to use. Plain CSS is the default:
px scaffold webapp my-app # plain CSS
px scaffold webapp my-app --styles sass
px scaffold webapp my-app --styles tailwindRun px scaffold without a template and the menu asks after you pick webapp or frontend, with plain CSS preselected. The other templates don't take --styles: minimal has no styling, and api and taskrunner style their status page inline.
All three options use the same markup and look the same. The template's pages use short class names, defined once in a shared stylesheet. Here is the home page's feature card and its rules:
private feature(icon: Node, title: string, desc: string) {
return html`
<div class="card">
<div class="card-icon">${icon}</div>
<h3 class="card-title">${title}</h3>
<p class="card-text">${desc}</p>
</div>
`;/* Cards */
.card {
display: flex; flex-direction: column; gap: 0.5rem; padding: 1.25rem;
border-radius: 0.75rem; border: 1px solid var(--color-surface-border); background-color: var(--color-surface);
}
.card-icon {
display: flex; align-items: center; justify-content: center;
width: 2.25rem; height: 2.25rem; border-radius: 0.5rem;
border: 1px solid var(--color-surface-border); background-color: var(--color-surface-raised); color: var(--color-accent);
}
.card-title { font-size: 0.875rem; line-height: 1.25rem; font-weight: 600; color: var(--color-text-primary); }
.card-text { font-size: 0.75rem; line-height: 1.625; color: var(--color-text-muted); }What each option adds:
- Plain CSS: that stylesheet is your
src/styles/global.css. No build step and no extra dependencies. - Sass: the stylesheet is
src/styles/app.css, andsrc/styles/main.scsspulls it in and holds your own Sass. It compiles topublic/styles.css. - Tailwind: the stylesheet is
src/styles/app.css, andsrc/styles/input.cssimports Tailwind and the app's styles. It compiles topublic/styles.css.
Adding Sass to an existing project
Install Sass, and concurrently to run its watcher next to px dev:
bun add -d sass@^1 concurrently@^9Create src/styles/main.scss. This is the one the webapp template ships; @use "app" loads src/styles/app.css, so drop that line if you don't have one:
// Sass entry, compiled to public/styles.css by `bun run build:css`
// (and by `bun run dev:css` while you develop).
// The app's own styles.
@use "app";
// Add your own rules below. A variable and nesting, for example:
$accent: #6d5cff;
.callout {
padding: 1rem 1.25rem;
border-left: 3px solid $accent;
border-radius: 0.5rem;
strong {
color: $accent;
}
}Then set these scripts in package.json:
{
"scripts": {
"dev": "concurrently \"px dev\" \"bun run dev:css\"",
"build": "bun run build:css && px build",
"dev:css": "sass --watch --no-source-map src/styles/main.scss public/styles.css",
"build:css": "sass --style=compressed --no-source-map src/styles/main.scss public/styles.css"
}
}The watcher skips source maps on purpose: a styles.css.map left in public/ would be copied into your production build and publish your Sass source. Add public/styles.css to .gitignore, since it's build output.
Adding Tailwind to an existing project
bun add -d tailwindcss@^4 @tailwindcss/cli@^4 concurrently@^9Create src/styles/input.css. This is the webapp template's; @import "./app.css" layer(components) loads src/styles/app.css, so drop that line if you don't have one:
/* Tailwind entry, compiled to public/styles.css by `bun run build:css`
(and by `bun run dev:css` while you develop). */
@import "tailwindcss";
/* The app's own styles. In the components layer, so a utility class you add
to an element still wins over them. */
@import "./app.css" layer(components);
/* The app's colours as Tailwind tokens, so utilities like bg-surface or
text-accent match the rest of the app. */
@theme {
--color-surface: #14161f;
--color-surface-raised: #191c27;
--color-surface-border: #242736;
--color-text-primary: #f2f3f9;
--color-text-secondary: #b6b9cc;
--color-text-muted: #757994;
--color-accent: #6d5cff;
--color-accent-hover: #8b7dff;
}It imports the app's own styles into Tailwind's components layer, so a utility class you add to an element still overrides them. The @theme block makes the app's colours available as utilities such as bg-surface and text-accent.
If you keep your rules in src/styles/global.css instead, palinx still links it, but those rules sit outside Tailwind's layers, and unlayered CSS beats every layer: bg-red-500 on an element that global.css also styles won't apply. To let utilities win, rename it to src/styles/app.css and keep the @import line above, as described in Switching between options.
Then set these scripts:
{
"scripts": {
"dev": "concurrently \"px dev\" \"bun run dev:css\"",
"build": "bun run build:css && px build",
"dev:css": "tailwindcss -i src/styles/input.css -o public/styles.css --watch",
"build:css": "tailwindcss -i src/styles/input.css -o public/styles.css --minify"
}
}As with Sass, public/styles.css is build output; keep it out of version control.
Switching between options
The markup doesn't change between options, only where the rules live and what compiles them:
- Plain CSS to Sass or Tailwind: rename
src/styles/global.csstosrc/styles/app.css, then follow the setup above. Deleteglobal.cssif you kept a copy, or palinx links both. - Sass or Tailwind to plain CSS: rename
src/styles/app.csstosrc/styles/global.css, move any rules you wrote inmain.scssorinput.cssinto it as plain CSS, then delete the entry file,public/styles.css, thedev:px,dev:cssandbuild:cssscripts, and the tool's dependencies. Setdevback topx devandbuildtopx build. - Sass to Tailwind, or back: replace the entry file and the two scripts, and swap the dependencies.
src/styles/app.cssstays as it is.
Next
Services & dependency injection covers the classes that hold your app's logic.