Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(theming): clarify how nuxt ui augments tailwind config #2010

Open
wants to merge 22 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 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
105 changes: 91 additions & 14 deletions docs/content/1.getting-started/3.theming.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,57 @@
---
title: Theming
description: 'Learn how to customize the look and feel of the components.'
---

This module relies on Nuxt [App Config](https://nuxt.com/docs/guide/directory-structure/app-config#app-config-file) file to customize the look and feel of the components at runtime with HMR (hot-module-replacement).
## Overview

Nuxt UI uses [Tailwind CSS](https://tailwindcss.com) to style its components and drive its color theme.

You'll use two main files to modify the defaults:

- [`app.config.ts`](https://nuxt.com/docs/guide/directory-structure/app-config#app-config-file) to specify the main theme colors and override component styles
- [`tailwind.config.ts`](https://nuxt.com/docs/guide/directory-structure/app-config#app-config-file) to configure individual colors and palettes

## Colors

### Configuration

Components are based on a `primary` and a `gray` color. You can change them in your `app.config.ts`.
Nuxt UI's theme is configured using `primary` and `gray` color palettes:

- `primary` is used for things like primary actions, accents, etc
- `gray` is used for things like secondary actions, text, borders, etc

Both these values indicate [named Tailwind colors](https://tailwindcss.com/docs/customizing-colors).

You can specify these values in your [`app.config.ts`](https://nuxt.com/docs/guide/directory-structure/app-config#app-config-file) file, under the `ui` key:

```ts [app.config.ts]
export default defineAppConfig({
ui: {
// these are the Nuxt UI defaults
primary: 'green',
gray: 'cool'
gray: 'gray'
}
})
```

You options are:

- skip config and accept the defaults, i.e. `green` and `grey`
davestewart marked this conversation as resolved.
Show resolved Hide resolved
- choose any other Tailwind color, i.e. `rose` and `neutral`
- choose nested colors, i.e. `brand.primary` and `brand.secondary`
- override default or create custom colors (see [Customisation](#customisation) below)

::callout{icon="i-heroicons-light-bulb"}
Try to change the `primary` and `gray` colors by clicking on the :u-icon{name="i-heroicons-swatch-20-solid" class="w-4 h-4 align-middle text-primary-500 dark:text-primary-400"} button in the header.
Click the :u-icon{name="i-heroicons-swatch-20-solid" class="w-4 h-4 align-middle text-primary-500 dark:text-primary-400"} button in the header to try out other Tailwind colors!

::

As this module uses Tailwind CSS under the hood, you can use any of the [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors or groups, such as `brand.primary`. By default, the `primary` color is `green` and the `gray` color is `cool`.
### Customisation

When [using custom colors](https://tailwindcss.com/docs/customizing-colors#using-custom-colors) or [adding additional colors](https://tailwindcss.com/docs/customizing-colors#adding-additional-colors) through the `extend` key in your `tailwind.config.ts`, you'll need to make sure to define all the shades from `50` to `950` as most of them are used in the components config defined in [`ui.config/`](https://github.com/nuxt/ui/tree/dev/src/runtime/ui.config) directory. You can [generate your colors](https://tailwindcss.com/docs/customizing-colors#generating-colors) using tools such as https://uicolors.app/ for example.
To use custom shades, you'll need a [tailwind.config.ts](https://tailwindcss.com/docs/installation) to [replace](https://tailwindcss.com/docs/customizing-colors#using-custom-colors) or [extend](https://tailwindcss.com/docs/customizing-colors#adding-additional-colors) the defaults.

In the following example, we extend the default `green` config with a richer hue:

```ts [tailwind.config.ts]
import type { Config } from 'tailwindcss'
Expand Down Expand Up @@ -54,23 +80,74 @@ export default <Partial<Config>>{
}
```

### CSS Variables
Colors must supply all values from `50` to `950` because the Nuxt UI components use the full range.

To provide dynamic colors that can be changed at runtime, this module uses CSS variables. As Tailwind CSS already has a `gray` color, the module automatically renames it to `cool` to avoid conflicts (`coolGray` was renamed to `gray` when Tailwind CSS v3.0 was released).
If you need help in creating the colors, consider an online tools such as [UI Colors](https://uicolors.app).

Likewise, you can't define a `primary` color in your `tailwind.config.ts` as it would conflict with the `primary` color defined by the module.
You can peek at Nuxt UI's classes and values in the `ui.config` folder:

::callout{icon="i-heroicons-light-bulb"}
We'd advise you to use those colors in your components and pages, e.g. `text-primary-500 dark:text-primary-400`, `bg-gray-100 dark:bg-gray-900`, etc. so your app automatically adapts when changing your `app.config.ts`.
::
- on GitHub: [`runtime/ui.config/**/*.ts`](https://github.com/search?q=repo%3Anuxt%2Fui+path%3A%2F%5Esrc%5C%2Fruntime%5C%2Fui%5C.config%5C%2F%2F+-primary&type=code)
- in your project: `node_modules/@nuxt/ui/dist/runtime/ui.config/**/*.mjs`

### Usage

You are encouraged to use these new `primary-*` and `gray-*` classes in your pages and components so that when you [choose a new theme](#configuration) color, your whole design updates.

Note that the `primary` color has a [default](https://tailwindcss.com/docs/customizing-colors#color-object-syntax) shade which lets you specify a color without a modifier:

```vue
<div class="text-primary focus-inside:ring-primary` />
```

The `primary` color also has a `DEFAULT` shade that changes based on the theme. It is `500` in light mode and `400` in dark mode. You can use as a shortcut in your components and pages, e.g. `text-primary`, `bg-primary`, `focus-visible:ring-primary`, etc.
Additionally, it is adjusted based on the color scheme (`500` in light mode and `400` in dark mode).

### How it works

To support a range of tints for `primary` and `grey` Nuxt UI takes several steps:
davestewart marked this conversation as resolved.
Show resolved Hide resolved

1. it renders the chosen theme color values into `:root`-level CSS variables
2. it augments your Tailwind config with two new color config template blocks
3. each template [combines](https://tailwindcss.com/docs/customizing-colors#using-css-variables) the CSS variables with alpha transparency placeholders

The augmented (and [virtual](https://tailwindcss.nuxtjs.org/getting-started/configuration#exposeconfig)) Tailwind configuration looks something like this:

```js
{
primary: {
50: 'rgb(var(--color-primary-50) / <alpha-value>)',
100: 'rgb(var(--color-primary-100) / <alpha-value>)',
200: 'rgb(var(--color-primary-200) / <alpha-value>)',
...
},
gray: {
50: 'rgb(var(--color-gray-50) / <alpha-value>)',
100: 'rgb(var(--color-gray-100) / <alpha-value>)',
200: 'rgb(var(--color-gray-200) / <alpha-value>)',
...
}}
```

The end-result is that a class like `bg-primary-500/30` will be compiled by Tailwind to:

```css
.bg-primary-500\/30 {
color: rgb(var(--color-primary-500) / 0.3);
}
```

This is what allows Nuxt UI to show subtle variations on things like focus rings, borders, etc.

::callout{icon="i-heroicons-bell-alert"}

Note that [before Nuxt UI 2.19.0](https://github.com/nuxt/ui/blob/dev/docs/content/1.getting-started/3.theming.md#css-variables) the color key names `primary` and `gray` were reserved, but a recent update allows configuring these keys and the values will passed straight through as the theme's colors.

::

### Smart Safelisting

Components having a `color` prop like [Avatar](/components/avatar#chip), [Badge](/components/badge#style), [Button](/components/button#style), [Input](/components/input#style) (inherited in [Select](/components/select) and [SelectMenu](/components/select-menu)), [RadioGroup](/components/radio-group), [Checkbox](/components/checkbox), [Toggle](/components/toggle), [Range](/components/range) and [Notification](/components/notification#timeout) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors.

Variant classes of those components are defined with a syntax like `bg-{color}-500 dark:bg-{color}-400` so they can be used with any color. However, this means that Tailwind will not find those classes and therefore will not generate the corresponding CSS.
Variant classes of those components are defined with a syntax like `bg-{color}-500` or `dark:bg-{color}-400` so they can be used with any color. However, this means that Tailwind will not find those classes and therefore will not generate the corresponding CSS.

The module uses the [Tailwind CSS safelist](https://tailwindcss.com/docs/content-configuration#safelisting-classes) feature to force the generation of all the classes for the `primary` color **only** as it is the default color for all the components.

Expand Down
18 changes: 13 additions & 5 deletions src/runtime/plugins/colors.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { computed } from 'vue'
import { get, hexToRgb } from '../utils'
import { defineNuxtPlugin, useAppConfig, useNuxtApp, useHead } from '#imports'
import { defineNuxtPlugin, useAppConfig, useHead, useNuxtApp } from '#imports'
import colors from '#tailwind-config/theme/colors'

export default defineNuxtPlugin(() => {
const appConfig = useAppConfig()
const nuxtApp = useNuxtApp()

const root = computed(() => {
const primary: Record<string, string> | undefined = get(colors, appConfig.ui.primary)
const gray: Record<string, string> | undefined = get(colors, appConfig.ui.gray)
const ui = appConfig.ui
const keyPrimary = '$primary' in colors && ui.primary === 'primary'
? '$primary'
: ui.primary
const keyGray = '$gray' in colors && ui.gray === 'gray'
? '$gray'
: ui.gray

const primary: Record<string, string> | undefined = get(colors, keyPrimary)
const gray: Record<string, string> | undefined = get(colors, keyGray)

if (!primary) {
console.warn(`[@nuxt/ui] Primary color '${appConfig.ui.primary}' not found in Tailwind config`)
console.warn(`[@nuxt/ui] Primary color '${ui.primary}' not found in Tailwind config`)
}
if (!gray) {
console.warn(`[@nuxt/ui] Gray color '${appConfig.ui.gray}' not found in Tailwind config`)
console.warn(`[@nuxt/ui] Gray color '${ui.gray}' not found in Tailwind config`)
}

return `:root {
Expand Down
27 changes: 19 additions & 8 deletions src/runtime/utils/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,18 @@ export const setGlobalColors = (theme: TWConfig['theme']) => {
...theme.extend?.colors
}

// @ts-ignore
globalColors.primary = theme.extend.colors.primary = {
// reference theme as any
const themeColors: any = theme.extend.colors

// track user colors
const userColors: ColorConfig = {}

// primary colors
if (globalColors.primary) {
userColors.$primary = themeColors.$primary = globalColors.primary
}

globalColors.primary = themeColors.primary = {
50: 'rgb(var(--color-primary-50) / <alpha-value>)',
100: 'rgb(var(--color-primary-100) / <alpha-value>)',
200: 'rgb(var(--color-primary-200) / <alpha-value>)',
Expand All @@ -250,13 +260,11 @@ export const setGlobalColors = (theme: TWConfig['theme']) => {
}

if (globalColors.gray) {
// @ts-ignore
globalColors.cool = theme.extend.colors.cool =
defaultColors.gray
userColors.$gray = themeColors.$gray = globalColors.gray
globalColors.cool = themeColors.cool = defaultColors.gray
}

// @ts-ignore
globalColors.gray = theme.extend.colors.gray = {
globalColors.gray = themeColors.gray = {
50: 'rgb(var(--color-gray-50) / <alpha-value>)',
100: 'rgb(var(--color-gray-100) / <alpha-value>)',
200: 'rgb(var(--color-gray-200) / <alpha-value>)',
Expand All @@ -270,7 +278,10 @@ export const setGlobalColors = (theme: TWConfig['theme']) => {
950: 'rgb(var(--color-gray-950) / <alpha-value>)'
}

return excludeColors(globalColors)
return [
...excludeColors(globalColors),
...Object.keys(userColors)
]
}

export const generateSafelist = (colors: string[], globalColors: string[]) => {
Expand Down