├── .eslintrc.base.js ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── apps ├── boilerplate-nextjs │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── api │ │ │ └── hello.ts │ │ └── index.tsx │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── components │ │ │ ├── Header.tsx │ │ │ ├── Layout.tsx │ │ │ ├── Meta.tsx │ │ │ ├── about.mdx │ │ │ ├── about.tsx │ │ │ ├── example-components.tsx │ │ │ ├── footer.tsx │ │ │ ├── intro.tsx │ │ │ ├── page.tsx │ │ │ └── theme-switcher.tsx │ │ ├── design-system.ts │ │ ├── images │ │ │ └── logo.svg │ │ └── providers │ │ │ ├── index.tsx │ │ │ ├── mdx-provider.tsx │ │ │ └── prism-theme.ts │ └── tsconfig.json └── example-parcel │ ├── . parcelrc │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── custom.d.ts │ ├── emotion.d.ts │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.tsx │ ├── components │ │ ├── Header.tsx │ │ ├── Layout.tsx │ │ ├── footer.tsx │ │ ├── page.tsx │ │ └── theme-switcher.tsx │ ├── design-system.ts │ ├── images │ │ └── logo.svg │ ├── index.tsx │ ├── pages │ │ └── home │ │ │ ├── about.tsx │ │ │ ├── example-components.tsx │ │ │ ├── home.tsx │ │ │ └── intro.tsx │ ├── providers │ │ └── index.tsx │ └── setupTests.ts │ └── tsconfig.json ├── babel.config.js ├── package.json ├── packages └── design-system │ ├── .gitignore │ ├── .storybook │ ├── main.js │ └── preview.js │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── components │ │ ├── Box │ │ │ ├── Box.stories.tsx │ │ │ ├── Box.tsx │ │ │ └── index.ts │ │ ├── Button │ │ │ ├── Button.stories.tsx │ │ │ ├── Button.styles.ts │ │ │ ├── Button.tsx │ │ │ └── index.ts │ │ ├── CSSReset │ │ │ ├── CSSReset.tsx │ │ │ ├── index.ts │ │ │ └── reset.tsx │ │ ├── Callout │ │ │ ├── Callout.stories.tsx │ │ │ ├── Callout.tsx │ │ │ └── index.ts │ │ ├── Text │ │ │ ├── Text.stories.tsx │ │ │ ├── Text.styles.ts │ │ │ ├── Text.tsx │ │ │ └── index.ts │ │ ├── ThemeProvider │ │ │ ├── ThemeProvider.tsx │ │ │ ├── global-styles.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── core │ │ ├── style-presets.ts │ │ ├── theme.ts │ │ └── types.ts │ ├── examples │ │ └── Homepage │ │ │ ├── Intro.tsx │ │ │ ├── Layout.tsx │ │ │ ├── Page.tsx │ │ │ ├── Principles.tsx │ │ │ └── ThemeSwitcher.tsx │ ├── hooks │ │ └── useToggleTheme.ts │ ├── index.ts │ ├── storybook-helpers │ │ ├── StorybookLayout.tsx │ │ └── args.ts │ ├── tokens │ │ ├── breakpoints.ts │ │ ├── colors.ts │ │ ├── radii.ts │ │ ├── space.ts │ │ └── typography.ts │ ├── types │ │ └── index.ts │ └── util │ │ ├── __tests__ │ │ ├── create-theme.test.ts │ │ └── object.test.ts │ │ ├── create-theme.ts │ │ ├── helpers.ts │ │ └── object.ts │ ├── tsconfig.jest.json │ └── tsconfig.json ├── tsconfig.json └── yarn.lock /.eslintrc.base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | es6: true, 6 | }, 7 | ignorePatterns: ['node_modules/*', '.eslintrc.js'], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | ecmaFeatures: { 11 | jsx: true, 12 | globalReturn: false, 13 | }, 14 | ecmaVersion: 2020, 15 | }, 16 | settings: { 17 | react: { 18 | version: 'detect', 19 | }, 20 | 'import/resolver': { 21 | typescript: {}, 22 | }, 23 | }, 24 | extends: [ 25 | 'plugin:import/errors', 26 | 'plugin:import/warnings', 27 | 'plugin:import/typescript', 28 | 'plugin:@typescript-eslint/recommended', 29 | 'plugin:regexp/recommended', 30 | 'plugin:prettier/recommended', 31 | ], 32 | plugins: ['@emotion', 'jest'], 33 | globals: { 34 | context: 'readonly', 35 | cy: 'readonly', 36 | assert: 'readonly', 37 | Cypress: 'readonly', 38 | }, 39 | rules: { 40 | 'linebreak-style': ['error', 'unix'], 41 | 'no-empty-function': 'off', 42 | '@typescript-eslint/no-empty-function': [ 43 | 'error', 44 | { allow: ['private-constructors'] }, 45 | ], 46 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 47 | 'import/default': 'off', 48 | 'import/no-named-as-default-member': 'off', 49 | 'import/no-named-as-default': 'off', 50 | 'import/order': [ 51 | 'error', 52 | { 53 | groups: [ 54 | 'builtin', 55 | 'external', 56 | 'internal', 57 | 'parent', 58 | 'sibling', 59 | 'index', 60 | 'object', 61 | ], 62 | alphabetize: { order: 'asc', caseInsensitive: true }, 63 | }, 64 | ], 65 | }, 66 | overrides: [ 67 | { 68 | // For performance run jest/recommended on test files, not regular code 69 | files: ['**/?(*.)+(test|spec).{js,jsx,ts,tsx}'], 70 | extends: ['plugin:jest/recommended'], 71 | rules: { 72 | '@typescript-eslint/no-non-null-assertion': 'off', 73 | '@typescript-eslint/no-object-literal-type-assertion': 'off', 74 | '@typescript-eslint/no-empty-function': 'off', 75 | }, 76 | }, 77 | { 78 | files: ['*.js'], 79 | rules: { 80 | '@typescript-eslint/ban-ts-comment': 'off', 81 | '@typescript-eslint/no-explicit-any': 'off', 82 | '@typescript-eslint/no-var-requires': 'off', 83 | '@typescript-eslint/explicit-module-boundary-types': 'off', 84 | 'import/order': 'off', 85 | }, 86 | }, 87 | ], 88 | }; 89 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | './.eslintrc.base.js', 5 | 'plugin:react/recommended', 6 | 'plugin:react-hooks/recommended', 7 | 'plugin:jsx-a11y/recommended', 8 | ], 9 | // By loading testing-library as a plugin, we can only enable it 10 | // on test files via overrides. 11 | ignorePatterns: ['**/dist/*', '**/.storybook/*'], 12 | plugins: ['testing-library'], 13 | // plugins: ['testing-library', 'storybook'], 14 | env: { 15 | browser: true, 16 | es6: true, 17 | node: true, 18 | }, 19 | rules: { 20 | 'react/prop-types': 'off', 21 | 'react/react-in-jsx-scope': 'off', 22 | 'jsx-a11y/anchor-is-valid': 'off', 23 | }, 24 | overrides: [ 25 | { 26 | // For performance run jest/recommended on test files, not regular code 27 | files: ['**/__tests__/**/*.{ts,tsx}'], 28 | extends: ['plugin:testing-library/react'], 29 | }, 30 | // { 31 | // // For performance run storybook/recommended on test files, not regular code 32 | // files: ['**/*.stories.{ts,tsx,mdx}'], 33 | // extends: ['plugin:storybook/recommended'], 34 | // }, 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .parcel-cache 2 | 3 | .vscode 4 | 5 | /node_modules 6 | 7 | yarn-error.log 8 | 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | **/.next/** 3 | **/dist/** 4 | **/tmp/** -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "tabWidth": 2, 5 | "bracketSpacing": true, 6 | "trailingComma": "es5", 7 | "bracketSameLine": true, 8 | "useTabs": false, 9 | "endOfLine": "auto", 10 | "overrides": [ 11 | { 12 | "files": "*.md", 13 | "options": { 14 | "singleQuote": false, 15 | "quoteProps": "preserve" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dinesh Pandiyan 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Design System Boilerplate 2 | 3 | A highly scalable, performance focused design system boilerplate setup with **configurable tokens**, **smart theming**, **strong types** and auto-generated css variables. 4 | 5 | > You can **server render themes** with zero runtime concerns. [This website](https://design-system-boilerplate.netlify.app) is server rendered with light theme. Try reloading the page after disabling JavaScript and you'll see the server rendered theme in action. 6 | 7 | ## How does it work? 8 | 9 | - Themes are statically compiled (no runtime theming). 10 | - Components are styled using [Emotion](https://emotion.sh/docs/introduction). 11 | - Tokens are based on [System UI Theme Specification](https://system-ui.com/theme/). 12 | - Components are built with [Styled System](https://styled-system.com/) component API for rapid UI development. 13 | - Strong types with full token autocomplete support in component APIs 14 | 15 | ## Principles 16 | 17 | The design system is built with a set of constraints that allows us to statically compile theme definitions and build flexibile APIs. 18 | 19 | - Predefine all your themes. i.e _no runtime theme creation_. This allows us to statically define themes using css variables and create strongly typed theme tokens. Strong types help with full autocomplete support in component APIs. Eg. 20 | 21 | - **Box** component `backgroundColor` prop autocompletes with available color tokens - `primary`, `secondary`, etc. 22 | - **Box** component `padding` prop autocompletes with available spacing tokens - `small`, `large`, etc. 23 | 24 | - Token groups are identified and based on [System UI Theme Specification](https://system-ui.com/theme/). Eg. `colors`, `space`, `fontSizes`, etc. 25 | 26 | - Keep your token groups flat. Don't nest your tokens within token groups. Eg. `colors.primary` is allowed. `colors.primary.success` is not allowed. 27 | 28 | - All themes should have the same set of tokens. Eg. _dark_ theme cannot have a token that's not available in _light_ theme. 29 | 30 | - Theming is setup with CSS custom properties (css variables). This makes it easy to switch or server render themes without runtime concerns. Eg. `token.colors.primary` has the same css variable across themes and makes it easy to statically define styles instead of defining the styles during runtime based on theme. `background: ${tokens.colors.primary}` instead of `background: ${(prop) => prop.theme.colors.primary}`. 31 | 32 | ## How does theming work? 33 | 34 | - Theme definitions are automatically converted to css variables using the `createTheme` utility. 35 | - The generated css variables are theme-name scoped in `ThemeProvider`. 36 | 37 | This is how the generated css variables are inserted into the global stylesheet — 38 | 39 | ```css 40 | body[data-theme='light'] { 41 | --theme-colors-primary: blue; 42 | --theme-colors-secondary: lightblue; 43 | } 44 | body[data-theme='dark'] { 45 | --theme-colors-primary: green; 46 | --theme-colors-secondary: lightgreen; 47 | } 48 | ``` 49 | 50 | So when you use a token in your component — 51 | 52 | ```jsx 53 | const Example = () => { 54 | return ( 55 |
Hello World
56 | ); 57 | }; 58 | ``` 59 | 60 | This is what is used as token value — 61 | 62 | ```jsx 63 | const Example = () => { 64 | return ( 65 |
66 | Hello World 67 |
68 | ); 69 | }; 70 | ``` 71 | 72 | And the css variable `--theme-colors-primary` value is scoped based on the data attribute in body element — `` 73 | 74 | ## Server rendering themes 75 | 76 | Theming is completely css driven. All themes are statically defined using tokens which are then converted to css variables. Current theme is decided based on a html attribute on body element. Eg. ``. So server rendering themes is just a matter of rendering the right theme name as body attribute. 77 | 78 | ## Example theme usage 79 | 80 | ### Using tokens 81 | 82 | All themes use the same css variable names as token values. So you can define the styles statically without needing runtime theme prop. 83 | 84 | ```jsx 85 | import { tokens } from '@unpublished/design-system'; 86 | 87 | const Example = () => { 88 | return ( 89 |
Hello World
90 | ); 91 | }; 92 | ``` 93 | 94 | ### Getting current theme name 95 | 96 | Getting current theme is as simple as querying the DOM attribute. You don't need Context or fancy hooks to provide you the value. 97 | 98 | ```js 99 | const themeName = document.body.getAttribute('data-theme'); 100 | ``` 101 | 102 | _Note: You can use a context based hook if you need — `useTheme()`._ 103 | 104 | ### Switching theme 105 | 106 | Switching theme is as simple as setting the theme name on body element. Since themes are completely css driven, themes can be changed without re-rendering the whole tree. 107 | 108 | ```js 109 | document.body.setAttribute('data-theme', 'dark'); 110 | ``` 111 | 112 | _Note: You can also re-render the whole tree on theme change using `useTheme()` hook if you have to._ 113 | 114 | ## License 115 | 116 | MIT © [Dinesh Pandiyan](https://github.com/flexdinesh) 117 | -------------------------------------------------------------------------------- /apps/boilerplate-nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /apps/boilerplate-nextjs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /apps/boilerplate-nextjs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | 'next/babel', 5 | { 6 | 'preset-react': { 7 | runtime: 'automatic', 8 | importSource: '@emotion/react', 9 | }, 10 | }, 11 | ], 12 | ], 13 | plugins: ['@emotion/babel-plugin'], 14 | }; 15 | -------------------------------------------------------------------------------- /apps/boilerplate-nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/boilerplate-nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const withMDX = require('@next/mdx')({ 4 | extension: /\.mdx?$/, 5 | options: { 6 | remarkPlugins: [], 7 | rehypePlugins: [], 8 | // If you use `MDXProvider`, uncomment the following line. 9 | providerImportSource: '@mdx-js/react', 10 | }, 11 | }); 12 | 13 | module.exports = withMDX({ 14 | reactStrictMode: true, 15 | pageExtensions: ['ts', 'tsx'], 16 | }); 17 | -------------------------------------------------------------------------------- /apps/boilerplate-nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ds-boilerplate/nextjs-example", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "build:static": "next build && next export", 9 | "clean": "rimraf --no-glob ./out .next ./tsconfig.tsbuildinfo ./node_modules/.cache ", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "dependencies": { 14 | "@emotion/css": "^11.7.1", 15 | "@emotion/react": "^11.7.1", 16 | "@emotion/server": "^11.4.0", 17 | "@mdx-js/loader": "^2.0.0", 18 | "@mdx-js/react": "^2.0.0", 19 | "@next/mdx": "^12.0.10", 20 | "@unpublished/design-system": "0.0.1", 21 | "next": "12.0.7", 22 | "prism-react-renderer": "^1.3.1", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2" 25 | }, 26 | "devDependencies": { 27 | "@emotion/babel-plugin": "11.7.2", 28 | "@types/node": "^17.0.10", 29 | "@types/react": "17.0.38", 30 | "eslint": "8.5.0", 31 | "eslint-config-next": "12.0.7", 32 | "typescript": "4.5.4", 33 | "typescript-styled-plugin": "^0.18.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/boilerplate-nextjs/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | 3 | function MyApp({ Component, pageProps }: AppProps) { 4 | return ; 5 | } 6 | 7 | export default MyApp; 8 | -------------------------------------------------------------------------------- /apps/boilerplate-nextjs/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { cache } from '@emotion/css'; 2 | import createEmotionServer from '@emotion/server/create-instance'; 3 | import Document, { 4 | Html, 5 | Head, 6 | Main, 7 | NextScript, 8 | DocumentContext, 9 | } from 'next/document'; 10 | import * as React from 'react'; 11 | 12 | const renderStatic = async (html: string | undefined) => { 13 | if (html === undefined) { 14 | throw new Error('did you forget to return html from renderToString?'); 15 | } 16 | const { extractCritical } = createEmotionServer(cache); 17 | const { ids, css } = extractCritical(html); 18 | return { html, ids, css }; 19 | }; 20 | 21 | export default class AppDocument extends Document { 22 | static async getInitialProps(ctx: DocumentContext) { 23 | const page = await ctx.renderPage(); 24 | const { css, ids } = await renderStatic(page.html); 25 | const initialProps = await Document.getInitialProps(ctx); 26 | 27 | return { 28 | ...initialProps, 29 | styles: ( 30 | 31 | {initialProps.styles} 32 |