typique
v0.1.1
Published
Bundler-agnostic, zero-runtime CSS-in-TS — powered by a TypeScript plugin
Readme
Typique
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 names directly as completion items in your editor. Styles exist only as types, so they vanish cleanly from your final build.
Example
import type { Css } from 'typique'
import { space } from './my-const'
const titleClass = 'title' satisfies Css<{
fontSize: '1.3rem'
fontWeight: '300'
padding: `calc(2 * ${typeof space}px)`
'&:hover': {
fontWeight: '500'
}
}>Anatomy:
titleClassfollows the configurable naming convention: Typique auto-completes a readable yet unique class name in the initializer'title'is a class name suggested by Typiquesatisfies Css<{...}>is where you declare your styles as a type.&is the parent selector, interpreted according to the CSS nesting spec.
Why Typique
- No bundler hell — ever. Requires no extra bundler or framework configuration.
- Fast by design. Reuses data TypeScript already computes for your editor.
- Framework-agnostic. Runs natively in
.tsand.tsx; other file types (Vue, Svelte, JS) can import styles from.tsfiles. - Colocation by default. Define styles anywhere — even inside loops or functions — as long as you’re in TypeScript.
- Feels like real CSS. Supports natural nesting (compatible with the CSS Nesting spec) and clean object syntax.
- Zero effort SSR / RSC. Works seamlessly because it emits plain CSS, not runtime code.
- Transparent naming. Class and variable names are readable, customizable, and visible right in your source — no magic.
- Easy to migrate away. Generated CSS is clean, formatted, and source-ready.
Documentation
- This README — continue reading for quick setup and basic examples
- Framework Integration — how to use Typique in files which are not compiled by the TypeScript compiler
- Composing Class Names — how to configure the variables naming conventions, class names patterns, and how to make them unique across multiple independent projects
- Plugin Description — how the Typique plugin interacts with the editor, how it affects the performance
- Configuration — complete plugin parameters reference
- CLI — command-line interface reference
Setup
1. Install Typique
npm i typique2. Add the TypeScript plugin
In your tsconfig.json:
"compilerOptions": {
"plugins": [
{
"name": "typique/ts-plugin"
}
]
}If you're using VS Code, make sure to select the Workspace TypeScript version: Command Palette → Select TypeScript Version → Use Workspace Version.
3. Write some styles
Name your constants ...Class and ...Var to instruct Typique to suggest completion items in the constant initializers. Constants names are configurable.
import type { Css, Var } from 'typique'
const sizeVar = '--size' satisfies Var
const roundButtonClass = 'round-button' satisfies Css<{
[sizeVar]: 32
borderRadius: `calc(${sizeVar} / 2)`
height: `var(${sizeVar})`
width: `var(${sizeVar})`
}>As you type in the opening quote in the constants initializer, you'll see the class names suggested by Typique. In WebStorm, you might need to invoke the explicit completion (Ctrl+Space) to see the suggestions.
The suggested class names are guaranteed to be unique within a project.
4. Import the generated CSS into your app
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>You can change the output file name via the plugin configuration.
5. Add a build step
Run the following command to build the CSS file from the command line:
npx typique --projectFile ./index.ts --tsserver ./path/to/tsserver.js -- ...ts-args--projectFile ./index.ts(required) — any TypeScript (.tsor.tsx) file in your project. It’s used to bootstrap the TypeScript project and initialize the Typique plugin. Common choices are your root component or application entry point. Relative paths are resolved against the current working directory. Note: don't specify heretsconfig.json, it will likely not work. See below on specifyingtsconfig.json.--tsserver ./path/to/tsserver.js(optional) — path to the TypeScript server executable. If not set, the script invokesimport.meta.resolve('typescript/lib/tsserver.js')to discover the file....ts-args(optional) — any valid TS server command line arguments, e.g. logging verbosity and logfile.
Example
This is how it can look like for Next.JS project with verbose logging enabled:
npx typique --projectFile ./app/layout.tsx -- --logVerbosity verbose --logFile ./tsserver.logSpecifying a custom tsconfig.json
Unlike tsc, the tsserver.js unfortunately doesn't allow specifying a custom tsconfig.json file: it locates the config file internally, when it opens the file specified by --projectFile. Usually it's the first tsconfig.json file up the directory hierarchy, which includes the specified --projectFile.
If you need a custom tsconfig.json, you may use the following workaround:
- Replace the original
tsconfig.jsonwith your custom file; - Run
npx typique; - Restore the original
tsconfig.json.
More examples
You can also check examples in the tests directory.
Sharing constants between CSS and runtime
Because CSS is defined as types, the task comes down to converting constants to types. The following language features can perform this for you:
typeof operator
const unit = 4
const spacedClass = 'spaced' satisfies Css<{
padding: typeof unit // Type is 4, rendered as 4px
}>String interpolation
This works for both types and values.
const unit = 4
const padding = `${typeof unit}em` // Type is `4em`
type Padding = `${typeof unit}em` // Same, type is `4em`Note: the + operator produces the string and not a constant type. Make sure to always use interpolation instead.
Computed properties
This is useful for CSS vars explained below.
const paddingVar = '--padding' satisfies Var
const spacedClass = 'spaced' satisfies Css<{
[paddingVar]: 4
}>Arithmetic operations
TypeScript doesn't directly support arithmetic operations on types, so it's easier to use CSS calc() function:
const unit = 4
const spacedClass = 'spaced' satisfies Css<{
padding: `calc(${typeof unit}px * 2)`
}>It's planned to introduce the precalculation of calc() with only constants.
Scoped classnames, React and TSX
Styles can appear in any place in the file, not only at the top-level: Typique recognizes any ... satisfies Css<{...}> expression as a CSS declaration. These expression can appear, for example, in functions, object literals, TSX properties etc.
export function Button() {
return <button className={ 'button' satisfies Css<{
border: 'none'
padding: `calc(${typeof unit}px * 2)`
}> }>
Click me
</button>
}However, to provide classnames as completion items, Typique tries to recognize the following patterns:
- Variable initializer, with variable name(s) matching
varNameRegex, for exampleconst buttonClass = ... - TSX property, with property name(s) matching
propNameRegex, for example<button className={...} /> - Inside composeFunction parameters if a composeFunction is configured, for example
cc('...') - For CSS vars: variable initializer, with variable name(s) matching
cssVarVarNameRegex, for exampleconst bgColorVar = ...
If you define CSS in some exotic place which Typique doesn't recognize, you can proceed without the completion item. Once you complete ... satisfies Css<{...}>, Typique will validate the name and suggest the correct one in case of issues.
Nesting
The nested rules are interpreted as per the emerging CSS Nesting Module specification. Currently Typique downlevels the nested CSS rules to plain objects; the support for native nesting is planned.
const fancy = '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;
}
}Multiple classnames
Array and object notations are supported.
Array notation
const [rootClass, largeClass, boldClass, smallClass] =
['root', 'large', 'bold', 'small'] satisfies Css<{
padding: '1rem'
'&.$1': {
padding: '1.3rem'
'&.$2': {
fontWeight: '700'
}
}
'&.$3': {
padding: '0.5rem'
'&.$2': {
fontWeight: '600'
}
}
}>Typique checks that all classnames are referenced, and that all references are valid.
It's possible to also reference the root classname with $0 which can be useful in the nested levels to back-reference the root:
const largeClass = 'large' satisfies Css<{
div: {
padding: '0.4em'
'&.$0': {
fontSize: '1.3em'
}
}
}>Object notation
This notation is especially useful to define styles based on component props. More on that in React examples.
const buttonClasses = {
r: 'button-r',
b: 'button-b',
sz: {
lg: 'button-sz-lg',
sm: 'button-sz-sm',
}
} satisfies Css<{
padding: '1rem'
'&.$sz$lg': {
padding: '1.3rem'
'&.$b': {
fontWeight: '700'
}
}
'&.$sz$sm': {
padding: '0.5rem'
'&.$b': {
fontWeight: '600'
}
}
}>Root non-object properties (padding: '1rem' here) are associated with the first defined classname property (r: 'button-r'). It can be also directly referenced with .$r. Like with array notation, all references and classnames are checked.
Global CSS
Styles not containing non-object properties on the top-level and $-references are output 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 as is. You can also mix local and global classnames:
const flexClass = 'flex-0' satisfies Css<{
display: 'flex'
'&.hidden': {
display: 'none'
}
}>This outputs:
.flex-0 {
display: flex;
}
.flex-0.hidden {
display: none;
}CSS variables and theming
Typique assumes theming with CSS-variables. Similar to classes, you can declare single variables, arrays and objects of them. To make sure the type is inferred as a constant, not just string, add as const after the array or object initializer. Finally, satisfies Var signals Typique to check if it's unique among other variables also marked this way. You may think of satisfies Var as a mark of a "managed variable".
import type {Css, Var} from 'typique'
const wVar = '--w' satisfies Var
const [bgColorVar, spaceVar] = ['--bg-color', '--space'] as const satisfies Var
const themeVars = {
bgColor: '--theme-bg-color',
space: '--theme-space'
} as const satisfies Var
[] satisfies Css<{
body: {
[wVar]: '100%'
[themeVars.bgColor]: '#ffffff'
[themeVars.space]: '4px'
}
'@media (prefers-color-scheme: dark)': {
body: {
[themeVars.bgColor]: '#303030'
}
}
}>Just like classnames, completion items are shown for names which follow the configured pattern varNameRegex/cssVars, which is by default Vars?([Nn]ames?)$. There are also configs to define the generated variable name. See ComposingClassNames.
Referencing any identifier
You can use $-references to reference any identifier (not just class names). This is useful for things like keyframes and layers, which are otherwise global:
const [buttonClass,] = ['button', 'cn'] satisfies Css<{
animation: '$1 0.3s ease-in-out'
'@keyframes $1': {
from: {
opacity: 0
}
to: {
opacity: 1
}
}
}>The explicitly ignored name (comma after buttonClass) instructs Typique to suggest the 2-places completion item inside ['']. You can also bind it to a variable if you need it in the runtime — in this case the left-hand side would be const [buttonClass, fadeInKeyframes].
Fallbacks
Use tuple notation to assign multiple values to the same property.
const c = 'c' satisfies Css<{
color: ['magenta', 'oklch(0.7 0.35 328)']
}>Reusing and templating CSS rule objects
Like any other TypeScript types, CSS objects can be defined as named aliases and reused multiple times; they can also be generic. Here are some examples how to use this in common patterns.
Dark theme
import type {Css, Var} from 'typique'
declare const [bgColorVar, nameVar]: Var<['--bgColor', '--name']>
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<'🌙'>
}>Classnames refactoring (planned)
Because classnames remain constants in the source code, they may get inconsistent as the project grows. Tools for project-wide classnames refactoring are planned.
