Custom CSS for your storefront
Style your Niblr storefront with your own CSS, written against a published set of data-nb-* attributes that survive platform updates. Covers the selector vocabulary, worked examples, what the validator strips on save, and why cart and checkout are excluded.
Every block on a Niblr storefront emits a stable set of data-nb-* attributes. Write your CSS against those and it survives platform updates — unlike CSS targeting utility classes or DOM structure, which can change on any release. Paste it into Store & branding → Custom CSS; it is validated on save and applied to your content pages.
Custom CSS is a Premium feature. The field is visible on every plan, but saving a stylesheet requires Premium.
Where do I put my CSS?
Store & branding → Branding → Custom CSS. One stylesheet per store, up to 32KB.
There is also a Custom CSS enabled toggle beside it. Turning it off disables your stylesheet without deleting it — useful if something looks wrong and you want the storefront back to normal while you work out what.
Your CSS is parsed when you save. If it does not parse, the save is rejected with an error rather than stored and silently half-applied.
What can I target?
Three attributes, on every block:
| Attribute | Where it appears | Value |
|---|---|---|
data-nb-block | The block's wrapper | The block type — featureGrid, reviews, mediaHero, ctaBanner, and so on |
data-nb-variant | The block's wrapper | The layout variant, when the block has one — cards, list, masonry. Absent entirely when no variant is selected |
data-nb-part | Elements inside a block | A semantic role — see the vocabulary below |
data-nb-block and data-nb-variant sit together on the wrapper around a block's output. data-nb-part sits on the individual elements it marks, wherever they land inside.
What are the part names?
Eleven, in two groups. Block-level parts mark a role the block has for itself. Item-level parts mark the same roles inside one entry of a repeated list.
| Part | Level | What it marks |
|---|---|---|
heading | Block | The block's own title |
body | Block | The block's own supporting text |
media | Block | The block's own image or video |
cta | Block | A call-to-action link or button |
eyebrow | Block | The small label above a heading |
badge | Block | A small chip carrying a number or status |
link | Block | A textual link that is not a call to action |
item | Item | One repeated entry — a card, a row, a tile |
item-heading | Item | The title inside a repeated entry |
item-body | Item | The supporting text inside a repeated entry |
item-media | Item | The image or video inside a repeated entry |
A block can have both levels at once. A feature grid has one heading for the section and an item-heading on every tile beneath it — two different elements, two different selectors.
link or cta? Pick by role, not by tag. Both mark anchors. cta is the button-styled link driving the block's primary action. link is a plain textual link sitting inline in the copy — a phone number or email address in a contact block, or a small credit line inside a review. If you are unsure, ask whether it should look like a button or like text.
How do I style something inside a repeated item?
Combine item with the part name. There is deliberately no item-badge or item-link — item already marks the repeated wrapper, so anything inside it is reached with a descendant selector:
[data-nb-part="item"] [data-nb-part="badge"] {
background: var(--color-accent);
}
How do I target a block's colour band?
A fourth attribute, data-scheme, sits on the same wrapper and names which colour band the section renders on:
| Value | What it means |
|---|---|
default | No band — inherits the page background |
muted | A subtle alternate surface, a slight step off the page background |
inverse | A full band in your primary brand colour, with contrast text |
accent | A full band in your accent colour, with contrast text |
This is worth combining with data-nb-block, because the same part needs different treatment depending on the band behind it — a cta on a default section rarely wants the same styling as one on inverse.
What do real selectors look like?
Style every feature tile's title, across every variant of that block:
[data-nb-block="featureGrid"] [data-nb-part="item-heading"] {
text-transform: uppercase;
letter-spacing: 0.04em;
}
Style review cards only in the masonry layout, leaving the grid and list layouts untouched:
[data-nb-block="reviews"][data-nb-variant="masonry"] [data-nb-part="item"] {
border-radius: 0;
}
Style a call-to-action only where it renders on an inverse band:
[data-nb-block="ctaBanner"][data-scheme="inverse"] [data-nb-part="cta"] {
box-shadow: 0 0 0 2px currentColor;
}
Will my CSS survive updates?
That is what the contract is for. It makes two promises.
A part name never changes meaning. heading will always be the element you would call the block's title, on any block, in any future markup. The contract is semantic, not structural — it promises the role exists, never that it lives on a particular tag or at a particular nesting depth. That is precisely what lets us rebuild a block's internals without breaking your stylesheet.
A block emits only the parts it has. An image gallery has no heading, so it never emits heading — only item and item-media. A spacer emits nothing at all. Absence is normal, not a bug. Write CSS that applies when a part is present; never write CSS that depends on it being there.
The contract is versioned nb-hooks/1. Adding a part name does not bump that version and cannot break you — a name that did not exist before cannot have been targeted before. Only renaming or removing a published name would be a breaking change.
What is not supported?
The contract is these attributes and nothing else.
- Utility classes. Blocks are built with Tailwind internally and those class names appear in the HTML, but they are an implementation detail. Undocumented, unstable, and liable to change on any release including a patch.
- Element and structural selectors. Do not target tag names (
h2,section,img), nesting depth (div > div > p), or sibling order. None of it is covered by the promises above. - Our DOM shape generally. Wrappers and containers can be added, removed or rearranged at any time. If it is not a
data-nb-*attribute ordata-scheme, do not build on it.
If a selector you need is not expressible against this vocabulary, that is a gap in the vocabulary — tell us rather than reaching past it into internals, because anything you reach for there can move without warning.
What does the validator strip on save?
Your CSS is parsed and cleaned rather than stored untouched.
@importis removed. Pulling in an external stylesheet is an unbounded surface — it would let arbitrary remote CSS load on every storefront visit.- URLs are default-deny. Only three shapes are kept: a
data:URI, a root-relative path beginning with a single/, and a same-document#fragmentreference such asurl(#gradient). Everything else is stripped, including absolute (https://…), protocol-relative (//example.com/…) and other relative forms (images/foo.png). This applies to every function taking a URL, not justurl()—image-set(),src()andimage()are held to the same rule. Host images in your own Media library and reference them with a/-relative path. - Backslash escapes outside a string are refused. A declaration containing a backslash anywhere other than inside a quoted string is dropped whole, because an escape can disguise any of the rules above.
content: "\2014"is fine;content: \2014is not. position: fixedis removed. A fixed element ignores the page's layout and can cover navigation or cart controls from anywhere in the stylesheet.position: stickyis kept.
One known rough edge: these rules read the raw declaration text, so the literal word url( inside a string — content: "see url(here)" — is treated as a URL and the declaration is dropped. That is the default-deny posture failing in the safe direction.
Does custom CSS apply to cart and checkout?
No, and deliberately.
Your CSS is applied to four route types: your store home page, your custom pages, the product listing, and product detail pages. The cart page and checkout pages receive none of it. Those carry the flow where a broken style could stop a customer completing a purchase, so they were kept out of scope from the start rather than protected afterwards.
The cart drawer is not the cart page, and it is styleable. The slide-out drawer renders on every route, so on the four content pages above your CSS reaches it — including its Checkout button. That is intentional; styling your own drawer is reasonable. But if you restyle it, check the Checkout button is still visible and clickable, because nothing stops you hiding it.
Where do I start?
Build your pages from content blocks first — the attributes above are emitted by those blocks, so there is nothing to target until a page has some. Then open your storefront, inspect a block, and you will see the data-nb-* attributes on it. Write your selectors against what you find there, and nothing else.
The rest of the documentation covers products, payments, domains and deployment.