├── .gitignore ├── demo ├── public │ ├── favicon.ico │ ├── favicon-16x16.png │ └── favicon-32x32.png ├── next.config.js ├── postcss.config.js ├── pages │ ├── _app.js │ ├── dark.js │ ├── index.js │ ├── variants.js │ ├── list-items.js │ └── themes.js ├── tailwind.config.js └── components │ ├── MarkdownSampleShort.mdx │ └── MarkdownSample.mdx ├── src ├── index.d.ts ├── utils.js ├── index.js ├── index.test.js └── styles.js ├── scripts ├── release-channel.js └── release-notes.js ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── 1.bug_report.yml ├── workflows │ ├── nodejs.yml │ ├── release.yml │ ├── release-insiders.yml │ └── prepare-release.yml ├── logo-dark.svg └── logo-light.svg ├── LICENSE ├── package.json ├── jest └── customMatchers.js ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | /demo/out 4 | coverage/ 5 | -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailwindlabs/tailwindcss-typography/HEAD/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailwindlabs/tailwindcss-typography/HEAD/demo/public/favicon-16x16.png -------------------------------------------------------------------------------- /demo/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailwindlabs/tailwindcss-typography/HEAD/demo/public/favicon-32x32.png -------------------------------------------------------------------------------- /demo/next.config.js: -------------------------------------------------------------------------------- 1 | const withMDX = require('@next/mdx')() 2 | 3 | module.exports = withMDX({ 4 | pageExtensions: ['js', 'jsx', 'mdx'], 5 | }) 6 | -------------------------------------------------------------------------------- /demo/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: { config: `${__dirname}/tailwind.config.js` }, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /demo/pages/_app.js: -------------------------------------------------------------------------------- 1 | import 'tailwindcss/tailwind.css' 2 | import React from 'react' 3 | 4 | export default function App({ Component, pageProps }) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare function plugin(options?: Partial<{ className: string; target: 'modern' | 'legacy' }>): { 2 | handler: () => void 3 | } 4 | 5 | declare namespace plugin { 6 | const __isOptionsFunction: true 7 | } 8 | 9 | export = plugin 10 | -------------------------------------------------------------------------------- /scripts/release-channel.js: -------------------------------------------------------------------------------- 1 | // Given a version, figure out what the release channel is so that we can publish to the correct 2 | // channel on npm. 3 | // 4 | // E.g.: 5 | // 6 | // 1.2.3 -> latest (default) 7 | // 0.0.0-insiders.ffaa88 -> insiders 8 | // 4.1.0-alpha.4 -> alpha 9 | 10 | let version = 11 | process.argv[2] || process.env.npm_package_version || require('../package.json').version 12 | 13 | let match = /\d+\.\d+\.\d+-(.*)\.\d+/g.exec(version) 14 | if (match) { 15 | console.log(match[1]) 16 | } else { 17 | console.log('latest') 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature Request 4 | url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas 5 | about: 'Suggest any ideas you have using our discussion forums.' 6 | - name: Help 7 | url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=help 8 | about: 'If you have a question or need help, ask a question on the discussion forums.' 9 | - name: Kind Words 10 | url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=kind-words 11 | about: "Have something nice to say about @tailwindcss/typography or Tailwind CSS in general? We'd love to hear it!" 12 | -------------------------------------------------------------------------------- /demo/pages/dark.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import MarkdownSampleShort from '../components/MarkdownSampleShort.mdx' 3 | 4 | export default function Index() { 5 | return ( 6 |
7 | 8 | Tailwind CSS Typography 9 | 10 |
11 |
12 |
13 |

Are you happy now?

14 | 15 |
16 |
17 |
18 |
19 | ) 20 | } 21 | 22 | export const config = { 23 | unstable_runtimeJS: false, 24 | } 25 | -------------------------------------------------------------------------------- /scripts/release-notes.js: -------------------------------------------------------------------------------- 1 | // Given a version, figure out what the release notes are so that we can use this to pre-fill the 2 | // release notes on a GitHub release for the current version. 3 | 4 | let path = require('path') 5 | let fs = require('fs') 6 | 7 | let version = 8 | process.argv[2] || process.env.npm_package_version || require('../package.json').version 9 | 10 | let changelog = fs.readFileSync(path.resolve(__dirname, '..', 'CHANGELOG.md'), 'utf8') 11 | let match = new RegExp( 12 | `## \\[${version}\\] - (.*)\\n\\n([\\s\\S]*?)\\n(?:(?:##\\s)|(?:\\[))`, 13 | 'g' 14 | ).exec(changelog) 15 | 16 | if (match) { 17 | let [, date, notes] = match 18 | console.log(notes.trim()) 19 | } else { 20 | console.log(`Placeholder release notes for version: v${version}`) 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [20, 22] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | 28 | - name: Install dependencies 29 | run: npm install 30 | 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /demo/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const mdx = require('@mdx-js/mdx') 2 | 3 | module.exports = { 4 | content: ['./demo/pages/**/*.{js,mdx}', './demo/components/**/*.{js,mdx}'], 5 | transform: { 6 | mdx: (content) => mdx.sync(content), 7 | }, 8 | theme: { 9 | extend: { 10 | colors: { 11 | red: { 1000: '#530F0F' }, 12 | orange: { 1000: '#521C0B' }, 13 | amber: { 1000: '#54240B' }, 14 | yellow: { 1000: '#4D280A' }, 15 | lime: { 1000: '#213708' }, 16 | green: { 1000: '#0F3D23' }, 17 | emerald: { 1000: '#02392C' }, 18 | teal: { 1000: '#073937' }, 19 | cyan: { 1000: '#09364A' }, 20 | sky: { 1000: '#072F49' }, 21 | blue: { 1000: '#17275C' }, 22 | indigo: { 1000: '#1F1C53' }, 23 | violet: { 1000: '#2F1265' }, 24 | purple: { 1000: '#3D1061' }, 25 | fuchsia: { 1000: '#4C0C4F' }, 26 | pink: { 1000: '#4E0B26' }, 27 | rose: { 1000: '#4E071B' }, 28 | }, 29 | }, 30 | }, 31 | variants: {}, 32 | plugins: [require('../src/index.js')], 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | id-token: write 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [22] 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | registry-url: 'https://registry.npmjs.org' 27 | cache: 'npm' 28 | 29 | - name: Install dependencies 30 | run: npm install 31 | 32 | - name: Test 33 | run: npm test 34 | 35 | - name: Calculate environment variables 36 | run: | 37 | echo "RELEASE_CHANNEL=$(npm run release-channel --silent)" >> $GITHUB_ENV 38 | 39 | - name: Publish 40 | run: npm publish --provenance --tag ${{ env.RELEASE_CHANNEL }} 41 | env: 42 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tailwind Labs, Inc. 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 | -------------------------------------------------------------------------------- /.github/workflows/release-insiders.yml: -------------------------------------------------------------------------------- 1 | name: Release Insiders 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | permissions: 8 | contents: read 9 | id-token: write 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [22] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | registry-url: 'https://registry.npmjs.org' 27 | cache: 'npm' 28 | 29 | - name: Install dependencies 30 | run: npm install 31 | 32 | - name: Test 33 | run: npm test 34 | 35 | - name: Resolve version 36 | id: vars 37 | run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" 38 | 39 | - name: 'Version based on commit: 0.0.0-insiders.${{ steps.vars.outputs.sha_short }}' 40 | run: npm version 0.0.0-insiders.${{ steps.vars.outputs.sha_short }} --force --no-git-tag-version 41 | 42 | - name: Publish 43 | run: npm publish --provenance --tag insiders 44 | env: 45 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1.bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a bug report for @tailwindcss/typography. 3 | labels: [] 4 | body: 5 | - type: input 6 | attributes: 7 | label: What version of @tailwindcss/typography are you using? 8 | description: 'For example: v0.4.0' 9 | validations: 10 | required: true 11 | - type: input 12 | attributes: 13 | label: What version of Node.js are you using? 14 | description: 'For example: v12.0.0' 15 | validations: 16 | required: true 17 | - type: input 18 | attributes: 19 | label: What browser are you using? 20 | description: 'For example: Chrome, Safari, or N/A' 21 | validations: 22 | required: true 23 | - type: input 24 | attributes: 25 | label: What operating system are you using? 26 | description: 'For example: macOS, Windows' 27 | validations: 28 | required: true 29 | - type: input 30 | attributes: 31 | label: Reproduction repository 32 | description: A public GitHub repo that includes a minimal reproduction of the bug. Unfortunately we can't provide support without a reproduction, and your issue will be closed and locked with no comment if this is not provided. 33 | validations: 34 | required: true 35 | - type: textarea 36 | attributes: 37 | label: Describe your issue 38 | description: Describe the problem you're seeing, any important steps to reproduce and what behavior you expect instead 39 | -------------------------------------------------------------------------------- /.github/workflows/prepare-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | prepare: 14 | permissions: 15 | contents: write # for softprops/action-gh-release to create GitHub release 16 | 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | matrix: 21 | node-version: [22] 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | registry-url: 'https://registry.npmjs.org' 31 | cache: 'npm' 32 | 33 | - name: Install dependencies 34 | run: npm install 35 | 36 | - name: Test 37 | run: npm test 38 | 39 | - name: Resolve version 40 | id: vars 41 | run: | 42 | echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV 43 | 44 | - name: Get release notes 45 | run: | 46 | RELEASE_NOTES=$(npm run release-notes --silent) 47 | echo "RELEASE_NOTES<> $GITHUB_ENV 48 | echo "$RELEASE_NOTES" >> $GITHUB_ENV 49 | echo "EOF" >> $GITHUB_ENV 50 | 51 | - name: Release 52 | uses: softprops/action-gh-release@v1 53 | with: 54 | draft: true 55 | tag_name: ${{ env.TAG_NAME }} 56 | body: ${{ env.RELEASE_NOTES }} 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tailwindcss/typography", 3 | "version": "0.5.19", 4 | "description": "A Tailwind CSS plugin for automatically styling plain HTML content with beautiful typographic defaults.", 5 | "main": "src/index.js", 6 | "types": "src/index.d.ts", 7 | "files": [ 8 | "src/*.js", 9 | "src/*.d.ts", 10 | "dist/" 11 | ], 12 | "repository": "https://github.com/tailwindlabs/tailwindcss-typography", 13 | "license": "MIT", 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "prettier": { 18 | "printWidth": 100, 19 | "semi": false, 20 | "singleQuote": true, 21 | "trailingComma": "es5" 22 | }, 23 | "scripts": { 24 | "test": "jest", 25 | "dev": "next dev demo", 26 | "build": "next build demo", 27 | "export": "next export demo", 28 | "start": "next start demo", 29 | "release-channel": "node ./scripts/release-channel.js", 30 | "release-notes": "node ./scripts/release-notes.js" 31 | }, 32 | "peerDependencies": { 33 | "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" 34 | }, 35 | "devDependencies": { 36 | "@mdx-js/loader": "^1.0.19", 37 | "@mdx-js/mdx": "^1.6.6", 38 | "@next/mdx": "^8.1.0", 39 | "autoprefixer": "^10.2.1", 40 | "highlight.js": "^10.4.1", 41 | "jest": "^29.7.0", 42 | "jest-diff": "^27.3.1", 43 | "next": "^12.0.1", 44 | "postcss": "^8.2.3", 45 | "prettier": "^2.1.2", 46 | "react": "^17.0.2", 47 | "react-dom": "^17.0.2", 48 | "tailwindcss": "^3.2.2" 49 | }, 50 | "dependencies": { 51 | "postcss-selector-parser": "6.0.10" 52 | }, 53 | "jest": { 54 | "setupFilesAfterEnv": [ 55 | "/jest/customMatchers.js" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /demo/components/MarkdownSampleShort.mdx: -------------------------------------------------------------------------------- 1 |

2 | Until now, trying to style an article, document, or blog post with Tailwind has been a very 3 | tedious task. 4 |

5 | 6 | The `@tailwindcss/typography` plugin is our attempt to give you what you _actually_ want, without any of the downsides of doing something stupid like disabling our base styles. 7 | 8 | > Why is Tailwind removing the default styles on my `h1` elements? How do I disable this? What do you mean I lose all the other base styles too? 9 | 10 |
11 |

Shouldn't be colored

12 |
13 | 14 | ```html 15 |

16 | But a recent study shows that the celebrated appetizer may be linked to a series of rabies cases 17 | springing up around the country. 18 |

19 | ``` 20 | 21 | For more information about how to use the plugin and the features it includes, [read the documentation](https://github.com/tailwindcss/typography/blob/main/README.md). 22 | 23 | --- 24 | 25 | ## This is a heading 26 | 27 | 1. We want everything to look good out of the box. 28 | 2. Really just the first reason, that's the whole point of the plugin. 29 | 3. Here's a third pretend reason though a list with three items looks more realistic than a list with two items. 30 | 31 | Now **I'm going to show you** an example of an unordered list to make sure that looks good, too: 32 | 33 | - So here is the first item in this list. 34 | - In this example we're keeping the items short. 35 | - Later, we'll use longer, more complex list items. 36 | 37 | Let's even style a table: 38 | 39 | | Wrestler | Origin | Finisher | 40 | | ----------------------- | ------------ | ------------------ | 41 | | Bret "The Hitman" Hart | Calgary, AB | Sharpshooter | 42 | | Stone Cold Steve Austin | Austin, TX | Stone Cold Stunner | 43 | | Randy Savage | Sarasota, FL | Elbow Drop | 44 | | Vader | Boulder, CO | Vader Bomb | 45 | | Razor Ramon | Chuluota, FL | Razor's Edge | 46 | 47 | Finally, a figure with a caption: 48 | 49 |
50 | 54 |
55 | Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of 56 | classical Latin literature from 45 BC, making it over 2000 years old. 57 |
58 |
59 | 60 | And that's the end of our little demo. 61 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const parser = require('postcss-selector-parser') 2 | const parseSelector = parser() 3 | 4 | function isObject(value) { 5 | return typeof value === 'object' && value !== null 6 | } 7 | 8 | function isPlainObject(value) { 9 | if (typeof value !== 'object' || value === null) { 10 | return false 11 | } 12 | 13 | if (Object.prototype.toString.call(value) !== '[object Object]') { 14 | return false 15 | } 16 | 17 | if (Object.getPrototypeOf(value) === null) { 18 | return true 19 | } 20 | 21 | let proto = value 22 | while (Object.getPrototypeOf(proto) !== null) { 23 | proto = Object.getPrototypeOf(proto) 24 | } 25 | 26 | return Object.getPrototypeOf(value) === proto 27 | } 28 | 29 | function merge(target, ...sources) { 30 | if (!sources.length) return target 31 | const source = sources.shift() 32 | 33 | if (isObject(target) && isObject(source)) { 34 | for (const key in source) { 35 | if (Array.isArray(source[key])) { 36 | if (!target[key]) target[key] = [] 37 | source[key].forEach((item, index) => { 38 | if (isPlainObject(item) && isPlainObject(target[key][index])) { 39 | target[key][index] = merge(target[key][index], item) 40 | } else { 41 | target[key][index] = item 42 | } 43 | }) 44 | } else if (isPlainObject(source[key])) { 45 | if (!target[key]) target[key] = {} 46 | merge(target[key], source[key]) 47 | } else { 48 | target[key] = source[key] 49 | } 50 | } 51 | } 52 | 53 | return merge(target, ...sources) 54 | } 55 | 56 | function castArray(value) { 57 | return Array.isArray(value) ? value : [value] 58 | } 59 | 60 | module.exports = { 61 | isObject, 62 | isPlainObject, 63 | merge, 64 | castArray, 65 | isUsableColor(color, values) { 66 | return isPlainObject(values) && color !== 'gray' && values[600] 67 | }, 68 | 69 | /** 70 | * @param {string} selector 71 | */ 72 | commonTrailingPseudos(selector) { 73 | let ast = parseSelector.astSync(selector) 74 | 75 | /** @type {import('postcss-selector-parser').Pseudo[][]} */ 76 | let matrix = [] 77 | 78 | // Put the pseudo elements in reverse order in a sparse, column-major 2D array 79 | for (let [i, sel] of ast.nodes.entries()) { 80 | for (const [j, child] of [...sel.nodes].reverse().entries()) { 81 | // We only care about pseudo elements 82 | if (child.type !== 'pseudo' || !child.value.startsWith('::')) { 83 | break 84 | } 85 | 86 | matrix[j] = matrix[j] || [] 87 | matrix[j][i] = child 88 | } 89 | } 90 | 91 | let trailingPseudos = parser.selector() 92 | 93 | // At this point the pseudo elements are in a column-major 2D array 94 | // This means each row contains one "column" of pseudo elements from each selector 95 | // We can compare all the pseudo elements in a row to see if they are the same 96 | for (const pseudos of matrix) { 97 | // It's a sparse 2D array so there are going to be holes in the rows 98 | // We skip those 99 | if (!pseudos) { 100 | continue 101 | } 102 | 103 | let values = new Set(pseudos.map((p) => p.value)) 104 | 105 | // The pseudo elements are not the same 106 | if (values.size > 1) { 107 | break 108 | } 109 | 110 | pseudos.forEach((pseudo) => pseudo.remove()) 111 | trailingPseudos.prepend(pseudos[0]) 112 | } 113 | 114 | if (trailingPseudos.nodes.length) { 115 | return [trailingPseudos.toString(), ast.toString()] 116 | } 117 | 118 | return [null, selector] 119 | }, 120 | } 121 | -------------------------------------------------------------------------------- /.github/logo-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/logo-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin') 2 | const styles = require('./styles') 3 | const { commonTrailingPseudos, isObject, isPlainObject, merge, castArray } = require('./utils') 4 | 5 | const computed = { 6 | // Reserved for future "magic properties", for example: 7 | // bulletColor: (color) => ({ 'ul > li::before': { backgroundColor: color } }), 8 | } 9 | 10 | function inWhere(selector, { className, modifier, prefix }) { 11 | let prefixedNot = prefix(`.not-${className}`).slice(1) 12 | let selectorPrefix = selector.startsWith('>') 13 | ? `${modifier === 'DEFAULT' ? `.${className}` : `.${className}-${modifier}`} ` 14 | : '' 15 | 16 | // Parse the selector, if every component ends in the same pseudo element(s) then move it to the end 17 | let [trailingPseudo, rebuiltSelector] = commonTrailingPseudos(selector) 18 | 19 | if (trailingPseudo) { 20 | return `:where(${selectorPrefix}${rebuiltSelector}):not(:where([class~="${prefixedNot}"],[class~="${prefixedNot}"] *))${trailingPseudo}` 21 | } 22 | 23 | return `:where(${selectorPrefix}${selector}):not(:where([class~="${prefixedNot}"],[class~="${prefixedNot}"] *))` 24 | } 25 | 26 | function configToCss(config = {}, { target, className, modifier, prefix }) { 27 | function updateSelector(k, v) { 28 | if (target === 'legacy') { 29 | return [k, v] 30 | } 31 | 32 | if (Array.isArray(v)) { 33 | return [k, v] 34 | } 35 | 36 | if (isObject(v)) { 37 | let nested = Object.values(v).some(isObject) 38 | if (nested) { 39 | return [ 40 | inWhere(k, { className, modifier, prefix }), 41 | v, 42 | Object.fromEntries(Object.entries(v).map(([k, v]) => updateSelector(k, v))), 43 | ] 44 | } 45 | 46 | return [inWhere(k, { className, modifier, prefix }), v] 47 | } 48 | 49 | return [k, v] 50 | } 51 | 52 | return Object.fromEntries( 53 | Object.entries( 54 | merge( 55 | {}, 56 | ...Object.keys(config) 57 | .filter((key) => computed[key]) 58 | .map((key) => computed[key](config[key])), 59 | ...castArray(config.css || {}) 60 | ) 61 | ).map(([k, v]) => updateSelector(k, v)) 62 | ) 63 | } 64 | 65 | module.exports = plugin.withOptions( 66 | ({ className = 'prose', target = 'modern' } = {}) => { 67 | return function ({ addVariant, addComponents, theme, prefix }) { 68 | let modifiers = theme('typography') 69 | 70 | let options = { className, prefix } 71 | 72 | for (let [name, ...selectors] of [ 73 | ['headings', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'th'], 74 | ['h1'], 75 | ['h2'], 76 | ['h3'], 77 | ['h4'], 78 | ['h5'], 79 | ['h6'], 80 | ['p'], 81 | ['a'], 82 | ['blockquote'], 83 | ['figure'], 84 | ['figcaption'], 85 | ['strong'], 86 | ['em'], 87 | ['kbd'], 88 | ['code'], 89 | ['pre'], 90 | ['ol'], 91 | ['ul'], 92 | ['li'], 93 | ['dl'], 94 | ['dt'], 95 | ['dd'], 96 | ['table'], 97 | ['thead'], 98 | ['tr'], 99 | ['th'], 100 | ['td'], 101 | ['img'], 102 | ['picture'], 103 | ['video'], 104 | ['hr'], 105 | ['lead', '[class~="lead"]'], 106 | ]) { 107 | selectors = selectors.length === 0 ? [name] : selectors 108 | 109 | let selector = 110 | target === 'legacy' ? selectors.map((selector) => `& ${selector}`) : selectors.join(', ') 111 | 112 | addVariant( 113 | `${className}-${name}`, 114 | target === 'legacy' ? selector : `& :is(${inWhere(selector, options)})` 115 | ) 116 | } 117 | 118 | addComponents( 119 | Object.keys(modifiers).map((modifier) => ({ 120 | [modifier === 'DEFAULT' ? `.${className}` : `.${className}-${modifier}`]: configToCss( 121 | modifiers[modifier], 122 | { 123 | target, 124 | className, 125 | modifier, 126 | prefix, 127 | } 128 | ), 129 | })) 130 | ) 131 | } 132 | }, 133 | () => { 134 | return { 135 | theme: { typography: styles }, 136 | } 137 | } 138 | ) 139 | -------------------------------------------------------------------------------- /jest/customMatchers.js: -------------------------------------------------------------------------------- 1 | const prettier = require('prettier') 2 | const { diff } = require('jest-diff') 3 | 4 | function format(input) { 5 | return prettier.format(input, { 6 | parser: 'css', 7 | printWidth: 100, 8 | }) 9 | } 10 | 11 | expect.extend({ 12 | // Compare two CSS strings with all whitespace removed 13 | // This is probably naive but it's fast and works well enough. 14 | toMatchCss(received, argument) { 15 | function stripped(str) { 16 | return str.replace(/\s/g, '').replace(/;/g, '') 17 | } 18 | 19 | const options = { 20 | comment: 'stripped(received) === stripped(argument)', 21 | isNot: this.isNot, 22 | promise: this.promise, 23 | } 24 | 25 | const pass = stripped(received) === stripped(argument) 26 | 27 | const message = pass 28 | ? () => { 29 | return ( 30 | this.utils.matcherHint('toMatchCss', undefined, undefined, options) + 31 | '\n\n' + 32 | `Expected: not ${this.utils.printExpected(format(received))}\n` + 33 | `Received: ${this.utils.printReceived(format(argument))}` 34 | ) 35 | } 36 | : () => { 37 | const actual = format(received) 38 | const expected = format(argument) 39 | 40 | const diffString = diff(expected, actual, { 41 | expand: this.expand, 42 | }) 43 | 44 | return ( 45 | this.utils.matcherHint('toMatchCss', undefined, undefined, options) + 46 | '\n\n' + 47 | (diffString && diffString.includes('- Expect') 48 | ? `Difference:\n\n${diffString}` 49 | : `Expected: ${this.utils.printExpected(expected)}\n` + 50 | `Received: ${this.utils.printReceived(actual)}`) 51 | ) 52 | } 53 | 54 | return { actual: received, message, pass } 55 | }, 56 | toIncludeCss(received, argument) { 57 | const options = { 58 | comment: 'stripped(received).includes(stripped(argument))', 59 | isNot: this.isNot, 60 | promise: this.promise, 61 | } 62 | 63 | const actual = format(received) 64 | const expected = format(argument) 65 | 66 | const pass = actual.includes(expected) 67 | 68 | const message = pass 69 | ? () => { 70 | return ( 71 | this.utils.matcherHint('toIncludeCss', undefined, undefined, options) + 72 | '\n\n' + 73 | `Expected: not ${this.utils.printExpected(format(received))}\n` + 74 | `Received: ${this.utils.printReceived(format(argument))}` 75 | ) 76 | } 77 | : () => { 78 | const diffString = diff(expected, actual, { 79 | expand: this.expand, 80 | }) 81 | 82 | return ( 83 | this.utils.matcherHint('toIncludeCss', undefined, undefined, options) + 84 | '\n\n' + 85 | (diffString && diffString.includes('- Expect') 86 | ? `Difference:\n\n${diffString}` 87 | : `Expected: ${this.utils.printExpected(expected)}\n` + 88 | `Received: ${this.utils.printReceived(actual)}`) 89 | ) 90 | } 91 | 92 | return { actual: received, message, pass } 93 | }, 94 | }) 95 | 96 | expect.extend({ 97 | // Compare two CSS strings with all whitespace removed 98 | // This is probably naive but it's fast and works well enough. 99 | toMatchFormattedCss(received, argument) { 100 | function format(input) { 101 | return prettier.format(input.replace(/\n/g, ''), { 102 | parser: 'css', 103 | printWidth: 100, 104 | }) 105 | } 106 | const options = { 107 | comment: 'stripped(received) === stripped(argument)', 108 | isNot: this.isNot, 109 | promise: this.promise, 110 | } 111 | 112 | let formattedReceived = format(received) 113 | let formattedArgument = format(argument) 114 | 115 | const pass = formattedReceived === formattedArgument 116 | 117 | const message = pass 118 | ? () => { 119 | return ( 120 | this.utils.matcherHint('toMatchCss', undefined, undefined, options) + 121 | '\n\n' + 122 | `Expected: not ${this.utils.printExpected(formattedReceived)}\n` + 123 | `Received: ${this.utils.printReceived(formattedArgument)}` 124 | ) 125 | } 126 | : () => { 127 | const actual = formattedReceived 128 | const expected = formattedArgument 129 | 130 | const diffString = diff(expected, actual, { 131 | expand: this.expand, 132 | }) 133 | 134 | return ( 135 | this.utils.matcherHint('toMatchCss', undefined, undefined, options) + 136 | '\n\n' + 137 | (diffString && diffString.includes('- Expect') 138 | ? `Difference:\n\n${diffString}` 139 | : `Expected: ${this.utils.printExpected(expected)}\n` + 140 | `Received: ${this.utils.printReceived(actual)}`) 141 | ) 142 | } 143 | 144 | return { actual: received, message, pass } 145 | }, 146 | }) 147 | -------------------------------------------------------------------------------- /demo/pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import MarkdownSample from '../components/MarkdownSample.mdx' 3 | 4 | export default function Index() { 5 | return ( 6 |
7 | 8 | Tailwind CSS Typography 9 | 10 |
11 | 53 |
54 |
55 | ) 56 | } 57 | 58 | export const config = { 59 | unstable_runtimeJS: false, 60 | } 61 | -------------------------------------------------------------------------------- /demo/pages/variants.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import MarkdownSampleShort from '../components/MarkdownSampleShort.mdx' 3 | 4 | export default function Variants() { 5 | return ( 6 |
7 | 8 | Tailwind CSS Typography 9 | 10 |
11 | 61 |
62 |
63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /demo/pages/list-items.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import MarkdownSampleShort from '../components/MarkdownSampleShort.mdx' 3 | 4 | export default function Index() { 5 | return ( 6 |
7 | 8 | Tailwind CSS Typography 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |

prose-sm

23 |

It's important to cover all of these use cases for a few reasons:

24 |
    25 |
  1. We want everything to look good out of the box.
  2. 26 |
  3. Really just the first reason, that's the whole point of the plugin.
  4. 27 |
  5. 28 | Here's a third pretend reason though a list with three items looks more realistic than 29 | a list with two items. 30 |
  6. 31 |
  7. We want everything to look good out of the box.
  8. 32 |
  9. Really just the first reason, that's the whole point of the plugin.
  10. 33 |
34 |

Now we're going to try out another header style.

35 |
    36 |
  • So here is the first item in this list.
  • 37 |
  • In this example we're keeping the items short.
  • 38 |
  • Later, we'll use longer, more complex list items.
  • 39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |

prose-base

55 |

It's important to cover all of these use cases for a few reasons:

56 |
    57 |
  1. We want everything to look good out of the box.
  2. 58 |
  3. Really just the first reason, that's the whole point of the plugin.
  4. 59 |
  5. 60 | Here's a third pretend reason though a list with three items looks more realistic than 61 | a list with two items. 62 |
  6. 63 |
  7. We want everything to look good out of the box.
  8. 64 |
  9. Really just the first reason, that's the whole point of the plugin.
  10. 65 |
66 |

Now we're going to try out another header style.

67 |
    68 |
  • So here is the first item in this list.
  • 69 |
  • In this example we're keeping the items short.
  • 70 |
  • Later, we'll use longer, more complex list items.
  • 71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |

prose-lg

87 |

It's important to cover all of these use cases for a few reasons:

88 |
    89 |
  1. We want everything to look good out of the box.
  2. 90 |
  3. Really just the first reason, that's the whole point of the plugin.
  4. 91 |
  5. 92 | Here's a third pretend reason though a list with three items looks more realistic than 93 | a list with two items. 94 |
  6. 95 |
  7. We want everything to look good out of the box.
  8. 96 |
  9. Really just the first reason, that's the whole point of the plugin.
  10. 97 |
98 |

Now we're going to try out another header style.

99 |
    100 |
  • So here is the first item in this list.
  • 101 |
  • In this example we're keeping the items short.
  • 102 |
  • Later, we'll use longer, more complex list items.
  • 103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |

prose-xl

119 |

It's important to cover all of these use cases for a few reasons:

120 |
    121 |
  1. We want everything to look good out of the box.
  2. 122 |
  3. Really just the first reason, that's the whole point of the plugin.
  4. 123 |
  5. 124 | Here's a third pretend reason though a list with three items looks more realistic than 125 | a list with two items. 126 |
  6. 127 |
  7. We want everything to look good out of the box.
  8. 128 |
  9. Really just the first reason, that's the whole point of the plugin.
  10. 129 |
130 |

Now we're going to try out another header style.

131 |
    132 |
  • So here is the first item in this list.
  • 133 |
  • In this example we're keeping the items short.
  • 134 |
  • Later, we'll use longer, more complex list items.
  • 135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |

prose-2xl

151 |

It's important to cover all of these use cases for a few reasons:

152 |
    153 |
  1. We want everything to look good out of the box.
  2. 154 |
  3. Really just the first reason, that's the whole point of the plugin.
  4. 155 |
  5. 156 | Here's a third pretend reason though a list with three items looks more realistic than 157 | a list with two items. 158 |
  6. 159 |
  7. We want everything to look good out of the box.
  8. 160 |
  9. Really just the first reason, that's the whole point of the plugin.
  10. 161 |
162 |

Now we're going to try out another header style.

163 |
    164 |
  • So here is the first item in this list.
  • 165 |
  • In this example we're keeping the items short.
  • 166 |
  • Later, we'll use longer, more complex list items.
  • 167 |
168 |
169 |
170 |
171 | ) 172 | } 173 | 174 | export const config = { 175 | unstable_runtimeJS: false, 176 | } 177 | -------------------------------------------------------------------------------- /demo/pages/themes.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import MarkdownSampleShort from '../components/MarkdownSampleShort.mdx' 3 | 4 | export default function Index() { 5 | return ( 6 |
7 | 8 | Tailwind CSS Typography 9 | 10 |
11 |
12 |
13 |

Slate

14 | 15 |
16 |
17 |
18 |
19 |

Slate

20 | 21 |
22 |
23 |
24 |
25 |

Gray

26 | 27 |
28 |
29 |
30 |
31 |

Gray

32 | 33 |
34 |
35 |
36 |
37 |

Zinc

38 | 39 |
40 |
41 |
42 |
43 |

Zinc

44 | 45 |
46 |
47 |
48 |
49 |

Neutral

50 | 51 |
52 |
53 |
54 |
55 |

Neutral

56 | 57 |
58 |
59 |
60 |
61 |

Stone

62 | 63 |
64 |
65 |
66 |
67 |

Stone

68 | 69 |
70 |
71 |
72 |
73 |

Red Links

74 | 75 |
76 |
77 |
78 |
79 |

Red Links

80 | 81 |
82 |
83 |
84 |
85 |

Orange Links

86 | 87 |
88 |
89 |
90 |
91 |

Orange Links

92 | 93 |
94 |
95 |
96 |
97 |

Amber Links

98 | 99 |
100 |
101 |
102 |
103 |

Amber Links

104 | 105 |
106 |
107 |
108 |
109 |

Yellow Links

110 | 111 |
112 |
113 |
114 |
115 |

Yellow Links

116 | 117 |
118 |
119 |
120 |
121 |

Lime Links

122 | 123 |
124 |
125 |
126 |
127 |

Lime Links

128 | 129 |
130 |
131 |
132 |
133 |

Green Links

134 | 135 |
136 |
137 |
138 |
139 |

Green Links

140 | 141 |
142 |
143 |
144 |
145 |

Emerald Links

146 | 147 |
148 |
149 |
150 |
151 |

Emerald Links

152 | 153 |
154 |
155 |
156 |
157 |

Teal Links

158 | 159 |
160 |
161 |
162 |
163 |

Teal Links

164 | 165 |
166 |
167 |
168 |
169 |

Cyan Links

170 | 171 |
172 |
173 |
174 |
175 |

Cyan Links

176 | 177 |
178 |
179 |
180 |
181 |

Sky Links

182 | 183 |
184 |
185 |
186 |
187 |

Sky Links

188 | 189 |
190 |
191 |
192 |
193 |

Blue Links

194 | 195 |
196 |
197 |
198 |
199 |

Blue Links

200 | 201 |
202 |
203 |
204 |
205 |

Indigo Links

206 | 207 |
208 |
209 |
210 |
211 |

Indigo Links

212 | 213 |
214 |
215 |
216 |
217 |

Violet Links

218 | 219 |
220 |
221 |
222 |
223 |

Violet Links

224 | 225 |
226 |
227 |
228 |
229 |

Purple Links

230 | 231 |
232 |
233 |
234 |
235 |

Purple Links

236 | 237 |
238 |
239 |
240 |
241 |

Fuchsia Links

242 | 243 |
244 |
245 |
246 |
247 |

Fuchsia Links

248 | 249 |
250 |
251 |
252 |
253 |

Pink Links

254 | 255 |
256 |
257 |
258 |
259 |

Pink Links

260 | 261 |
262 |
263 |
264 |
265 |

Rose Links

266 | 267 |
268 |
269 |
270 |
271 |

Rose Links

272 | 273 |
274 |
275 |
276 |
277 | ) 278 | } 279 | 280 | export const config = { 281 | unstable_runtimeJS: false, 282 | } 283 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | - Nothing yet! 11 | 12 | ## [0.5.19] - 2025-09-24 13 | 14 | ### Fixed 15 | 16 | - Fixed broken color styles ([#405](https://github.com/tailwindlabs/tailwindcss-typography/pull/405)) 17 | 18 | ## [0.5.18] - 2025-09-19 19 | 20 | ### Fixed 21 | 22 | - Fixed undefined variable error ([#403](https://github.com/tailwindlabs/tailwindcss-typography/pull/403)) 23 | 24 | ## [0.5.17] - 2025-09-19 25 | 26 | ### Added 27 | 28 | - Add modifiers for description list elements ([#357](https://github.com/tailwindlabs/tailwindcss-typography/pull/357)) 29 | - Add `prose-picture` modifier ([#367](https://github.com/tailwindlabs/tailwindcss-typography/pull/367)) 30 | 31 | ### Fixed 32 | 33 | - Include unit in `hr` border-width value ([#379](https://github.com/tailwindlabs/tailwindcss-typography/pull/379)) 34 | - Ensure `` styles work with Tailwind CSS v4 ([#387](https://github.com/tailwindlabs/tailwindcss-typography/pull/387)) 35 | 36 | ### Changed 37 | 38 | - Remove lodash dependencies ([#402](https://github.com/tailwindlabs/tailwindcss-typography/pull/402)) 39 | 40 | ## [0.5.16] - 2025-01-07 41 | 42 | ### Fixed 43 | 44 | - Support installing with beta versions of Tailwind CSS v4 ([#365](https://github.com/tailwindlabs/tailwindcss-typography/pull/365)) 45 | 46 | ## [0.5.15] - 2024-08-28 47 | 48 | ### Fixed 49 | 50 | - Support installing with alpha versions of Tailwind CSS v4 ([#358](https://github.com/tailwindlabs/tailwindcss-typography/pull/358)) 51 | 52 | ## [0.5.14] - 2024-08-07 53 | 54 | ### Fixed 55 | 56 | - Fix table text alignment ([#346](https://github.com/tailwindlabs/tailwindcss-typography/pull/346)) 57 | 58 | ## [0.5.13] - 2024-04-26 59 | 60 | ### Fixed 61 | 62 | - Don't apply margins to `
` elements contained in an `
  • ` in FF ([#350](https://github.com/tailwindlabs/tailwindcss-typography/pull/350)) 63 | 64 | ## [0.5.12] - 2024-03-27 65 | 66 | ### Added 67 | 68 | - Use logical properties for better RTL support ([#323](https://github.com/tailwindlabs/tailwindcss-typography/pull/323)) 69 | 70 | ## [0.5.11] - 2024-03-26 71 | 72 | ### Added 73 | 74 | - Add `prose-kbd` modifier ([#340](https://github.com/tailwindlabs/tailwindcss-typography/pull/340)) 75 | 76 | ### Fixed 77 | 78 | - Fix space between `
    ` and `