Linkify: recognising links in text without shipping a browser detection heuristic
JavaScript plugin for finding links in plain-text and converting them to HTML <a> tags.
At a glance
- What is it?
- Linkify is a small JavaScript library that turns URLs, emails, hashtags and mentions into links, with separate interfaces for HTML output, plain strings and the browser DOM. Its core is a plugin-driven tokeniser, and its main surprise is that find() and linkifyHtml() disagree about the protocol scheme by default.
- Who is it for?
- Adopt Linkify when you need link detection over user-supplied text in Node, in a bundler pipeline or in the browser and you want the raw token positions rather than a rendered blob, since find() hands back type, value, start and end offsets that you can map onto your own renderer.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 8 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 6, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Three interfaces, and the interface you pick changes the output
Linkify ships as a set of packages rather than one, and the choice of interface is the first decision you make. The core package linkifyjs only finds things. linkify-html turns a string of plain text into HTML with anchor tags. linkify-string is the one the README tells you to use when the input has no HTML in it, which is an admission that the HTML interface is for text you generated, not for markup you are passing through.
The interface documentation also covers React, jQuery and the browser DOM, where instead of returning a string the library annotates existing elements. That is the reason for the separate interfaces at all: the core detection logic is identical, only the sink changes.
Installation in a module environment takes two packages:
npm install linkifyjs linkify-htmlThe React, jQuery and DOM variants are documented on the interfaces page rather than in the README, so if you need one of them you are already past the quick start.
find() returns offsets, and a different scheme than linkifyHtml does
The core call returns an array of token objects rather than a rewritten string. Each record carries a type, the matched value, an isLink flag, an href, and the start and end character offsets in the original string:
linkify.find('Any links to github.com here? If not, contact [email protected]');which yields a url token with value github.com, href http://github.com and offsets 13 to 23, then an email token with a mailto href and offsets 46 to 62. Those offsets are the reason to use the core directly: you can highlight, wrap or rewrite only the matched span and leave the rest of the string untouched.
Now compare that with the HTML interface, which takes a defaultProtocol option:
const options = { defaultProtocol: 'https' };
linkifyHtml('Any links to github.com here? If not, contact [email protected]', options);The returned href there is https://github.com. So with no options passed, the same bare host comes back as http from find() and as https from linkify-html. That is not a bug to file, it is a default, but it is the kind of default that turns into mixed-content warnings or a silent protocol change when the two APIs are used in the same feature. Pass the option explicitly rather than relying on either value.
There is also a test helper for validation, which accepts an optional type as the second argument:
linkify.test('github.com', 'email'); // false
linkify.test('[email protected]', 'email'); // trueURLs and emails by default, everything else is a plugin
Out of the box the library detects web URLs and email addresses, and nothing else. Mentions, hashtags and #-ticket style references are separate packages that have to be installed, which is a sensible default for a library whose job is to make URLs clickable: a chat client wants mentions, a documentation tool does not.
The plugin model is also how custom patterns are added. Rather than exposing one giant regular expression for users to edit, a link plugin declares a pattern, a class of text it matches, and how the href should be built. That is the extension point the README points at when it mentions custom link plugins and a plugin documentation page.
One dependency detail makes recognition sharper than a naive regex: the repository carries an npm script named tlds that runs tasks/update-tlds.cjs, which regenerates the list of top-level domains used for bare-host detection. Domain recognition is therefore data-driven, and the accuracy you get depends on when that list was last rebuilt. A domain that launched after your build will not be recognised as a link, which is exactly the class of bug that shows up as a support ticket rather than an exception.
React, jQuery and the DOM: annotating instead of returning
The string-returning interfaces are the easy half of the library. When you already have elements on screen, Linkify takes a different route and works on the DOM, which is what the interfaces documentation is for, with dedicated guidance for React and jQuery.
The conceptual shift is from transformation to decoration. Instead of linkifyHtml producing a new string that you inject, the DOM path finds the same tokens inside existing content and applies link behaviour to just those spans. For a React application that is the difference between dangerouslySetInnerHTML and a component that renders matched text as elements, which is why the devDependencies list includes React type packages at 19.x alongside the jQuery typings.
The tradeoff is that the DOM interfaces are documented almost entirely off the README. What the README itself gives you is the pointer, the link to the interfaces page, and the rule of thumb that plugins other than URL and email detection must be installed separately. If you are choosing between this and writing the span-wrapping yourself, the deciding question is how many match sites you need in one string: one match per element is trivial by hand, and a paragraph of mixed text is where the offsets start paying.
The build: npm workspaces, Rollup, mocha and Karma
The repository root is a monorepo. package.json holds the name link at version 4.3.3, a type of module, and scripts that fan out across workspaces with npm run build --workspaces. The actual published packages live under packages/, and the dist script runs clean, prepack, copy and copy:license in sequence, where copy gathers each workspace dist file and its licence text into a flat dist directory.
Testing is split by environment. mocha --recursive runs the Node tests with c8 for coverage, while two Karma configurations named test/ci1.conf.cjs and test/ci2.conf.cjs run the same suite in browsers with --single-run for continuous integration. That is a fuller matrix than most small utilities bother with, and it matches the README claim of 99% test coverage.
Packaging uses Rollup with Babel, commonjs, node-resolve, replace and terser plugins, and Babel is also what the README names for older browsers. The minified footprint is quoted as about 20kB, or roughly 11kB gzipped, which is the number to compare against a hand-written regex when you are deciding whether to take the dependency at all.
Node 18, ESM on main, and what IE 11 support really means
The runtime requirements are modest. The README states that Linkify is tested on Node.js 18 and up, with older versions unofficially supported, and the browser story is native support for modern browsers with Babel available for older ones.
The compatibility claim needs one clarification. The README advertises compatibility with Internet Explorer 11 and up, while the repository on the main branch is an ES module package. Those are not contradictory, but they describe different delivery routes: npm consumers get an ESM build for modern toolchains, and the browser releae download is the path for a script tag on an older page. If you need IE 11, you are consuming a published bundle, not the main branch source.
The transfer history is worth knowing for provenance. The README records that in November 2024 the project moved from the Hypercontext namespace to its primary maintainer nfrasser as Hypercontext wound down. Package ownership under nfrasser means code review and publishing sit with the same person.
Where Linkify is the wrong tool
Three cases stand out.
The first is text that already contains HTML. Linkify's job is scanning plain text; the existence of a separate linkify-string package for non-HTML input is a hint about how the HTML variant behaves. Nothing in the README describes escaping or merging behaviour for markup you did not produce, so if your input is user-submitted HTML, this is the wrong layer. Sanitise first, or write a React component that maps token offsets onto elements, which is what the React interface is for.
The second is a static, closed set of URLs. If your links come from a known list rather than from text a human typed, a regex over that list is smaller, faster and easier to audit than a tokeniser with plugins.
The third is freshness. Detection depends on the TLD list described above, so a build baked before a new domain suffix existed will silently under-detect. There is no documented runtime update path for that data in the README.
What the project does not try to be is a full Markdown or rich text parser. It finds things and hands back positions; what surrounds them is your problem.
Editorial conclusion
Adopt Linkify when you need link detection over user-supplied text in Node, in a bundler pipeline or in the browser and you want the raw token positions rather than a rendered blob, since find() hands back type, value, start and end offsets that you can map onto your own renderer. Do not adopt it when your input already contains HTML you did not generate, because the HTML interface is built for plain text and the documentation does not describe escaping behaviour for surrounding markup. Verify three things first. Decide which interface you actually need, because linkify-html emits anchors for a fragment while linkify-string is what the README tells you to use for text without HTML, and mixing them up is the single most common integration mistake. Pass defaultProtocol explicitly, since the core returns http:// for a bare host while linkifyHtml only produces https:// when you pass the option. And check the TLD list your build carries, because domain recognition depends on it and the repository regenerates it through a dedicated task rather than at install time. Start with npm install linkifyjs linkify-html, then call linkify.test('github.com', 'email') to confirm the module wiring before wiring it into a render path.
Frequently asked questions
How do I install Linkify and turn text into links?
Run npm install linkifyjs linkify-html, then call linkifyHtml(text, { defaultProtocol: 'https' }) to get a string with anchor tags. Use linkify-string instead when the input contains no HTML.
How do I detect hashtags and mentions with Linkify?
Install the separate plugin packages for @mentions, #hashtags and #-ticket patterns. By default Linkify only detects web URLs and email addresses.
What does linkify.find return?
An array of token objects, each with a type, value, isLink flag, href, and start and end offsets in the source string, so you can map matches back onto the original text.
Which Node.js versions does Linkify support?
The README states that Linkify is tested on Node.js 18 and up, with older versions unofficially supported. The package on the main branch is published as an ES module.
How do I check if a string is a valid link with Linkify?
Use linkify.test, which accepts an optional type as the second argument: linkify.test('github.com') returns true, while linkify.test('github.com', 'email') returns false.