From d2056faff92263d4258888f6067465dc2ae307df Mon Sep 17 00:00:00 2001 From: Sean Collings Date: Fri, 27 Feb 2026 09:42:11 -0700 Subject: [PATCH] Implement support for wrapping styles in a css @layer rule --- .changeset/kind-crabs-march.md | 5 + .../docs/Guides/Customization/CSSLayers.mdx | 104 ++++++++++++++++++ .../src/components/ElementsProvider.tsx | 48 ++++++-- .../atomic-elements/src/styles/plugins.ts | 37 +++++++ 4 files changed, 183 insertions(+), 11 deletions(-) create mode 100644 .changeset/kind-crabs-march.md create mode 100644 packages/atomic-elements/docs/Guides/Customization/CSSLayers.mdx create mode 100644 packages/atomic-elements/src/styles/plugins.ts diff --git a/.changeset/kind-crabs-march.md b/.changeset/kind-crabs-march.md new file mode 100644 index 000000000..cd1131dc5 --- /dev/null +++ b/.changeset/kind-crabs-march.md @@ -0,0 +1,5 @@ +--- +"@atomicjolt/atomic-elements": minor +--- + +Wrap all styles in a @layer elements directive for easier style overrides diff --git a/packages/atomic-elements/docs/Guides/Customization/CSSLayers.mdx b/packages/atomic-elements/docs/Guides/Customization/CSSLayers.mdx new file mode 100644 index 000000000..446da5f92 --- /dev/null +++ b/packages/atomic-elements/docs/Guides/Customization/CSSLayers.mdx @@ -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 = () => ( + + + +); +``` + +## 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 + + + +``` + +## 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 + + + +``` diff --git a/packages/atomic-elements/src/components/ElementsProvider.tsx b/packages/atomic-elements/src/components/ElementsProvider.tsx index 10c7e6a5f..edb5f0fed 100644 --- a/packages/atomic-elements/src/components/ElementsProvider.tsx +++ b/packages/atomic-elements/src/components/ElementsProvider.tsx @@ -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 { @@ -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 ( - - - - {applyDefaultStyles && } - {children} - - + + + + + {applyDefaultStyles && } + {children} + + + ); } diff --git a/packages/atomic-elements/src/styles/plugins.ts b/packages/atomic-elements/src/styles/plugins.ts new file mode 100644 index 000000000..20a5030b6 --- /dev/null +++ b/packages/atomic-elements/src/styles/plugins.ts @@ -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}}}`); + }; +}