Import text files as strings in JavaScript

Import a text file as a string without calling fetch() or filesystem APIs in your application code. This is useful for HTML templates, shaders, SQL, GraphQL, Markdown, and code examples.

The best syntax depends on your runtime or build tool. Import attributes are the emerging standard; Vite and older webpack setups have their own established solutions.

These imports are for files that are part of your module graph. Use fetch() instead when the file is remote, changes independently of the build, or should only be loaded in response to user input.

Using import attributes with { type: "text" }: the modern way

The modern syntax uses a type: "text" import attribute:

import template from './template.html' with { type: 'text' }
 
console.log(typeof template) // "string"

The file is decoded as UTF-8 and exposed as the module’s default export. Its extension does not matter, so the same syntax works for .txt, .html, .sql, or even .js files that must not be executed.

Dynamic imports use an options object and return a module namespace:

const { default: template } = await import('./template.html', {
  with: { type: 'text' },
})

There is one compatibility caveat: import attributes are standard JavaScript, but the "text" type is still a Stage 3 proposal as of this revision. It is already supported by runtimes and tools including Deno, Bun, and webpack. Node supports it behind the --experimental-import-text flag. Browser support is still rolling out, so check your targets before shipping it without a build step.

Prefer this form when your runtime or bundler supports it. Unlike conventions such as ?raw, it expresses the intent in standard JavaScript syntax.

Using Vite

Vite imports a file as a string when its specifier ends in ?raw:

import template from './template.html?raw'
import shader from './shader.glsl?raw'

No Vite configuration is required. The file becomes part of the dependency graph, so editing it updates the development server and includes the new value in the next build.

?raw is a Vite convention, not JavaScript syntax. It is a good default for a Vite application today, especially when the generated code must run in browsers that do not yet support text import attributes.

Using webpack

Current webpack supports the standard import-attribute form without additional configuration:

import template from './template.html' with { type: 'text' }

Webpack also provides the asset/source module type. Use it when you want imports with particular extensions to return strings:

// webpack.config.js
export default {
  module: {
    rules: [
      {
        test: /\.(html|md|sql|txt)$/,
        type: 'asset/source',
      },
    ],
  },
}

The matching files can then be imported normally:

import template from './template.html'

For opt-in behavior similar to Vite’s, match a resource query instead:

// webpack.config.js
export default {
  module: {
    rules: [
      {
        resourceQuery: /raw/,
        type: 'asset/source',
      },
    ],
  },
}
import source from './Component.jsx?raw'

The word raw has no special meaning to webpack here; it is simply the query matched by the rule. If another rule processes the same file, exclude raw requests from that rule so the source is not transformed first:

{
  test: /\.[cm]?[jt]sx?$/,
  resourceQuery: { not: [/raw/] },
  use: 'babel-loader',
}

This built-in asset module replaces the old raw-loader package and its inline raw-loader!./file syntax.

Gatsby, for posterity

Gatsby Cloud reached end of life. Gatsby JS itself was not formally deprecated, but it moved toward maintenance and stability rather than major framework innovation. Existing Gatsby sites still need occasional webpack configuration, so here is the historical setup.

First try the standard import attribute if the webpack version in your Gatsby project supports it:

import source from './Component.jsx' with { type: 'text' }

For older Gatsby and MDX builds, add an asset/source rule in gatsby-node.js. The important part is excluding ?raw requests from the JavaScript rule added by the MDX plugin. Otherwise Babel and React Refresh may modify the source before webpack turns it into a string.

// gatsby-node.js
 
/** Adds raw text imports to Gatsby's webpack configuration */
exports.onCreateWebpackConfig = ({ actions, getConfig }) => {
  const config = getConfig()
 
  config.module.rules = config.module.rules.map(rule => {
    // This was the JavaScript rule added by the Gatsby MDX plugin
    if (String(rule.test) !== String(/\.(js|mjs|jsx)$/)) return rule
 
    return { ...rule, resourceQuery: { not: [/raw/] } }
  })
 
  config.module.rules.push({
    resourceQuery: /raw/,
    type: 'asset/source',
  })
 
  actions.replaceWebpackConfig(config)
}

You can then import the untouched source:

import source from './Component.jsx?raw'
 
<pre>
  <code>{source}</code>
</pre>

The exact JavaScript rule can differ between Gatsby and MDX plugin versions. Inspect getConfig().module.rules and adjust the match if your project does not use /\.(js|mjs|jsx)$/.