reactjstypescripttailwind-cssvitetailwind-css-4

External components lib source from v4


As my project contains 2 tauri apps with React as frontend, I decided to create an external react components library I will import in each app.

The main problem I have is I'm using tailwindcss 4 with both apps ans the lib. But, When I import components from the lib, in app, they are not correctly styled.

My workspace looks like this (it's a nestjs workspace in which I've added tauri apps)

| - apps
|  | - admin-app
|  | - client-app
| - libs
|  | - components

My libs/components/vite.config.ts

import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [tailwindcss(), react()],
  build: {
    lib: {
      entry: {
        'components/index': resolve(__dirname, 'src/components/index.ts'),
        'contexts/index': resolve(__dirname, 'src/contexts/index.ts'),
        'helpers/index': resolve(__dirname, 'src/helpers/index.ts'),
        'hooks/index': resolve(__dirname, 'src/hooks/index.ts'),
      },
      formats: ['es', 'cjs'],
      fileName: (format, entryName) => {
        return `${entryName}.${format === 'es' ? 'es' : 'cjs'}.js`;
      },
    },
    cssCodeSplit: false,
    rollupOptions: {
      external: ['react', 'react-dom', 'react/jsx-runtime'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
    sourcemap: true,
    emptyOutDir: true,
  },
});

My admin-app/vite.config.ts

import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import { defineConfig } from 'vite';

const host = process.env.TAURI_DEV_HOST;

// https://vitejs.dev/config/
export default defineConfig(async () => ({
  plugins: [tailwindcss(), react()],
  resolve: {
    alias: {
      '@app/client': path.resolve(__dirname, '../../libs/client/src'),
      '@app/components': path.resolve(__dirname, '../../libs/components/src'),
      '@app/interfaces': path.resolve(__dirname, '../../libs/interfaces/src'),
    },
  },
  clearScreen: false,
  server: {
    fs: {
      allow: ['..', '../../libs'],
    },
    port: 1420,
    strictPort: true,
    host: host || false,
    hmr: host
      ? {
          protocol: 'ws',
          host,
          port: 1421,
        }
      : undefined,
    watch: {
      ignored: ['**/src-tauri/**'],
    },
  },
}));

And finally a form where inputs are imported from libs/components

import { Schema } from '@app/client';
import { SubmitButton, TextInput } from '@app/components/components';
import { FormProvider } from 'react-hook-form';
import { useCustomForm } from '../../hooks/useCustomForm';
import { IFormProps } from '../../ts/Form';
import { defaultProductCreateValues } from '../../ts/Values';

interface IProductFormProps extends IFormProps<typeof Schema.ProductCreate> {}

export const ProductForm = ({
  values: val,
  onCancel,
  onSubmit,
}: IProductFormProps) => {
  const { methods, values, isSubmitable } = useCustomForm({
    defaultValues: defaultProductCreateValues(val),
    schema: Schema.ProductCreate,
  });

  return (
    <FormProvider {...methods}>
      <form
        className="grid grid-cols-6 gap-y-4"
        onSubmit={methods.handleSubmit(onSubmit)}
      >
        <TextInput
          className="col-span-6"
          value={values['name']}
          name="name"
          label="Nom"
          register={methods.register}
          type="text"
        />
        <SubmitButton
          text="Valider"
          disabled={!isSubmitable()}
          onCancel={onCancel}
        />
      </form>
    </FormProvider>
  );
};

SubmitButton.tsx

import { Fragment } from 'react';
import { classNames } from '../../helpers/style';

import '../../style.css';

interface ISubmitButtonProps {
  cancelDisabled?: boolean;
  disabled?: boolean;
  text?: string;
  fullSize?: boolean;
  onCancel?: () => void;
  onValidate?: () => void;
}

export const SubmitButton = ({
  text,
  cancelDisabled,
  disabled = false,
  fullSize = false,
  onCancel,
  onValidate,
}: ISubmitButtonProps) => {
  return (
    <Fragment>
      {onCancel ? (
        <div
          className={classNames(
            fullSize ? 'justify-between' : 'justify-end',
            'flex gap-x-4 col-span-6',
          )}
        >
          <button
            disabled={disabled}
            type="submit"
            onClick={onValidate}
            className={classNames(
              fullSize && 'flex-1',
              'bg-green-600 hover:bg-green-500 disabled:bg-gray-800 disabled:text-gray-500 disabled:border disabled:border-gray-900 py-2 px-4 rounded-md',
            )}
          >
            Valider
          </button>
          {onCancel && (
            <button
              type="button"
              disabled={cancelDisabled}
              className={classNames(
                fullSize && '',
                'bg-red-600 hover:bg-red-500 disabled:bg-gray-800 disabled:text-gray-500 disabled:border disabled:border-gray-900 py-2 px-4 rounded-md',
              )}
              onClick={onCancel}
            >
              Annuler
            </button>
          )}
        </div>
      ) : (
        <button
          disabled={disabled}
          className="bg-blue-500 text-gray-200 disabled:bg-gray-800 disabled:text-gray-500 col-span-6 uppercase py-2 mt-4 rounded-md font-semibold"
        >
          {text}
        </button>
      )}
    </Fragment>
  );
};

As you can see, there are colors defined for the buttons but, the result looks like this.

enter image description here

PS: components lib style is applied in admin-app/App.tsx

import { DialogArea, NotificationArea } from '@app/components/components';
import { RouterProvider } from 'react-router-dom';
import { router } from './routes';

import '@app/components/style.css';
import './assets/styles/style.css';

function App() {
  return (
    <DialogArea>
      <NotificationArea>
        <RouterProvider router={router} />
      </NotificationArea>
    </DialogArea>
  );
}

export default App;

Solution

  • Although you haven't shared your TailwindCSS configuration, I believe your issue is related to source declaration.

    Tailwind v4 introduced automatic source detection. With this, you no longer need to manually declare the paths to your source files and folders where class names are used. However, to save resources, automatic source detection ignores paths listed in .gitignore, as well as the node_modules directory. Understandably, it also doesn't include any sources located outside the project root by default.

    The CSS-first configuration - which is now the only supported method as of v4 - provides a @source CSS directive that allows you to expand or restrict the list of source files. In your case, you need to expand it by adding the src folders from your external libs directory like this:

    ./style.css

    @import "tailwindcss";
    
    @source "./../../libs/client/src";
    @source "./../../libs/components/src";
    @source "./../../libs/interfaces/src";
    

    Important: Note that you need to use a relative path from your main CSS file. Only include paths that actually contain files where TailwindCSS classes might be used.