quicklook-kit
quicklook-kit
Build Quick Look previews for macOS and Windows with Vite-based web frameworks, no Swift or C# required
Your preview is a web page. quicklook-kit builds it with your framework and wraps it in a small native host: a Quick Look extension on macOS, a QuickLook plugin on Windows. You never touch Swift, C# or Xcode.
Built-in support for SvelteKit, Astro, SolidStart, Nuxt and Analog. Any other framework that builds with Vite and writes a static index.html works through an integration.
Requirements
- Node.js 22.17 or later, which runs the
quicklookCLI - For macOS: macOS 14 or later, with Xcode and XcodeGen installed
- For Windows: the .NET SDK, on any OS. Testing the plugin needs Windows with QuickLook and the Microsoft Edge WebView2 Runtime, which ships with Windows 11
Installation
npm install --save-dev quicklook-kit
To start from scratch, run pnpm create quicklook instead.
Setup
1. Configure
Create quicklook.config.ts in your project root:
import { defineConfig } from 'quicklook-kit';
export default defineConfig({
name: 'Quick Look Example',
macos: {
bundleId: 'com.example.QuickLookExample',
types: [
{
id: 'com.example.QuickLookExample.example',
description: 'Example File',
extensions: ['example'],
conformsTo: ['public.plain-text'],
},
],
},
windows: {
id: 'QuickLook.Plugin.Example',
extensions: ['example'],
},
framework: 'sveltekit',
});
Each platform group is optional, but you need at least one: leave out windows to build for macOS only, or macos for Windows only.
A new type conforms to public.plain-text for text formats, or public.data for binary ones. To preview a type macOS already knows about, use a plain string like 'public.json' in place of an object.
The config can also be quicklook.config.js, .json, .yaml or .toml, or a .quicklookrc. The keys are the same in every format.
| Key | Default | |
|---|---|---|
name |
required | App name, shown in Finder and System Settings |
macos |
none | macOS options, see below |
windows |
none | Windows options, see below |
framework |
required | 'sveltekit', 'astro', 'solid-start', 'nuxt', 'analog', or an integration |
integrations |
[] |
See Integrations |
csp |
locked down | Directives here replace the defaults of the same name |
maxFileSize |
10_000_000 |
Larger files get Quick Look's generic preview |
public |
{} |
Values the page can read at build time |
settings |
{} |
Settings the user edits in the app, see Settings |
devFile |
none | A file the dev server previews until you drop another, relative to the project |
Under macos:
| Key | Default | |
|---|---|---|
bundleId |
required | The extension gets <bundleId>.Extension |
types |
required | File types to preview |
icon |
none | Path to an .icns file for the app |
minimumVersion |
'14.0' |
Minimum macOS version |
Under windows:
| Key | Default | |
|---|---|---|
id |
required | Plugin name, starting with QuickLook.Plugin., e.g. QuickLook.Plugin.Example |
extensions |
required | File extensions to preview, without the dot. They win over QuickLook's own viewers |
Upgrading from a version without platform groups? The old top-level bundleId, icon, macOS and types now live under macos, with macOS renamed to minimumVersion.
A framework can take options as a [name, options] pair, e.g. ['sveltekit', { outDir: 'dist' }] when your adapter writes somewhere other than the default.
Each framework needs a static, client-side build:
| Framework | Requirement |
|---|---|
| Analog | @analogjs/vite-plugin-angular |
| Astro | the default output: 'static' |
| Nuxt | ssr: false |
| SolidStart | ssr: false, Nitro's static preset prerendering / |
| SvelteKit | @sveltejs/adapter-static |
2. Add the Vite plugin
import { quicklook } from 'quicklook-kit/vite';
// in the framework's Vite config
plugins: [quicklook()];
For TypeScript, add the types of the virtual module, either in a .d.ts file:
/// <reference types="quicklook-kit/virtual" />
or in tsconfig.json under compilerOptions.types as "quicklook-kit/virtual".
3. Read the file
import { browserFile, config, nativeFile } from 'virtual:quicklook';
const file = nativeFile(); // the previewed file, undefined in the browser
config.public; // values from quicklook.config.*
| Field | Type | |
|---|---|---|
name |
string |
"notes.example" |
extension |
string |
"example", lowercase, no dot |
bytes |
Uint8Array |
the raw file contents |
Decode bytes however your format needs: new TextDecoder().decode(file.bytes) for text, or a parser for binary formats.
In the dev server, nativeFile() returns devFile if you set one, and undefined otherwise. Pass a dropped or picked File to browserFile() to get the same shape.
4. Add scripts
{
"scripts": {
"dev": "quicklook dev",
"build": "quicklook build",
"register": "quicklook build && quicklook register",
"unregister": "quicklook unregister"
}
}
CLI
| Command | Description |
|---|---|
quicklook dev |
Starts the framework's dev server |
quicklook build:web |
Builds the page only, into .quicklook/web |
quicklook build |
Builds every configured platform into .quicklook/build/<platform> |
quicklook register |
Installs the build on the current OS: the app on macOS, the plugin on Windows |
quicklook unregister |
Removes it again |
build takes --platform macos or --platform windows to build just one. The Windows plugin builds on any OS; the macOS app only on a Mac, so elsewhere build skips it. Run build and register again after every change.
On macOS, register copies the app to /Applications and registers its extension. Select a file in Finder and press Space. If the preview doesn't show up, check pluginkit -mAv | grep <your macos.bundleId>, and make sure no other app claims the same file type.
On Windows, register copies the plugin to %APPDATA%\pooi.moe\QuickLook\QuickLook.Plugin\<windows.id>. Quit and restart QuickLook, then select a file in Explorer and press Space.
Add .quicklook to your .gitignore.
Things to know
- Nothing can be loaded from the network. The native host serves your build from disk with a strict Content Security Policy, on both platforms. Import images, fonts and CSS normally and they get bundled. The dev server only reports violations in the console, without blocking them.
- Inline scripts are allowed by hash. Frameworks that bootstrap with inline
<script>tags keep working; the hashes are added to the CSP at build time. - Test in Safari and Edge. macOS previews run in WebKit, like Safari; Windows previews in WebView2, like Edge.
- The light/dark theme follows the system. Use
prefers-color-schemein CSS. - The macOS app is signed ad hoc. Other people have to allow it in System Settings → Privacy & Security on first launch. Distributing it without that step requires an Apple Developer account and notarization.
- Windows users install the
.qlpluginfrom.quicklook/build/windowsby previewing it in QuickLook and clicking the install button.
Releasing
Projects made with create quicklook include a release workflow. Push a tag like v1.0.0 and it builds every configured platform and attaches <App>-v1.0.0-macos.zip and <App>-v1.0.0-windows.qlplugin to a GitHub release.
If you publish a Homebrew cask, its URL needs the -macos suffix once you move to a release with platform groups.
Settings
settings declares options the user can change. On macOS, the app shows a control for each entry, picked by its type:
settings: {
theme: {
type: "string",
default: "light",
enum: [{ value: "light", description: "Light" }, "dark"],
radio: true,
},
wrap: { type: "boolean", default: true, description: "Wrap **long** lines" },
fontSize: { type: "integer", default: 13, minimum: 9, maximum: 32 },
editor: {
type: "object",
properties: { tabSize: { type: "integer", default: 2, enum: [2, 4, 8] } },
},
},
| Type | Control |
|---|---|
boolean |
Checkbox |
enum (on string, integer or number) |
Picker, or radio buttons with radio: true |
string |
Text field |
integer, number |
Number field, clamped to minimum/maximum; integer rejects fractions |
object |
A group with a heading, for nested properties |
Every entry except object needs a default. title defaults to the key in words (fontSize becomes "Font Size"), description is inline Markdown, and order sorts entries. enum entries are plain values or { value, description }. A schema with an unknown type or field, a field on the wrong type, a key containing ., or a default that doesn't fit its type, enum or bounds fails the build with the key path, e.g. settings.editor.tabSize: default 0 is below minimum 1.
The page reads the values, nested like the schema, with settings():
import { settings } from 'virtual:quicklook';
const { wrap, editor } = settings();
In the dev server, settings() returns the defaults.
Windows has no settings window. Users set values in %APPDATA%\pooi.moe\QuickLook\<windows.id>.config, with nested keys joined by dots, and restart QuickLook. Values that don't fit the schema fall back to the default.
<?xml version="1.0" encoding="utf-8"?>
<Settings>
<theme>dark</theme>
<editor.tabSize>4</editor.tabSize>
</Settings>
Integrations
An integration is an object with a name and any of these hooks. They run in order: the framework first, then each entry in integrations.
import type { Integration } from 'quicklook-kit';
export default (options: { key: string }): Integration => ({
name: 'my-integration',
hooks: {
// Adjust the resolved config.
'config:resolved'(config) {},
// Post-process the framework's static output before it's bundled.
async 'build:done'({ outDir, config }) {},
// Edit the generated XcodeGen spec: Info.plist keys, entitlements, targets. macOS only.
'xcode:spec'(spec, config) {
spec.targets.QuickLookExampleExtension.info.properties.MyKey = options.key;
},
// Add Vite plugins wherever quicklook() is used.
vite: () => [],
},
});
Add it as an object, or by name so JSON and YAML configs can use it too. A name is imported from the project: a package or a relative path, whose default export is the integration or a function returning it.
{
"integrations": ["quicklook-some-package", ["./my-integration.ts", { "key": "value" }]]
}
A framework is an integration with a build field:
framework: {
name: 'my-framework',
build: {
dev: ['vite', 'dev'],
command: ['vite', 'build'],
outDir: 'dist'
}
}
The commands run with the project's own binaries.
License
This work is licensed under The MIT License.
Related Packages
integrate zikojs elements within other ui framework like vue react solidjs svelt...
Make your website global with Worldality, a framework-agnostic internationalizat...
Flexible passkey authentication for Astro, SvelteKit, NextJS and other framework...
Consent-first analytics for the browser and server: one API in front of GA4, Met...
Version History
| Version | Released | License |
|---|---|---|
| 0.7.0 | MIT | |
| 0.6.0 | MIT | |
| 0.5.0 | MIT | |
| 0.4.0 | MIT | |
| 0.3.1 | MIT | |
| 0.3.0 | MIT | |
| 0.2.0 | MIT | |
| 0.1.1 | MIT | |
| 0.1.0 | MIT |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |