├── .gitignore ├── jasmine.json ├── tsconfig.json ├── .github └── workflows │ └── nodejs.yml ├── spec └── index.spec.ts ├── LICENSE ├── package.json ├── index.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | index.js 3 | index.d.ts 4 | .vscode 5 | -------------------------------------------------------------------------------- /jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": ["**/*.spec.ts"], 4 | "helpers": ["helpers/**/*.ts"], 5 | "stopSpecOnExpectationFailure": false, 6 | "random": true 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "jsx": "react-native", 6 | "strict": true, 7 | "moduleResolution": "node", 8 | "skipLibCheck": true, 9 | "declaration": true, 10 | "outDir": "." 11 | }, 12 | "files": ["index.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm ci 23 | npm run build --if-present 24 | npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /spec/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { registerThemes, useTheme } from ".." 2 | 3 | const lightTheme = { backgroundColor: "white" } 4 | const darkTheme = { backgroundColor: "black" } 5 | 6 | const styleSheetFactory = registerThemes({ light: lightTheme, dark: darkTheme }, () => "light") 7 | 8 | const themedStyles = styleSheetFactory(theme => ({ 9 | container: { 10 | backgroundColor: theme.backgroundColor 11 | } 12 | })) 13 | 14 | describe("useTheme with explicit theme name", function() { 15 | it("should return the correct data", function() { 16 | const [styles, theme, name] = useTheme(themedStyles, "dark") 17 | expect(styles.container.backgroundColor).toEqual("black") 18 | expect(theme).toEqual(darkTheme) 19 | expect(name).toEqual("dark") 20 | }) 21 | }) 22 | 23 | describe("useTheme without explicit theme name", function() { 24 | it("should return the correct data", function() { 25 | const [styles, theme, name] = useTheme(themedStyles) 26 | expect(styles.container.backgroundColor).toEqual("white") 27 | expect(theme).toEqual(lightTheme) 28 | expect(name).toEqual("light") 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ward van Teijlingen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-themed-styles", 3 | "version": "0.0.4", 4 | "description": "Dead simple theming for React Native stylesheets", 5 | "author": "Ward van Teijlingen", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/wvteijlingen/react-native-themed-styles.git" 10 | }, 11 | "main": "index.js", 12 | "files": [ 13 | "index.js", 14 | "index.ts", 15 | "index.d.ts" 16 | ], 17 | "scripts": { 18 | "prepublish": "npm run build", 19 | "build": "tsc", 20 | "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json" 21 | }, 22 | "keywords": [ 23 | "react native", 24 | "theme", 25 | "theming", 26 | "stylesheet" 27 | ], 28 | "devDependencies": { 29 | "@types/jasmine": "^3.4.6", 30 | "@types/react-native": "^0.60.17", 31 | "jasmine": "^3.5.0", 32 | "prettier": "1.19.1", 33 | "ts-node": "^8.5.0", 34 | "typescript": "^3.7.2" 35 | }, 36 | "prettier": { 37 | "printWidth": 103, 38 | "trailingComma": "none", 39 | "tabWidth": 2, 40 | "semi": false, 41 | "singleQuote": false 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { ImageStyle, TextStyle, ViewStyle } from "react-native" 2 | 3 | type AppearanceProvider = () => T 4 | 5 | type NamedStyles = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle } 6 | 7 | interface StyleSheetData { 8 | styles: Record 9 | themes: Record 10 | appearanceProvider: AppearanceProvider 11 | } 12 | 13 | export function registerThemes( 14 | themes: Record, 15 | appearanceProvider: AppearanceProvider 16 | ) { 17 | return | NamedStyles>( 18 | fn: (theme: T) => S 19 | ): StyleSheetData => { 20 | const styles: any = {} 21 | for (const [name, theme] of Object.entries(themes)) { 22 | styles[name] = fn(theme as T) 23 | } 24 | return { styles, themes, appearanceProvider } 25 | } 26 | } 27 | 28 | export function useTheme | NamedStyles>( 29 | data: StyleSheetData, 30 | name?: N 31 | ): [NamedStyles, T, N] { 32 | const resolvedName = name || data.appearanceProvider() 33 | const theme = data.themes[resolvedName] 34 | if (!theme) { 35 | throw new Error(`Theme not defined: ${resolvedName}`) 36 | } 37 | const styles = data.styles[resolvedName] 38 | 39 | return [styles, theme, resolvedName] 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/wvteijlingen/react-native-themed-styles/workflows/Node%20CI/badge.svg) 2 | 3 | # react-native-themed-styles 4 | 5 | A small package that allows you to create custom UI themes and use them throughout your app with a useTheme hook. 6 | 7 | It does not impose any structure on your theme, which means you can use it not only for light/dark mode, but also for spacing, fonts or whatever you dream up. 8 | 9 | - No dependencies 10 | - Simple and clear API 11 | - Fully typed 12 | - No new concepts to learn, it builds on StyleSheets and hooks 13 | 14 | ## Installation 15 | 16 | **Using Yarn** 17 | 18 | ``` 19 | yarn add -D react-native-themed-styles 20 | ``` 21 | 22 | **Using NPM** 23 | 24 | ``` 25 | npm install --save-dev react-native-themed-styles 26 | ``` 27 | 28 | **Using copy/paste** 29 | 30 | If you want to keep your dependencies low and don't care about upstream updates, you can also just 31 | copy the index file into your own repository. 32 | 33 | ## Usage 34 | 35 | Define your themes: 36 | 37 | ```ts 38 | // themes.ts 39 | 40 | import { registerThemes } from "react-native-themed-styles" 41 | 42 | const light = { backgroundColor: "white", textColor: "black" } 43 | const dark = { backgroundColor: "black", textColor: "white" } 44 | 45 | const styleSheetFactory = registerThemes( 46 | { light, dark }, // All themes you want to use. 47 | () => "light" // A function that returns the name of the default theme. 48 | ) 49 | 50 | export { styleSheetFactory } 51 | ``` 52 | 53 | Use your themes: 54 | 55 | ```tsx 56 | // my-component.tsx 57 | 58 | import { useTheme } from "react-native-themed-styles" 59 | import { styleSheetFactory } from "./themes" 60 | 61 | const themedStyles = styleSheetFactory(theme => ({ 62 | container: { 63 | backgroundColor: theme.backgroundColor, 64 | flex: 1 65 | }, 66 | text: { 67 | color: theme.textColor 68 | } 69 | })) 70 | 71 | const MyComponent = () => { 72 | const [styles] = useTheme(themedStyles) 73 | 74 | return ( 75 | 76 | Hello there 77 | 78 | ) 79 | } 80 | ``` 81 | 82 | ## Mirroring the OS theme 83 | 84 | You most likely want your app to automatically switch themes based on the OS theme, i.e. dark or light mode. 85 | You can easily implement this with the `react-native-appearance` package, by using its `useColorScheme` hook in the second argument of `registerThemes`: 86 | 87 | ```ts 88 | import { useColorScheme } from "react-native-appearance" 89 | import { registerThemes } from "react-native-themed-styles" 90 | 91 | const styleSheetFactory = registerThemes({ light, dark }, () => { 92 | const colorScheme = useColorScheme() 93 | return ["light", "dark"].includes(colorScheme) ? colorScheme : "light" 94 | }) 95 | ``` 96 | 97 | ## API 98 | 99 | ### Function: `registerThemes(themes, appearanceProvider)` 100 | 101 | Use this function to register your themes. This will return a factory function that you can use to create a themed StyleSheet. 102 | 103 | **Parameters** 104 | 105 | - `themes`: An object containing all your themes, keyed by name. All themes must have the same data structure. 106 | - `appearanceProvider`: A function that returns the name of the default theme. 107 | 108 | **Returns** 109 | 110 | ``` 111 | ThemedStyleSheetCreator 112 | ``` 113 | 114 | --- 115 | 116 | ### Function: `ThemedStyleSheetCreator` 117 | 118 | A function that you can use to create a themed StyleSheet. 119 | 120 | **Parameters** 121 | 122 | - `callback`: A callback from which you must return an object of styles, as you would when using `StyleSheet.create`. You can access the `theme` 123 | argument to access your theme data. 124 | 125 | **Returns** 126 | 127 | ``` 128 | ThemedStyleSheet 129 | ``` 130 | 131 | --- 132 | 133 | ### Function: `useTheme(themedStyleSheet[, themeName])` 134 | 135 | Use this function to apply a theme and retrieve computed component styles. 136 | 137 | **Parameters** 138 | 139 | - `themedStyleSheet`: A `ThemedStyleSheet` as returned from the `createStyles` function. 140 | - `themeName`: Optional string defining which theme to apply. If not passed, it applies the theme returned by the `appearanceProvider` that you passed to the `registerThemes` function. 141 | 142 | **Returns** 143 | 144 | ``` 145 | [styles, theme, themeName] 146 | ``` 147 | 148 | A tuple containing the following entries: 149 | 150 | - `styles`: The styles with the theme applied 151 | - `theme`: The raw theme that was applied. 152 | - `themeName`: The name of the applied theme. 153 | --------------------------------------------------------------------------------