diff --git a/docs/_guide/styleable.md b/docs/_guide/styleable.md
new file mode 100644
index 00000000..9e203600
--- /dev/null
+++ b/docs/_guide/styleable.md
@@ -0,0 +1,104 @@
+---
+chapter: 8
+subtitle: Bringing CSS into ShadowDOM
+hidden: true
+---
+
+Components with ShadowDOM typically want to introduce some CSS into their ShadowRoots. This is done with the use of `adoptedStyleSheets`, which can be a little cumbersome, so Catalyst provides the `@style` decorator and `css` utility function to more easily add CSS to your component.
+
+If your CSS lives in a different file, you can import the file with using the `assert { type: 'css' }` import assertion. You might need to configure your bundler tool to allow for this. If you're unfamiliar with this feature, you can [check out the web.dev article on CSS Module Scripts](https://web.dev/css-module-scripts/):
+
+```typescript
+import {controller, style} from '@github/catalyst'
+import DesignSystemCSS from './my-design-system.css' assert { type: 'css' }
+
+@controller
+class UserRow extends HTMLElement {
+ @style designSystem = DesignSystemCSS
+
+ connectedCallback() {
+ this.attachShadow({ mode: 'open' })
+ // adoptedStyleSheets now includes our DesignSystemCSS!
+ console.assert(this.shadowRoot.adoptedStyleSheets.includes(this.designSystem))
+ }
+}
+```
+
+Multiple `@style` tags are allowed, each one will be applied to the `adoptedStyleSheets` meaning you can split your CSS without worry!
+
+```typescript
+import {controller} from '@github/catalyst'
+import UtilityCSS from './my-design-system/utilities.css' assert { type: 'css' }
+import NormalizeCSS from './my-design-system/normalize.css' assert { type: 'css' }
+import UserRowCSS from './my-design-system/components/user-row.css' assert { type: 'css' }
+
+@controller
+class UserRow extends HTMLElement {
+ @style utilityCSS = UtilityCSS
+ @style normalizeCSS = NormalizeCSS
+ @style userRowCSS = UserRowCSS
+
+ connectedCallback() {
+ this.attachShadow({ mode: 'open' })
+ }
+}
+```
+
+### Defining CSS in JS
+
+Sometimes it can be useful to define small snippets of CSS within JavaScript itself, and so for this we have the `css` helper function which can create a `CSSStyleSheet` object on-the-fly:
+
+```typescript
+import {controller, style, css} from '@github/catalyst'
+
+@controller
+class UserRow extends HTMLElement {
+ @style componentCSS = css`:host { display: flex }`
+
+ connectedCallback() {
+ this.attachShadow({ mode: 'open' })
+ }
+}
+```
+
+As always though, the best way to handle dynamic per-instance values is with CSS variables:
+
+```typescript
+import {controller, style, css} from '@github/catalyst'
+
+const sizeCSS = (size = 1) => css`:host { font-size: var(--font-size, ${size}em); }`
+
+@controller
+class UserRow extends HTMLElement {
+ @style componentCSS = sizeCSS
+
+ @attr set fontSize(n: number) {
+ this.style.setProperty('--font-size', n)
+ }
+}
+```
+```html
+Alex
+Riley
+```
+
+The `css` function is memoized; it will always return the same `CSSStyleSheet` object for every callsite. This allows you to "lift" it into a function that can change the CSS for all components by calling the function, which will replace the CSS inside it.
+
+```typescript
+import {controller, style, css} from '@github/catalyst'
+
+const sizeCSS = (size = 1) => css`:host { font-size: ${size}em; }`
+
+// Calling sizeCSS will always result in the same CSSStyleSheet object
+console.assert(sizeCSS(1) === sizeCSS(2))
+
+@controller
+class UserRow extends HTMLElement {
+ @style componentCSS = sizeCSS
+
+ #size = 1
+ makeAllUsersLargerFont() {
+ sizeCSS(this.#size++)
+ }
+}
+```
diff --git a/src/stylable.ts b/src/stylable.ts
new file mode 100644
index 00000000..cd9cabfc
--- /dev/null
+++ b/src/stylable.ts
@@ -0,0 +1,53 @@
+import type {CustomElementClass, CustomElement} from './custom-element.js'
+import {controllable, attachShadowCallback} from './controllable.js'
+import {createMark} from './mark.js'
+import {createAbility} from './ability.js'
+
+type TemplateString = {raw: readonly string[] | ArrayLike}
+
+const cssMem = new WeakMap()
+export const css = (strings: TemplateString, ...values: unknown[]): CSSStyleSheet => {
+ if (!cssMem.has(strings)) cssMem.set(strings, new CSSStyleSheet())
+ const sheet = cssMem.get(strings)!
+ sheet.replaceSync(String.raw(strings, ...values))
+ return sheet
+}
+
+const [style, getStyle, initStyle] = createMark(
+ ({name, kind}) => {
+ if (kind === 'setter') throw new Error(`@style cannot decorate setter ${String(name)}`)
+ if (kind === 'method') throw new Error(`@style cannot decorate method ${String(name)}`)
+ },
+ (instance: CustomElement, {name, kind, access}) => {
+ return {
+ get: () => (kind === 'getter' ? access.get!.call(instance) : access.value),
+ set: () => {
+ throw new Error(`Cannot set @style ${String(name)}`)
+ }
+ }
+ }
+)
+
+export {style, getStyle}
+export const stylable = createAbility(
+ (Class: T): T =>
+ class extends controllable(Class) {
+ [key: PropertyKey]: unknown
+
+ // TS mandates Constructors that get mixins have `...args: any[]`
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ constructor(...args: any[]) {
+ super(...args)
+ initStyle(this)
+ }
+
+ [attachShadowCallback](root: ShadowRoot) {
+ super[attachShadowCallback]?.(root)
+ const styleProps = getStyle(this)
+ if (!styleProps.size) return
+ const styles = new Set([...root.adoptedStyleSheets])
+ for (const name of styleProps) styles.add(this[name] as CSSStyleSheet)
+ root.adoptedStyleSheets = [...styles]
+ }
+ }
+)
diff --git a/test/styleable.ts b/test/styleable.ts
new file mode 100644
index 00000000..71b5f94d
--- /dev/null
+++ b/test/styleable.ts
@@ -0,0 +1,54 @@
+import {expect, fixture, html} from '@open-wc/testing'
+import {style, css, stylable} from '../src/stylable.js'
+
+describe('Styleable', () => {
+ const globalCSS = ({color}: {color: string}) =>
+ css`
+ :host {
+ color: ${color};
+ }
+ `
+
+ @stylable
+ class StylableTest extends HTMLElement {
+ @style foo = css`
+ body {
+ display: block;
+ }
+ `
+ @style bar = globalCSS({color: 'rgb(255, 105, 180)'})
+
+ constructor() {
+ super()
+ this.attachShadow({mode: 'open'}).innerHTML = 'Hello
'
+ }
+ }
+ window.customElements.define('stylable-test', StylableTest)
+
+ it('adoptes styles into shadowRoot', async () => {
+ const instance = await fixture(html``)
+ expect(instance.foo).to.be.instanceof(CSSStyleSheet)
+ expect(instance.bar).to.be.instanceof(CSSStyleSheet)
+ expect(instance.shadowRoot!.adoptedStyleSheets).to.eql([instance.foo, instance.bar])
+ })
+
+ it('updates stylesheets that get recomputed', async () => {
+ const instance = await fixture(html``)
+ expect(getComputedStyle(instance.shadowRoot!.children[0]!).color).to.equal('rgb(255, 105, 180)')
+ globalCSS({color: 'rgb(0, 0, 0)'})
+ expect(getComputedStyle(instance.shadowRoot!.children[0]!).color).to.equal('rgb(0, 0, 0)')
+ })
+
+ it('throws an error when trying to set stylesheet', async () => {
+ const instance = await fixture(html``)
+ expect(() => (instance.foo = css``)).to.throw(/Cannot set @style/)
+ })
+
+ describe('css', () => {
+ it('returns the same CSSStyleSheet for subsequent calls from same template string', () => {
+ expect(css``).to.not.equal(css``)
+ const mySheet = () => css``
+ expect(mySheet()).to.equal(mySheet())
+ })
+ })
+})