Typique (pronounced /ti'pik/) is a framework- and bundler-agnostic, zero-runtime CSS-in-TS library powered by a TypeScript plugin. It generates readable, unique class and variable names directly as completion items in your editor. Styles exist only as types, so they vanish cleanly from your final build.
import type { Css } from 'typique'
import { space } from './my-const'
const titleClass = 'title-1' satisfies Css<{
paddingTop: `calc(2 * ${typeof space}px)`
'& > code': {
backgroundColor: '#eee'
}
}>How it works
- The constant
titleClassfollows the configurable naming convention, which instructs Typique to provide completion items. - The value
title-1was suggested becausetitleandtitle-0are already used elsewhere in the project. typeoftogether with string interpolation lets you compose string literal types from runtime constants and other types.- Nesting works the same way you’re used to from CSS preprocessors.
- Any selector supported by CSS can be used.
- The plugin scans your TypeScript project and emits CSS file(s).
- On edits, only the changed files are re-scanned.
Typique is built to feel boring — in a good way. No new runtime model, no clever indirections, just CSS generated directly from TypeScript with minimal friction.
- No bundler hell — ever. Requires no extra bundler or framework configuration.
- Fast by design. Reuses data TypeScript already computes for your editor.
- Framework-agnostic. Works directly in TypeScript files; other file types can import styles from
.ts. - Colocation by default. Define styles anywhere TypeScript allows — top level, inside functions, even inside loops.
- Feels like real CSS. Supports natural nesting (compatible with the CSS Nesting spec) with a clean, object-based syntax.
- Zero effort SSR / RSC. Works seamlessly because it emits plain CSS, not runtime code.
- Transparent naming. Class and variable names stay readable, configurable, and visible in your source — no hidden magic.
- Easy to migrate away. The generated CSS is clean, formatted, and source-ready.
- TypeScript: 5.5 up to 6.0
TypeScript-Go (7) is not supported yet because it does not currently expose a plugins API. Typique will support it once the API becomes available. - Node.js: 18 and above
A file type is supported if it is opened by the TypeScript server and contains TypeScript syntax.
- Native support:
.ts,.tsx,.mts - Vue:
.vuefiles are supported when your IDE uses the official Vue TypeScript plugin (this is the default in VS Code).How does it work?
The Vue TypeScript plugin intercepts file-open requests and transpiles.vuefiles into plain TypeScript syntax. This allows TypeScript — and custom plugins like Typique — to operate on them as if they were regular.tsfiles. - Not supported:
.svelteand.jsfiles. Styles can still be defined in TypeScript and imported from there.
Using workspace-scoped TypeScript plugins like Typique requires a workspace TypeScript. Both the typescript and typique packages must be installed in the same node_modules. To ensure this, run the npm i / pnpm add commands from the same directory — typically the project root:
npm i -D typescript
npm i typiqueOr:
pnpm add -D typescript
pnpm add typiqueIf you use VS Code, switch to the workspace TypeScript: Command Palette → Select TypeScript Version → Use Workspace Version.
{
"compilerOptions": {
"plugins": [
{
"name": "typique/ts-plugin"
}
]
}
}How is the path typique/ts-plugin resolved?
The path (or name) is resolved from the node_modules directory where typescript is installed, regardless of the relative location of tsconfig.json. Since typique is installed in the same node_modules, the path is always the same: typique/ts-plugin.
Note: specifying a path here is only supported starting with TypeScript 5.5; earlier versions support only a pathless plugin name.
Name your constants ...Class and ...Var to instruct Typique to suggest completion items in the constant initializers. In WebStorm, you may need to invoke explicit completion (Ctrl+Space) to see the suggestions.
import type { Css, Var } from 'typique'
const sizeVar = '--size' satisfies Var
// ^
// Completion appears here
const roundButtonClass = 'round-button' satisfies Css<{
// ^
// Completion appears here
[sizeVar]: 32
borderRadius: `calc(${typeof sizeVar} / 2)`
height: `var(${typeof sizeVar})`
width: `var(${typeof sizeVar})`
}>Full naming conventions are explained further.
The suggested class names are guaranteed to be unique within the TypeScript project. The scope of this uniqueness is explained in more detail below.
By default, Typique outputs a single CSS file named typique-output.css in your project root. Import it into your HTML template or entry point:
<html>
<head>
...
<link href="./typique-output.css" rel="stylesheet">
</head>
...
</html>The file name is configurable.
Run the following command to generate the CSS file from the command line:
npx typique --projectFile ./index.ts --tsserver ./path/to/tsserver.js -- ...ts-args--projectFile(required) — any TypeScript file used to bootstrap the TypeScript project, for example your root component or application entry point.--tsserver(optional) — the path to the TypeScript server executable. Defaults to the result ofimport.meta.resolve('typescript/lib/tsserver.js')....ts-args(optional) — any valid TypeScript server command-line arguments, such as logging or global plugins.
How can I specify a custom tsconfig.json?
Unlike tsc, tsserver.js does not allow explicitly specifying a custom tsconfig.json file. Instead, it locates the configuration internally when opening the file provided via --projectFile. This is usually the first tsconfig.json found when walking up the directory hierarchy from that file.
If you need to use a custom tsconfig.json, you can apply the following workaround:
- Temporarily replace the original
tsconfig.jsonwith your custom one. - Run
npx typique. - Restore the original
tsconfig.json.
One of the core ideas of Typique as a tooling is to recognize when you are about to specify a class or CSS variable name and suggest a readable, unique name via code completion. A location where Typique expects a class or CSS variable name is called a completion context. Completion behaves slightly differently depending on the context.
All of the examples above use this kind of context. Completion is suggested when the variable name matches a configurable pattern, which by default is:
Class(es)?([Nn]ame(s)?)?$for class namesVar(s)?([Nn]ame(s)?)?$for variable names
This is useful for TSX-native frameworks such as React, Preact, SolidJS, Qwik, and others. Completion is shown in the value of a property whose name matches the configurable pattern, which by default is ^class(Name)?$:
export function Button() {
return <button className={ 'button' satisfies Css<{
// ^
// Completion appears here
border: 'none'
padding: `calc(${typeof unit}px * 2)`
}> }>
Click me
</button>
}In principle, you can manually add satisfies Css<...> or satisfies Var to any literal expression. This does not provide completion, but everything else — CSS generation and uniqueness checks — works the same way as in other contexts.
export function Button() {
return `<button class="${ 'button' satisfies Css<...> }" />`
}A context name is what Typique uses to generate class and css-var names. It is an internal identifier derived from the surrounding code.
const lgBtnClass = ''
// ^
// context name: lgBtnfunction AppTitle() {
return <h1 className={''} />
// ^
// context name: AppTitle/h1
}The context name is very close to the variable name or TSX path, yet not identical:
- For variables, it does not include the matched part of the naming convention
(for example, the
Classsuffix). - For TSX, it does not include the prop name (
className).
The context name defines which class/css-var names are suggested in this place. Actual names do not have to include the full context name. For example:
- For
lgBtn, possible names include:lg-btn,lg,btn,l-b, etc. - For
AppTitle/h1, possible names include:app-title-h1,app-h1, etc. - For any context name, it's also allowed to have a numeric suffix (counter):
-0,-1,-2etc. Typique uses it when the name is already taken elsewhere.
Finally, the naming options let you control how the context name is transformed into a class or css-var name. For example, you can add constant parts, random parts, or even exclude the context name entirely.
Typique validates that a name:
- Matches the context name and the current naming options
- Is unique within the TypeScript project
If a name is invalid, a diagnostic is shown:
Name-related diagnostics are always accompanied by appropriate quick fixes:
As a reminder, a TypeScript project consists of:
- A
tsconfig.jsonfile - The source files included by that config (often called roots)
- All files reachable via imports from roots
A single workspace can contain multiple TypeScript projects, which is common in monorepos.
Typique enforces name uniqueness at the TypeScript project level, not at the workspace level. To guarantee uniqueness across multiple TypeScript projects, you can add prefixes or suffixes via the naming options. For more advanced setups, see the Monorepos and Shared Code guide.
The next several sections describe the syntax Typique supports for defining styles. They are mostly independent and can be read in any order.
Nested rules are interpreted according to the emerging CSS Nesting Module specification. Currently, Typique downlevels nested rules into plain CSS; support for native CSS nesting is planned.
const fancyClass = 'fancy' satisfies Css<{
color: 'teal'
'@media (max-width: 600px)': {
color: 'cyan'
'&:active': {
color: 'magenta'
}
}
}>Output:
.fancy {
color: teal;
}
@media (max-width: 600px) {
.fancy {
color: cyan;
}
.fancy:active {
color: magenta;
}
}Allows defining multiple related CSS variables or class names in a single expression.
For CSS variables, make sure to add as const after the array initializer; otherwise, TypeScript will infer the type as string[]:
const [xVar, yVar] = ['--x', '--y'] as const satisfies Var
// ^
// A two-place completion appears hereFor styles, it’s possible to reference class names from the left-hand side using $0, $1, $2, and so on:
const [rootClass, largeClass, boldClass, smallClass] =
['root', 'large', 'bold', 'small'] satisfies Css<{
// ^
// A four-place completion appears here
padding: '1rem' // root
'&.$1': { // root.large
padding: '1.3rem'
'&.$2': { // root.large.bold
fontWeight: '700'
}
}
'&.$3': { // root.small
padding: '0.5rem'
'&.$2': { // root.small.bold
fontWeight: '600'
}
}
}>Typique validates that all names are referenced and that all $ references are valid.
$ references can be used to reference any identifier, not just class names. This is useful for keyframes, layers, and similar constructs.
const [buttonClass] = ['button', 'cn-1'] satisfies Css<{
// ^ ^
// Completion appears in both positions
animation: '$1 0.3s ease-in-out'
'@keyframes $1': {
from: {
opacity: 0
}
to: {
opacity: 1
}
}
}>The name 'cn-1' is derived from the configurable default context name and, like any other name, is guaranteed to be unique. If you need the generated keyframes name at runtime, you can request it on the left-hand side, as usual: const [buttonClass, fadeInKeyframes] = ....
For CSS variables, object notation is useful for defining themes:
const themeVars = {
bgColor: '--theme-bg-color',
// ^
// Completion appears here and likewise below
space: '--theme-space'
} as const satisfies VarAs with array notation, make sure to add as const after the object initializer; otherwise, TypeScript will infer the values as string.
For styles, this notation allows referencing class names via named $ references:
const buttonClasses = {
_: 'button',
// ^
// Completion appears here and likewise below
b: 'button-b',
sz: {
lg: 'button-sz-lg',
sm: 'button-sz-sm',
}
} satisfies Css<{
padding: '1rem' // associated with the first classname, 'button'
'&.$sz$lg': { // button.button-sz-lg
padding: '1.3rem'
'&.$b': { // button.button-sz-lg.button-b
fontWeight: '700'
}
}
'&.$sz$sm': { // button.button-sz-sm
padding: '0.5rem'
'&.$b': { // button.button-sz-sm.button-b
fontWeight: '600'
}
}
}>Object notation also enables selecting class names based on props in a declarative way using the co() function, explained below.
Styles that don’t contain non-object properties at the top level and don’t use $ references are emitted as-is, resulting in global CSS:
[] satisfies Css<{
body {
margin: 0
}
'.hidden': {
display: 'none'
}
'@font-face': {
fontFamily: 'Open Sans'
src: 'open-sans.woff'
}
}>Typique outputs this CSS unchanged. You can also mix local and global class names:
const flexClass = 'flex-0' satisfies Css<{
display: 'flex'
'&.hidden': {
display: 'none'
}
}>This outputs:
.flex-0 {
display: flex;
}
.flex-0.hidden {
display: none;
}Use tuple notation to assign multiple values to the same property.
const cClass = 'c' satisfies Css<{
color: ['magenta', 'oklch(0.7 0.35 328)']
}>Typique provides two utilities for combining class names: cc() and co(). Both are exported from the typique/util module.
Simply concatenates all values that are truthy. This is useful for selecting class names based on conditions, for example, prop values.
<button className={ cc(
'button' satisfies Css<{
border: 'unset'
}>,
isLarge && 'button-0' satisfies Css<{
fontSize: '1.4em'
}>,
) }/>Selects class names from a classesObject based on props. This utility is designed to work together with object notation.
<button className={ co(
{size},
{
_: 'button',
size: {
small: 'button-size-small',
large: 'button-size-large',
},
} satisfies Css<{
border: 'unset'
'.$size$small': {
fontSize: '.8em'
}
'.$size$large': {
fontSize: '1.4em'
}
}>,
) }/>Based on the value of the size variable passed in the first object, the resulting class name will be either button button-size-small or button button-size-large. See the co() TSDoc for more examples.
Here are some TypeScript recipes that come in handy with Typique.
Converts a constant to a literal type.
const unit = 4
const spacedClass = 'spaced' satisfies Css<{
padding: typeof unit // Type is 4, rendered as 4px
}>This works for both types and values.
const unit = 4
const padding = `${typeof unit}em` as const // Type is `4em`
type Padding = `${typeof unit}em` // Type is `4em`Note: the + operator produces the string and not a constant type. Make sure to always use the interpolation instead.
This is useful for assigning a value to a CSS variable.
const paddingVar = '--padding' satisfies Var
const spacedClass = 'spaced' satisfies Css<{
[paddingVar]: 4
}>This is example of the dark theme which is by default synchronized with the system theme, but also can be overridden by user settings.
import type {Css, Var} from 'typique'
const [bgColorVar, nameVar] = ['--bgColor', '--name'] as const satisfies Var
type Light<Name extends string = '🖥️'> = {
[bgColor]: '#fff'
[name]: `"${Name}"`
}
type Dark<Name extends string = '🖥️'> = {
[bgColor]: '#444'
[name]: `"${Name}"`
}
const [lightClass, darkClass] = ['light', 'dark'] satisfies Css<{
body: Light
'@media (prefers-color-scheme: dark)': {
body: Dark
}
'body.$0': Light<'☀️'>
'body.$1': Dark<'🌙'>
}>This can be used to join multiple type objects:
type NoPaddingNoMargin = {
padding: 0
margin: 0
}
const buttonClass = 'button' satisfies Css<NoPaddingNoMargin & {
// ...
}>Can be used, for example, to define specs of multiple properties at once:
[] satisfies Css<{
[_ in `@property ${typeof c0 | typeof c1}`]: {
syntax: '"<color>"'
initialValue: '#888'
inherits: false
}
}>- Why Typique exists — background and context (DEV.to)
- Demos — examples of using Typique with different frameworks, including configuring TypeScript in monorepos
- Configuration — a complete reference of plugin settings
- Monorepos and Shared Code — guidance on using Typique in monorepos and reusable libraries
- Plugin Description — details on the plugin and package architecture and performance
Depending on community feedback, the project may evolve in the following directions:
- CSS syntax highlighting and improved completion
- Names defined purely in type space, without requiring runtime values
- More flexible configuration of name prefixes and suffixes
- Refactoring tools
Typique draws inspiration from the following great libraries:


