diff --git a/docs/content/1.usage/2.guides/3.class-attr.md b/docs/content/1.usage/2.guides/3.class-attr.md
index f208e114..30eb0541 100644
--- a/docs/content/1.usage/2.guides/3.class-attr.md
+++ b/docs/content/1.usage/2.guides/3.class-attr.md
@@ -1,45 +1,79 @@
---
-title: Class Attribute
-description: Learn how to use the class attribute with Unhead.
+title: Class & Style Attributes
+description: Style your pages by applying classes and styles to your `` and `
` tags.
---
-When using the `htmlAttrs` or `bodyAttrs` options, you can use the `class` attribute to add classes to the `html` or `body` elements.
+When you need to style your page by adding classes or styles to the `` or ``, Unhead makes it easy by
+providing object and array support for the `class` and `style` attributes.
-```ts
+## Static Classes & Styles
+
+If your classes or styles aren't going to change, you can provide them as a string.
+
+::code-block
+
+```ts [Html Attrs]
useHead({
htmlAttrs: {
- class: 'my-class',
+ class: 'my-class my-other-class',
+ style: 'background-color: red; color: white;'
}
})
```
-For improved reactivity and merging support, you can provide the class as an object or an array.
+```ts [Body Attrs]
+useHead({
+ bodyAttrs: {
+ class: 'my-class my-other-class',
+ style: 'background-color: red; color: white;'
+ }
+})
+```
+::
-## Class Object
+Tip: If you're server-side rendering and applying
+it to your default layout, you can use [useServerHead](/api/useServerHead) for a minor performance improvement.
-When providing class as an object, the key should be the class and the value will be whether the class should be added or not.
+### Array Classes & Styles
-```ts
-const darkMode = false
+Using manual separators for classes and styles can be a bit cumbersome, so Unhead allows you to use arrays for both.
+```ts
useHead({
htmlAttrs: {
- class: {
- // will be rendered
- dark: darkMode,
- // will not be rendered
- light: !darkMode,
- }
+ class: [
+ 'my-class',
+ 'my-other-class'
+ ],
+ style: [
+ 'background-color: red',
+ 'color: white'
+ ],
}
})
```
-## Class Array
+## Dynamic Classes & Styles
+
+For improved reactivity and merging support, you can provide the class as an object or an array.
+
+When providing class as an object, the key should be the class and the value will be whether the class should be added or not.
```ts
+const darkMode = ref(false)
+
useHead({
htmlAttrs: {
- class: ['my-class', 'my-other-class'],
+ class: {
+ // will be rendered
+ dark: () => darkMode,
+ // will not be rendered
+ light: () => !darkMode,
+ },
+ style: {
+ // will not render when darkMode is false
+ 'background-color': () => darkMode ? 'rgba(0, 0, 0, 0.9)' : false,
+ }
}
})
```
diff --git a/packages/shared/src/normalise.ts b/packages/shared/src/normalise.ts
index 2c1ad555..a045d963 100644
--- a/packages/shared/src/normalise.ts
+++ b/packages/shared/src/normalise.ts
@@ -47,26 +47,29 @@ export async function normaliseTag(tagName: T['tag'], input:
: tag
}
-export function normaliseClassProp(v: Required['htmlAttrs']['class']>) {
+
+export function normaliseStyleClassProps(key: T, v: Required['htmlAttrs']['class']> | Required['htmlAttrs']['style']>) {
+ const sep = key === 'class' ? ' ' : ';'
if (typeof v === 'object' && !Array.isArray(v)) {
- // @ts-expect-error untyped
- v = Object.keys(v).filter(k => v[k])
+ v = Object.entries(v)
+ .filter(([, v]) => v)
+ .map(([k, v]) => key === 'style' ? `${k}:${v}` : k)
}
// finally, check we don't have spaces, we may need to split again
- return (Array.isArray(v) ? v.join(' ') : v as string)
- .split(' ')
+ return (Array.isArray(v) ? v.join(sep) : v as string)
+ .split(sep)
.filter(c => c.trim())
.filter(Boolean)
- .join(' ')
+ .join(sep)
}
export async function normaliseProps(props: T['props'], virtual?: boolean): Promise {
// handle boolean props, see https://html.spec.whatwg.org/#boolean-attributes
for (const k of Object.keys(props)) {
// class has special handling
- if (k === 'class') {
+ if (['class', 'style'].includes(k)) {
// @ts-expect-error untyped
- props[k] = normaliseClassProp(props[k])
+ props[k] = normaliseStyleClassProps(k, props[k])
continue
}
// first resolve any promises
diff --git a/packages/vue/src/types/schema.ts b/packages/vue/src/types/schema.ts
index c40de07d..e36e2e90 100644
--- a/packages/vue/src/types/schema.ts
+++ b/packages/vue/src/types/schema.ts
@@ -11,13 +11,19 @@ export interface HtmlAttr extends Omit {
class?: MaybeArray> | Record>
}
-export interface BodyAttr extends Omit {
+export interface BodyAttr extends Omit {
/**
* The class global attribute is a space-separated list of the case-sensitive classes of the element.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class
*/
class?: MaybeArray> | Record>
+ /**
+ * The class global attribute is a space-separated list of the case-sensitive classes of the element.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class
+ */
+ style?: MaybeArray> | Record>
}
export type Title = MaybeComputedRef<_Title>
diff --git a/test/unhead/ssr/tagDuplicateStrategy.test.ts b/test/unhead/ssr/tagDuplicateStrategy.test.ts
index be954f04..a6a33689 100644
--- a/test/unhead/ssr/tagDuplicateStrategy.test.ts
+++ b/test/unhead/ssr/tagDuplicateStrategy.test.ts
@@ -41,7 +41,7 @@ describe('tagDuplicateStrategy', () => {
const { htmlAttrs } = await renderSSRHead(head)
expect(htmlAttrs).toMatchInlineSnapshot(
- `" class="html-doc my-specific-page" style="color: red; background: green;""`,
+ `" class="html-doc my-specific-page" style="color: red; background: green""`,
)
})
})
diff --git a/test/vue/dom/styles.test.ts b/test/vue/dom/styles.test.ts
new file mode 100644
index 00000000..05f226e8
--- /dev/null
+++ b/test/vue/dom/styles.test.ts
@@ -0,0 +1,78 @@
+import { describe, it } from 'vitest'
+import { createHead, setHeadInjectionHandler, useHead } from '@unhead/vue'
+import { computed, ref } from 'vue'
+import { renderDOMHead } from '@unhead/dom'
+import { useDom } from '../../fixtures'
+
+describe('vue dom styles', () => {
+ it('empty style', async () => {
+ const dom = useDom()
+
+ const head = createHead({ document: dom.window.document })
+ setHeadInjectionHandler(() => head)
+
+ const isNavActive = ref(false)
+
+ useHead({
+ bodyAttrs: {
+ style: computed(() => {
+ return isNavActive.value ? 'background-color: red' : ''
+ }),
+ },
+ })
+
+ await renderDOMHead(head, { document: dom.window.document })
+ expect(dom.window.document.body.getAttribute('style')).toEqual(null)
+ isNavActive.value = true
+
+ // wait 100ms
+ await new Promise(resolve => setTimeout(resolve, 100))
+ await renderDOMHead(head, { document: dom.window.document })
+ expect(dom.window.document.body.getAttribute('style')).toEqual('background-color: red;')
+ })
+ it('array style', async() => {
+ const dom = useDom()
+
+ const head = createHead({ document: dom.window.document })
+ setHeadInjectionHandler(() => head)
+
+ useHead({
+ bodyAttrs: {
+ style: [
+ 'background-color: red',
+ 'color: white',
+ ],
+ },
+ })
+
+ await renderDOMHead(head, { document: dom.window.document })
+
+ expect(dom.window.document.body.getAttribute('style')).toEqual(`background-color: red; color: white;`)
+ })
+ it('object style', async () => {
+ const dom = useDom()
+
+ const head = createHead({ document: dom.window.document })
+ setHeadInjectionHandler(() => head)
+
+ const isNavActive = ref(false)
+
+ useHead({
+ bodyAttrs: {
+ style: {
+ 'background-color': () => isNavActive.value ? 'red' : '',
+ },
+ },
+ })
+
+ await renderDOMHead(head, { document: dom.window.document })
+ expect(dom.window.document.body.getAttribute('style')).toEqual(null)
+
+ isNavActive.value = true
+
+ // wait 100ms
+ await new Promise(resolve => setTimeout(resolve, 100))
+ await renderDOMHead(head, { document: dom.window.document })
+ expect(dom.window.document.body.getAttribute('style')).toEqual('background-color: red;')
+ })
+})