# bad-words: a small profanity filter with a tagged template twist

> bad-words is a TypeScript filter that replaces profanity in a string with a placeholder, and its most useful feature is the tagged template form that cleans interpolated values for you. The library is MIT licensed and still receiving pushes, though the published release history and the package version disagree with each other.

**nyvorin/badwords** — A javascript filter for badwords

- Repository: https://github.com/nyvorin/badwords
- Stars: 698 · Forks: 319
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/nyvorin-badwords

## A word list, a placeholder and two regular expressions

bad-words is a JavaScript filter for bad words, and it is small enough that the whole mechanism fits in a paragraph. You construct a Filter, you call clean on a string, and the profanity comes back replaced by a placeholder:

```js
import { Filter } from 'bad-words'
...
const filter = new Filter();

console.log(filter.clean("Don't be an ash0le")); //Don't be an ******
```

The word list is not embedded in the package. The single runtime dependency is badwords-list at 2.0.1-4, and the list is the thing you are actually filtering against. That separation is the most consequential design decision in the library, and it cuts both ways. On one side, you can update the list without waiting for a library release, and you can inspect exactly which words are in it before you decide to ship it. On the other side, the behaviour of your application changes when that dependency changes, so the version of the list becomes part of your own release decisions.

Two regular expressions complete the behaviour. The regex option overrides the pattern used to find the words to replace, and the replaceRegex option overrides the pattern used to decide what character of the matched word gets replaced. The README notes the second one enables multilingual support for word filtering, and gives a pattern covering Latin letters, digits and the Korean range. That is the mechanism by which a filter designed for one script can be made to work in another.

## The tagged template that cleans interpolated values

The feature that sets this library apart from a list of replacement strings is the tagged template. The recommended form is createFilter(options), which binds a tag to its own configured filter:

```js
import { createFilter } from 'bad-words'

const filter = createFilter({ placeHolder: 'x', exclude: ['hells'] })

filter`Don't be an ash0le` //Don't be an xxxxxx
filter`you ${userInput}!` //interpolated values are cleaned too
```

The point of the second line is the whole point of the feature. A template literal has literal parts and interpolated parts, and a naive sanitisation that only looks at the literal parts will pass anything the user typed straight through. This tag joins the template's literals and interpolated values, then cleans the whole string, and the README states that the output is identical to calling clean on the same text.

There is also a zero-config filter tag exported for quick use, which the documentation says is equivalent to createFilter():

```js
import { filter } from 'bad-words'

filter`Don't be an ash0le` //Don't be an ******
```

The options accepted here are the same as for the class constructor. placeHolder sets the replacement character, and exclude lists words to leave alone, which is how you keep a word that appears in a legitimate phrase from being masked. The constructor takes the same options, so a custom placeholder works either way:

```js
const customFilter = new Filter({ placeHolder: 'x' })

customFilter.clean("Don't be an ash0le") //Don't be an xxxxxx
```

For a server that renders user content into a page, the tagged template is the safer default, because it removes the possibility of forgetting to clean one interpolated fragment.

## Editing the blacklist at runtime

The list is not fixed at construction time. You can add words to the blacklist after the filter exists:

```js
const filter = new Filter()

filter.addWords('some', 'bad', 'word')

filter.clean('some bad word!') //**** *** ****!
```

The same method takes an array through the spread operator, and you can also seed the list at construction:

```js
const filter = new Filter({ list: ['some', 'bad', 'word'] })

filter.clean('some bad word!') //**** *** ****!
```

Removal works the same way, with removeWords taking a list of words and an array through the spread operator:

```js
const filter = new Filter()

filter.removeWords('hells', 'sadist')

filter.clean('some hells word!') //some hells word!
```

There is one constructor option worth understanding, because it is not obvious from the name. emptyList: true starts the filter with nothing in it, so nothing is cleaned:

```js
const filter = new Filter({ emptyList: true })
filter.clean('hell this wont clean anything') //hell this wont clean anything
```

The combination of an empty list, plus addWords, plus exclude, gives you a way to start from nothing and build exactly the list your product needs. That is arguably the intended usage for a product with its own moderation policy, and it is a better fit than the default list for a system that has decided what it does and does not want to block.

## A package that publishes on its own terms

The package manifest is where the project reveals its current shape. The name is bad-words, the version in the manifest is 4.1.5, the package is an ES module with types shipped alongside, and the exports field provides separate import and require conditions pointing at a dist/esm build and a dist CommonJS build. The build script runs two TypeScript compilations, one for CommonJS and one for ESM, and then writes the dist markers, which is the standard way to ship a dual package.

The scripts are conventional and reveal the tooling. Tests run under ava, and there is a separate test:packaging script that shells out to a script in the scripts directory, which suggests the packaging is tested rather than assumed. Documentation is generated by typedoc from src/badwords.ts into the docs directory, and there is an eslint config, a prettier config, husky, lint-staged, and standard-version for releases. A Makefile at the root wraps the three common tasks, test, build and docs, each delegating to the corresponding yarn script.

The release story is the part that needs a caveat. The recent releases listed for the repository are v1.6.2 from 2018-08-02, v1.1.0 from 2015-03-17 and 0.5.0 from 2014-07-30, while the manifest says 4.1.5. Those two facts cannot both describe the current published state, and the readme badges point at a different repository, web-mech/badwords, which suggests the project has moved or forked. The last push to the nyvorin/badwords repository was on 2026-10-03 and the repository is not archived, so work is happening, but the published version you get from the registry is worth checking against the manifest before you depend on it. The licence is MIT, copyright 2013 Michael Price, and the package is installed with yarn add bad-words.

## What a word list cannot decide for you

The first limitation is the one the library is upfront about in its shape. A word list is a policy expressed as a set of strings, and it can only match what the list contains, in the form the regexes describe. A user who writes a word with a zero for a letter, as in the example, is handled because the word is in the list, but a user who writes it in a script the list does not cover, or with a character the replaceRegex does not treat as part of a word, is not. The library gives you two knobs, the regex and the replaceRegex, and the README's Korean example shows the direction of travel for the second, but the coverage is a property of the list you configure.

The second limitation is that the replacement leaks length. The default placeholder is a run of asterisks, one per matched character, so the output tells the reader exactly how long the word was. For a profanity filter in a public forum that is often fine. In a product where the word itself is the sensitive information, or where the user is on a shared screen, a fixed placeholder is the better choice, and the placeHolder option exists for that.

The third limitation is that the filter has no notion of context. A single dictionary of words, with no surrounding information, cannot tell the difference between a word used as an insult and the same word used in a technical term or a quoted message. The exclude option handles the case where you know which specific words to leave alone, and it is a list of exceptions, not a rule.

The real alternative is a moderation service or a classifier, and the difference in approach is exactly the trade. A service or a model can judge intent, weigh context, handle multilingual input properly and give you a decision rather than a replacement, and it costs money, a network hop and an operational dependency. A local word list is free, instant, deterministic, auditable and trivially testable, and it will happily mask an innocent word while waving through a novel slur. For a small application that just needs to stop the most obvious cases, the local list is the right amount of machinery. For a product where the harm is real and the users are adversarial, the local list is a first pass at best, and the real question is what happens to the text it does not match.

## Conclusion

Adopt bad-words when you need a ready-made word list in a JavaScript or TypeScript service, and prefer the tagged template form when the text you are cleaning includes user input, since the interpolation is cleaned along with the literals. Do not adopt it as a moderation system, because it is a string replacement over a fixed list and does nothing about the surrounding policy, and do not expect it to catch obfuscation that the list and the two regexes do not describe. Before you ship it, check the word list the package pulls in, since badwords-list is a separate dependency you can update on its own schedule, and decide on the placeholder deliberately, because the default of asterisks leaks the length of the word it replaced.

## FAQ

### What is bad-words?

It is a JavaScript filter for bad words, published as the bad-words package and written in TypeScript, with the word list supplied by a separate badwords-list dependency. The Filter class exposes a clean method, and the package also exports createFilter and a zero-config filter tag for use as tagged template literals.

### How do I install and use bad-words?

Install it with yarn add bad-words, then construct a Filter and call clean on the string. The usage example imports { Filter } from 'bad-words', creates an instance, and calls filter.clean on the text, replacing the matched word with a run of asterisks.

### What does the tagged template in bad-words do?

The createFilter and filter tags join the template's literals and interpolated values and then clean the whole string, so user input passed through an interpolation is cleaned as well. The documentation states that the output is identical to calling clean on the same text.

### Can I add or remove words from the bad-words list?

Yes. addWords and removeWords accept individual words or an array passed with the spread operator, and the list can also be seeded through the list constructor option. The emptyList option starts a filter with nothing in it, so you can build a list from scratch.

### Is bad-words still maintained?

The nyvorin/badwords repository is not archived and the last push was on 2026-10-03. The listed releases are v1.6.2 from 2018-08-02, v1.1.0 from 2015 and 0.5.0 from 2014, while the package manifest carries version 4.1.5, and the readme badges point at a different repository, so check the published version before depending on it.

## Sources

- [Issues](https://github.com/nyvorin/badwords/issues)
- [License: MIT](https://github.com/nyvorin/badwords/blob/main/LICENSE)
- [nyvorin/badwords on GitHub](https://github.com/nyvorin/badwords)
- [README](https://github.com/nyvorin/badwords/blob/main/README.md)
- [Releases](https://github.com/nyvorin/badwords/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/nyvorin-badwords
