vadimdemedes / tailwind-rn

🦎 Use Tailwind CSS in React Native projects
MIT License
4.24k stars 171 forks source link

Any ideas on how to support Theming ? #47

Closed aswinmohanme closed 3 years ago

aswinmohanme commented 3 years ago

First of all thanks for making such an awesome library, really love using Tailwind everywhere :smiley:

I'm going to start work on a new app, but it would require theming. Are there any pointers on how to support user switchable themes using this library?

I thought about creating a separate function that wraps the tailwind function and which then would replace bg-primary with the current selected theme, but that seems error-prone. Any suggestions are welcome.

I found this library https://www.npmjs.com/package/rn-themed-tailwind but it is nowhere near as mature as this one.

ajsmth commented 3 years ago

i'm not sure if this is supported, but you could in theory write custom configs for your color pallets in a tailwind.config.js:

colors: {
 light: {}
 dark: {}
}

and then access whichever one like:${theme}-bg-primary

aswinmohanme commented 3 years ago
colors: {
 light: {}
 dark: {}
}

and then access whichever one like:${theme}-bg-primary

Finally found the solution based on this. I used react-native-dynamic to create a hook to change the theme based on the device theme.

mport { create } from "tailwind-rn"
import styles from "./styles.json"
import { useDynamicValue } from "react-native-dynamic"

const { tailwind, getColor } = create(styles)

function themedStyle(variant) {
  return function (classNames: string) {
    return tailwind(classNames.replaceAll("theme", variant))
  }
}

// Usage: style = useStyle();
// style('bg-theme-primary') which would be converted either to dark or light based on device theme
const useStyle = function () {
  const variant = useDynamicValue("light", "dark")

  return themedStyle(variant)
}

export { tailwind, getColor, useStyle }
jamoy commented 3 years ago

I've modified the above to make use of a global stylesheet

import { create } from 'tailwind-rn';
import styles from 'themes/styles/styles.json'; // generated by tailwind-rn
import defaults from 'themes/styles/defaults'; // global stylesheet
import { useDynamicValue } from 'react-native-dynamic';

const { tailwind, getColor } = create(styles);

export { tailwind, getColor };

function themedStyle(variant: string, scopedStyles: object = {}) {
  const rootStyles = { ...defaults, ...scopedStyles };
  function parse(...args: Array<object | string>) {
    let derivedStyles = {};
    args.map((definition: string | object) => {
      if (typeof definition === 'string') {
        const classes = definition.split(' ');
        const tailwindClasses = classes
          .filter((cls: string) => !rootStyles[cls])
          .join(' ');
        let classesInString = {};
        classes
          .filter((cls: string) => rootStyles[cls])
          .map((cls: string) => {
            if (Array.isArray(rootStyles[cls])) {
              classesInString = {
                ...classesInString,
                ...parse.apply(null, rootStyles[cls]),
              };
            } else {
              classesInString = { ...classesInString, ...rootStyles[cls] };
            }
          });
        derivedStyles = {
          ...derivedStyles,
          ...tailwind(tailwindClasses.replace(/theme/gim, variant)),
          ...classesInString,
        };
      } else {
        derivedStyles = {
          ...derivedStyles,
          ...definition,
        };
      }
    });
    return derivedStyles;
  }
  return parse;
}

export function useTheme(scopedStyles?: object) {
  const variant = useDynamicValue('light', 'dark');
  return themedStyle(variant, scopedStyles);
}

you can use this in your hooks like before with added features such as

  1. using your root stylesheet key useTheme('bg-theme-primary classInDefaultsJs text-xl')
  2. use multiple arguments useTheme('bg-theme-primary', 'classInDefaultsJs', { padding: 2 })
  3. extend an existing key in your stylesheet from within the stylesheet.
    {
    "classInDefaultsJs": { borderWidth: 3 },
    "inherited": ["classInDefaultsJs", "bg-white", { margin: 2 }]
    }

    and reference that as useTheme('inherited') and will get all the values from the above

  4. you can also scope it in your components via
    import { useTheme } from 'themes/theme';
    const styles = {
    textInput: {
    borderWidth: 2,
    },
    };
    export default () => useTheme(styles);

    and use it as

    
    import useTheme from './style';
    export const Input = (props: TextInputProps) => {
    const theme = useTheme();
    return <TextInput {...props} style={theme('p-4 textInput')} />;
    };


thanks @aswinmohanme for the great idea.
Rudiney commented 3 years ago

I had handle Dark/Light theme on my app and thanks to @aswinmohanme and @jamoy idea i build a solution that ended up pretty close with the official way of writing it: bg-white dark:bg-black etc..

here a glance of it:

<View style={tw("p-4 bg-gray-100 dark:bg-black")}>
  <Text style={tw("text-blue dark:text-white")}>DarkMode:</Text>
</View>

I added a function before the tailwind and getColor functions to handle the dark: string depending on the current color scheme.

Here is the hook code: useTailwind.js

import React, { createContext, useState, useContext } from "react";
import { Appearance } from "react-native";

import { create } from "tailwind-rn";
import styles from "../styles.json";
const { tailwind, getColor } = create(styles);

export default tailwind;
export { getColor };

const TailwindContext = createContext();

function handleThemeClasses(classes, isDarkMode) {
  const regExp = isDarkMode ? /dark:/g : /dark:\S+/g;
  return classes.replace(regExp, "").replace(/\s\s/g, " ").trim();
}

function useTailwind() {
  const context = useContext(TailwindContext);
  if (!context) throw new Error(`useTailwind must be used within a TailwindProvider`);

  const [currentColorScheme, setCurrentColorScheme] = context;
  const isDarkMode = currentColorScheme == "dark";

  return {
    isDarkMode,
    setDarkMode: (isDark) => setCurrentColorScheme(isDark ? "dark" : "light"),
    tw: (classes) => tailwind(handleThemeClasses(classes, isDarkMode)),
    getColor: (colors) => getColor(handleThemeClasses(colors, isDarkMode)),
  };
}

function TailwindProvider({ children }) {
  const contextValue = useState(Appearance.getColorScheme());
  return <TailwindContext.Provider value={contextValue}>{children}</TailwindContext.Provider>;
}

export { TailwindProvider, useTailwind };

and an usage example:

import React from "react";

import { ScrollView, View, Switch } from "react-native";
import { Text, Divider } from "react-native-elements";

import { useTailwind } from "../theme/tailwind";

export default function ConfigScreen() {
  const { isDarkMode, setDarkMode, tw, getColor } = useTailwind();

  return (
    <ScrollView contentContainerStyle={tw("flex-1 p-4 bg-gray-100 dark:bg-black")}>
      <View style={tw("flex-row px-2")}>
        <Text h3 h3Style={tw("flex-1 dark:text-white")}>DarkMode:</Text>
        <Switch value={isDarkMode} onValueChange={() => setDarkMode(!isDarkMode)} />
      </View>

      <Divider style={tw("my-4")} />

      <Text h2 h2Style={tw("dark:text-white")}>
        CurrentMode: {isDarkMode ? "dark" : "light"}
      </Text>

      <Divider style={tw("my-4")} />

      <Text h4 h4Style={tw("dark:text-white mb-2")}>
        getColor('green dark:yellow'):
      </Text>

      <View style={{ backgroundColor: getColor("green-500 dark:yellow-500") }}>
        <Text style={tw("m-3")}>{getColor("green-500 dark:yellow-500")}</Text>
      </View>
    </ScrollView>
  );
}

https://user-images.githubusercontent.com/307759/112774208-4d41d000-900f-11eb-84b4-935d82c3b8dc.mov

hope it helps!