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 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
120 changes: 105 additions & 15 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


You can choose which colors to use 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: {
primary: 'green',
gray: 'cool'
primary: 'rose',
gray: 'neutral'
}
})
```

These settings point to [named Tailwind colors](https://tailwindcss.com/docs/customizing-colors); either Nuxt UI's or your project's own.

Your options are:

- skip config and accept the defaults (`green` and `gray`)
- choose different Tailwind colors, e.g. `teal` and `slate`
- create and use custom colors (see [Customisation](#customisation) below)

Note that you can target nested color config, e.g. `brand.primary` and `brand.secondary`

::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.
You can preview available colors now by clicking 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
::

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

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.

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.
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,87 @@ export default <Partial<Config>>{
}
```

### CSS Variables
::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.
::

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).
Colors must supply all values from `50` to `950` because the Nuxt UI components use the full range.

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.
If you need help in creating the colors, consider an online tool such as [UI Colors](https://uicolors.app).

::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`.
::
### 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 color's tint is also tailored to the color scheme – `500` in light mode and `400` in dark mode.

You can peek at Nuxt UI's classes and values in the `ui.config` folder:

- 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`

### How it works

To support a range of tints for your chosen theme, Nuxt UI takes several steps.

First, your Tailwind config is [augmented](https://tailwindcss.nuxtjs.org/getting-started/configuration#exposeconfig) with templates for `primary` and `gray` colors:

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

Then, the `rgb` values for each color and shade are [injected](https://github.com/nuxt/ui/blob/main/src/runtime/plugins/colors.ts) as CSS variables into the site's head:

```css
:root {
--color-primary-50: 239 253 245;
--color-primary-100: 217 251 232;
...
--color-gray-50: 248 250 252;
--color-gray-100: 241 245 249;
...
}
```

Finally, at build-time Tailwind [compiles](https://tailwindcss.com/docs/customizing-colors#using-css-variables) used class names and templates into the final CSS code:

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

Additionally, Nuxt UI supports a "templated" class name format (on internal classes and on [`ui` props](#ui-prop)) which it [replaces](https://github.com/nuxt/ui/blob/d6658209b6f9840b8c7dbf927ba3a47372808254/src/runtime/components/elements/Button.vue#L157) with the app config color or supplied component `color` prop at runtime:

```js
const ui = {
base: 'bg-{color}-200' // will be converted to bg-green-200 at run time
}
```

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.
These process are what allows Nuxt UI to provide subtle variations on focus rings, borders, shadows, etc as well as change theme colors dynamically at runtime.

### 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