Overriding styles
Oblyx renders into light DOM: no shadow root, no encapsulation, no ::part() indirection. A <oblyx-button>'s inner markup is a real <button class="ox-btn ox-btn--primary"> sitting in your page's own document, reachable by any CSS selector, inspectable with the browser's DevTools like anything else on the page.
Why light DOM
Shadow DOM buys style isolation at the cost of exactly the thing this override story needs: a plain selector reaching in from outside. Astryx's hashed StyleX class names (x1a2b3c4-style) are unreadable and unstable release to release, so overriding upstream means fighting specificity with generated names that can change under you. Oblyx discards that scheme entirely and ships hand-written, stable BEM-style class names instead:.ox-btn, .ox-btn--primary, .ox-card-header. Light DOM plus stable names means you restyle a component the same way you'd restyle a <button> you wrote by hand: open DevTools, find the class, write a rule.
Finding the class to target
Every component's real markup is visible after upgrade. Take <oblyx-button variant="primary">:
A primary button
<oblyx-button variant="primary">Save</oblyx-button>oblyxButton(variant = OblyxButtonVariant.PRIMARY) { +"Save" }Inspecting it shows a <button class="ox-btn ox-btn--primary"> inside the custom element. ox-btn is the base class every button variant shares;ox-btn--primary is the modifier for this one variant. A style targeting only primary buttons goes on .ox-btn--primary; a style for every button regardless of variant goes on .ox-btn.
Sizing a component: the tag is the box, the class is the look
Light DOM has a consequence beyond overridable class names: the box a component's own CSS paints is an ordinary child element, not the custom-element tag itself. For <oblyx-card>, the bordered, clipped box you see is a <div class="ox-card"> sitting one level inside the tag, not the tag itself.
That split matters the moment a card sits inside a layout you control. Whatever ancestor flex or grid context you place <oblyx-card> in sees <oblyx-card> as the item, because there's no shadow root hiding it behind anything else. Sizing and positioning properties (flex, flex-grow, align-self,grid-column, grid-row, grid-area, explicit width/height) belong on the tag for this same reason: written against .ox-card instead, they'd target an element your layout's own algorithm never actually measured. Appearance (border, background, radius, the card's own internal header/body/footer stacking) stays on .ox-card, exactly as the override section above already showed.
A card that needs to fill a fixed-height container is the concrete case. Setting display: flex; flex-direction: column directly on <oblyx-card> looks like the obvious move and doesn't work: it puts the internal div.ox-card(which is already its own column flexbox, stacking header/body/footer) into a second, nested column context with no way for the outer one to hand it the min-height: 0 it would need to stop overflowing. The working form skips the nested column entirely:display: flex with the default row direction, plus flex: 1 1 auto, on the tag. Default align-items: stretch in a row container stretches its one child, div.ox-card, to the tag's full cross-axis size, and flex: 1 1 auto lets the tag itself grow to fill its own parent. The card's internal layout is never touched.
A card filling a fixed-height container
flex: 1 1 auto and display: flex (row, the default direction) are set on <oblyx-card> itself; .ox-card's own header/body/footer stacking is untouched.
<div style="display: flex; height: 220px; padding: 8px; border: 1px dashed var(--ox-color-border-emphasized);">
<oblyx-card style="display: flex; flex: 1 1 auto;">
<oblyx-card-header>Plan: Pro</oblyx-card-header>
<oblyx-card-body>The card fills the dashed container's full height.</oblyx-card-body>
<oblyx-card-footer>
<oblyx-button variant="primary">Manage plan</oblyx-button>
</oblyx-card-footer>
</oblyx-card>
</div>div {
style = "display: flex; height: 220px; padding: 8px; border: 1px dashed var(--ox-color-border-emphasized);"
oblyxCard {
attributes["style"] = "display: flex; flex: 1 1 auto;"
oblyxCardHeader { +"Plan: Pro" }
oblyxCardBody { +"The card fills the dashed container's full height." }
oblyxCardFooter {
oblyxButton(variant = OblyxButtonVariant.PRIMARY) { +"Manage plan" }
}
}
}Writing the override
Oblyx's build does not use CSS cascade layers (no @layer anywhere in the shipped stylesheet), so ordinary cascade rules decide the winner: a selector of equal or higher specificity, loaded after oblyx.css, wins. Your own stylesheet, linked after Oblyx's, needs nothing more than a plain class selector:
.ox-btn--primary {
background-color: #7952ff;
border-radius: var(--ox-radius-full);
}No !important, no id selector, no wrapper element to break out of. This is the entire argument for the light-DOM decision: a developer restyles Oblyx components with the CSS they already know, not a component-specific escape hatch.
Composition classes vs. token overrides
Two different tools for two different jobs, and it's worth knowing which one you want:
- A class override (this page) changes one component, one property, one value. Reach for it when a specific button, card, or control needs to look different from the rest of the library on this page.
- A token override (see Theming and color scheme) changes a design value globally, everywhere that value is used. Reach for it when you're rebranding a color, a radius, or a spacing step across the whole site, not restyling one instance.
The two compose: .ox-btn--primary above still reads var(--ox-radius-full) for its border radius rather than a hardcoded pixel value, so a page that also overrides --ox-radius-full gets both changes applied together, consistently.