Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/kind-crabs-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atomicjolt/atomic-elements": minor
---

Wrap all styles in a @layer elements directive for easier style overrides
104 changes: 104 additions & 0 deletions packages/atomic-elements/docs/Guides/Customization/CSSLayers.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# CSS Layers

Atomic Elements supports wrapping all component styles in a [CSS `@layer`](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer),
giving you explicit control over how library styles interact with your own CSS.

## Why use CSS layers?

Without layers, overriding library styles requires matching or beating their specificity — which often means
adding extra selectors, using `!important`, or relying on source order. CSS layers solve this cleanly:
styles in a lower-priority layer are always overridden by styles outside any layer, regardless of specificity.

```css
/* Without layers: this might not win due to specificity */
.my-button {
background: blue;
}

/* With layers: this always wins, no specificity tricks needed */
@layer elements {
.aje-btn {
background: red;
}
}
.my-button {
background: blue;
} /* beats @layer elements unconditionally */
```

## Enabling layers

By default, `ElementsProvider` wraps all component styles in an `@layer elements` block.
No configuration is needed — just use `ElementsProvider` as normal:

```tsx
import { ElementsProvider } from "@atomicjolt/atomic-elements";

const App = () => (
<ElementsProvider>
<YourApp />
</ElementsProvider>
);
```

## Customizing the layer name

Use the `layerName` prop to change the layer name. This is useful when you need to coordinate
layer ordering with other style sources:

```tsx
<ElementsProvider layerName="ui">
<YourApp />
</ElementsProvider>
```

## Controlling layer order

Declare your layer stack at the top of your global CSS to lock in the cascade order:

```css
/* Establish layer order — last layer wins */
@layer reset, elements, overrides;
```

With this in place, any styles you write outside a layer (or inside `@layer overrides`) will
always take precedence over `@layer elements`, making it straightforward to customize
component styles without fighting specificity.

```css
@layer reset, elements, overrides;

@layer overrides {
/* These always win over @layer elements, no !important needed */
.aje-btn--primary {
--btn-bg-clr: indigo;
--btn-hover-bg-clr: darkslateblue;
}
}
```

## Overriding with plain CSS

Styles written outside any `@layer` have higher priority than any layered style by default.
This means you can override component styles with ordinary CSS rules, even ones with lower specificity:

```css
/* No @layer declaration needed — unlayered styles always win */
.my-button {
--btn-bg-clr: green;
}
```

## Browser support

`@layer` is supported in all modern browsers (Chrome 99+, Firefox 97+, Safari 15.4+).
For applications that need to support older browsers, the layer wrapping will cause all
component styles to be ignored in those browsers, since unsupported at-rules are skipped.
If you need to support older browsers, do not use the `layerName` prop — disable the feature
by passing null or an empty string:

```tsx
<ElementsProvider layerName={null}>
<YourApp />
</ElementsProvider>
```
48 changes: 37 additions & 11 deletions packages/atomic-elements/src/components/ElementsProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { createContext, useContext } from "react";
import { ThemeProvider } from "styled-components";
import { createContext, useContext, useMemo } from "react";
import { StyleSheetManager, ThemeProvider } from "styled-components";
import { CssGlobalDefaults } from "@styles/globals";
import { defaultTheme, Theme } from "@styles/theme";
import { layerPlugin } from "@styles/plugins";

export interface ElementsConfig {
/** The theme to be used by the application. */
theme?: Theme;
/**Flag to determine if default styles should be applied.
/** Flag to determine if default styles should be applied.
* NOTE: this will apply some styles globally to your page,
* so only use this if you are not using a global style reset already.
*/
applyDefaultStyles?: boolean;

/** The name of the CSS layer to use for the components' styles.
* @default "elements"
*/
layerName?: string | null;
}

export interface ElementsProviderProps extends ElementsConfig {
Expand All @@ -33,16 +39,36 @@ export function useElementsConfig(): ElementsConfig {
* wrap the root of the application and provides the theme and global styles
*/
export function ElementsProvider(props: ElementsProviderProps) {
const { children, theme = defaultTheme, applyDefaultStyles = false } = props;
const {
children,
theme = defaultTheme,
applyDefaultStyles = false,
layerName = "elements",
} = props;

const CssVariables = theme._Component;

const plugins = useMemo(() => {
const plugins = [];

if (layerName) {
plugins.push(layerPlugin({ name: layerName }));
}

return plugins;
}, [layerName]);

return (
<ElementsProviderContext.Provider value={{ theme, applyDefaultStyles }}>
<ThemeProvider theme={theme}>
<CssVariables />
{applyDefaultStyles && <CssGlobalDefaults />}
{children}
</ThemeProvider>
</ElementsProviderContext.Provider>
<StyleSheetManager stylisPlugins={plugins}>
<ElementsProviderContext.Provider
value={{ theme, applyDefaultStyles, layerName }}
>
<ThemeProvider theme={theme}>
<CssVariables />
{applyDefaultStyles && <CssGlobalDefaults />}
{children}
</ThemeProvider>
</ElementsProviderContext.Provider>
</StyleSheetManager>
);
}
37 changes: 37 additions & 0 deletions packages/atomic-elements/src/styles/plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Element, Middleware, serialize } from "stylis";

interface LayerPluginOptions {
name?: string;
}

export function layerPlugin(options: LayerPluginOptions = {}): Middleware {
const { name = "elements" } = options;

return (element, _index, _children, callback) => {
if (element.type !== "rule" || element.return) return;

// Skip rules inside @layer (leave alone) or @keyframes (not real CSS rules)
let parent = element.parent;
while (parent) {
if (parent.type === "@layer" || parent.type === "@keyframes") return;
parent = parent.parent;
}

const selector = Array.isArray(element.props)
? element.props.join(",")
: element.props;

// element.children can be a string if element is a declaration,
// but it should always be an array for rules
const declarations = serialize(element.children as Element[], callback);

// Clear children so that middlewares down the line don't overwrite
// element.return with their own stringification of the rule.
element.children = [];

// Set element.return so styled-components' rulesheet collects our output.
// (styled-components ignores serialize()'s return value — it reads element.return instead.)
// Also return it directly for non-rulesheet usage.
return (element.return = `@layer ${name}{${selector}{${declarations}}}`);
};
}