HelioKit
A component library that refuses to be a dependency: the CLI copies the source into your repository and then gets out of the way.

What it is
HelioKit is a set of animated React components and the command-line tool that installs them. Running one command copies a component's source files into the project that asked for it. Nothing is imported from a package afterwards, and no HelioKit dependency is left in the tree.
Component libraries normally ask for a trade. You get polished components, and in exchange the markup belongs to the package: changing a detail means finding whichever theming escape hatch the author decided to ship, and a version bump can move something nobody on your team touched. HelioKit takes the other side of that trade. From the first render the code is ordinary project code, editable and greppable and reviewable in the same pull request as everything else. What you give up is automatic upgrades.
That reframes what is actually being distributed. The library is not the product; the installer is. The published npm package is a CLI whose job is to copy files out of itself, and the components ride along inside it.
One repository holds both halves plus a documentation site that renders every component live. It builds to two separate outputs that never touch each other.
How it is built
One repository, two artifacts
The same repository produces a published CLI and a documentation website, and the two builds are kept apart on purpose. The CLI is bundled by tsup into an ESM file that the package manifest points its binary at. The site is built by Vite into a separate directory so it can never collide with the CLI bundle sitting next to it.
This arrangement is what makes the copy-in model work at all. The components live in the repository as ordinary source files, the site imports them directly to render live previews, and the published package carries those same files so the CLI has something to copy. There is no build step that turns a component into a distributable, because a component never becomes one.
What the CLI does
Five commands, built on Commander. Two of them list what is available, one sets a project up, one adds components, and one adds a full page template.
Adding a component is deliberately unclever: work out the destination directory, then copy the component's folder there. Components are self-contained, so there is no dependency graph to walk and nothing to rewrite on the way in. A slug that does not exist prints an error and the run continues with the rest rather than aborting the batch.
Deciding where files go, once
The tool does not try to infer the whole shape of an unfamiliar repository. It asks: Vite, Next.js, or a custom path. Guessing wrong here is not cosmetic, since it means files land in a directory the project does not use, with imports that never resolve, so the question is asked outright rather than answered by heuristics.
There is exactly one inference. On Next.js it checks whether a source directory exists and picks the nested or root component path accordingly, printing a warning when it falls back to the root.
The answer is then offered up for saving into a small config file at the project root. Every later command reads that file and skips the prompt entirely, so the interrogation happens once per project rather than once per component.
The setup command
Components assume Tailwind v4, framer-motion and lucide-react. Rather than documenting that and hoping, there is a command that installs them, with the Tailwind integration package chosen by framework: the Vite plugin for Vite, the PostCSS plugin plus PostCSS itself for Next.
It then wires the build. On Vite it reads the config, prepends the plugin import and injects the call into the plugins array, but only when the plugin is not already referenced, so re-running it does not stack duplicates. On Next it writes a PostCSS config when one is missing.
Finally it makes sure the Tailwind import is present in the stylesheet entry, creating that file if it does not exist, appending the line if the file exists without it, and saying so and doing nothing if it is already there. Each of the three outcomes prints a different message, because a setup step that is silent about what it changed in someone else's repository is worse than one that does nothing.
Templates, and the one place dependencies are tracked
Beyond single components there are full page templates. A manifest maps each template name to its source file and to the list of component slugs that page imports.
Copying a template therefore copies the page and then the components it needs, so the result compiles without a second command and without the user having to read the imports to find out what is missing. This manifest is the only place in the project where one thing declares that it depends on another, and it exists precisely because a template is the only thing here that is not self-contained.
Components that stay portable
Two dozen components live in the repository, each in its own folder with a demo file beside it that the documentation preview renders.
Portability is a constraint, not an aspiration. Beyond React the only imports permitted are framer-motion and lucide-react, and colours come from theme tokens rather than literals, which is what allows a component to be dropped into a repository nobody planned for and still look correct. The heavier visual pieces skip the motion library entirely and drive a canvas from a requestAnimationFrame loop with typed, documented props for the things worth tuning.
Two themes from three variables
The whole design system is three colour tokens and a handful of font tokens declared in the Tailwind v4 theme block. Light mode is a single class that redefines those three colours. There is no dark variant anywhere in any component, because a component that reads a background token is already correct in both themes and one that hardcodes a colour cannot be fixed by adding variants to it.
The background colour is also painted on the document itself rather than only on component wrappers. Anything the application has not painted yet, an overscroll bounce or a gap while a lazy route loads, shows the browser canvas underneath, and that canvas is white unless the document says otherwise.
The documentation site
Every component has a data module holding its live preview, its source, its installation steps and its props table. A slug map turns those into dynamic imports, so opening one component's page loads that component's payload and nothing else. A small Redux slice holds whichever component is currently being viewed and backs the preview, the code panel and the props table from one place.
The manual install snippet is not a copy of the component pasted into a string. It imports the component file raw, so the code shown on the page is the file on disk. Documentation that is a transcription of the source drifts from it the first time the source changes; documentation that is the source cannot.
Prerendering, and what is deliberately not prerendered
The site is prerendered to static HTML, and the routes are lazily loaded per page so the initial download does not carry the syntax highlighter, the motion library and every canvas component along with the landing page.
Only the static routes are emitted as HTML. The parameterised ones, individual component pages and template previews, stay client-rendered, because those are the pages a crawler does not need and a reader reaches by navigating rather than by landing.
Head tags come from one component fed by a single site config, so canonical URLs, social cards and structured data are all derived from one origin string and are baked into the prerendered HTML rather than applied after hydration. Full page template previews get their own route outside the site layout, with no navigation bar, so a template can be framed in the docs or opened standalone and fill the viewport as its own page.
What it does
- One-command install: Name a component and its source files are copied into the project, with no package left behind.
- Project setup: A single command installs Tailwind v4, framer-motion and lucide-react, then wires the build config.
- Two dozen components: Accordions, calendars, pricing tables, card stacks, canvas backgrounds and text effects.
- Full page templates: Landing pages, a dashboard and a board, which pull in the components they import automatically.
- Remembered destination: The install path is asked once and stored at the project root, so later commands run without prompting.
- Two themes, three variables: Light and dark come from redefining three colour tokens, with no dark variants in any component.
- Live documentation: Every component has a rendered preview, its real source, install steps and a props table.
- Prerendered site: Static routes ship as HTML with canonical, social and structured data already in the head.
- Published and open: On npm under the MIT licence, with the components readable in the repository.
Technical decisions
Copy the source in, do not ship a runtime package
- Chose
- Files written into the consumer's repository.
- Why
- Any line can be changed without a wrapper component or a theme override, and the change shows up in normal code review. The cost is losing automatic upgrades, which is accepted, because an unrequested change to a component's markup is usually the problem rather than the fix.
Ask which framework it is
- Chose
- A prompt on first run, saved to a config file.
- Why
- Detection that is right most of the time is worse than a question here, because the failure mode is files in a directory the project does not use with imports that never resolve. Asking once and remembering the answer costs one prompt per project, not one per command.
Keep components self-contained
- Chose
- React, framer-motion and lucide-react, nothing else.
- Why
- The installer copies a folder and stops. That only stays true while a component cannot reach outside its own directory, so the constraint on imports is what keeps the install step from needing a dependency resolver.
One repository, two build outputs
- Chose
- tsup for the CLI, Vite for the site, separate directories.
- Why
- The documentation site and the published package need the same component sources, and keeping them in one place is the only way the previews are guaranteed to be what gets installed. Separate output directories stop the two builds from overwriting each other.
Import the component source for its own documentation
- Chose
- Raw file imports instead of pasted code strings.
- Why
- A snippet copied into a template literal is correct exactly until the component changes. Importing the file means the page cannot disagree with the code it is documenting.
Theme tokens instead of dark variants
- Chose
- Three colour variables, overridden by one class.
- Why
- A component that reads a token is already correct in both themes. One that hardcodes a colour cannot be rescued by adding variants, and every variant added is a second place to forget.
Prerender the pages a crawler lands on, and only those
- Chose
- Static routes to HTML, parameterised routes left client-rendered.
- Why
- The marketing and index pages are what search results point at and what a first-time visitor sees. Individual component pages are reached by navigating, so prerendering all of them would multiply build output for pages nobody arrives at cold.
Paint the background on the document
- Chose
- The background colour set on the root elements, not just on wrappers.
- Why
- Anything the application has not painted yet shows the browser canvas, which is white. Overscroll on iOS and the gap while a lazy route loads both hit that, and a component wrapper is too far down the tree to cover either.