Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b667d62
chore: πŸ€– add pkg vite-plugin-static-copy
punkbit Mar 16, 2026
5d864a9
chore: πŸ€– use vite setup to copy css to distribution
punkbit Mar 16, 2026
12ce679
refactor: πŸ’‘ make static copy process common for cjs and esm
punkbit Mar 16, 2026
7fecd14
docs: πŸ“ about css modules
punkbit Mar 16, 2026
fd5ca57
style: πŸ’„ add Button css module
punkbit Mar 16, 2026
516be07
style: πŸ’„ assign css module to component Button
punkbit Mar 16, 2026
3c2d8bd
chore: πŸ€– add class-variance-authority and clsx
punkbit Mar 16, 2026
04cb2d1
style: πŸ’„ migrate styled component to css modules
punkbit Mar 16, 2026
6e2e863
style: πŸ’„ migrate styled component to css modules
punkbit Mar 16, 2026
a295313
refactor: πŸ’‘ token generator
punkbit Mar 16, 2026
ebf3917
chore: πŸ€– storybook support for css modules
punkbit Mar 16, 2026
0c27a7f
chore: πŸ€– add button stories for each state, e.g. primary, secondary, etc
punkbit Mar 16, 2026
076d438
chore: πŸ€– add changeset
punkbit Mar 16, 2026
573659b
chore: πŸ€– remove comment
punkbit Mar 16, 2026
e2ce441
chore: πŸ€– update changeset
punkbit Mar 16, 2026
12b16ff
chore: πŸ€– declare css files as sideEffects
punkbit Mar 16, 2026
8354c30
refactor: πŸ’‘ make class utility libraries, e.g. cva; actual dependencies
punkbit Mar 16, 2026
b878349
chore: πŸ€– add TODO button
punkbit Mar 16, 2026
269b486
style: πŸ’„ possible typo, e.g. make similar bg size as others
punkbit Mar 16, 2026
85aade0
refactor: πŸ’‘ role button is redundant per the ARIA in HTML spec, add a…
punkbit Mar 16, 2026
ca41eae
chore: πŸ€– format
punkbit Mar 16, 2026
e29e9f1
refactor: πŸ’‘ generate tokens, e.g. theme config shared file as json, r…
punkbit Mar 16, 2026
118164c
chore: πŸ€– add TODO regarding removal of styled components for the futu…
punkbit Mar 16, 2026
a187687
chore: πŸ€– add migration note to changeset
punkbit Mar 16, 2026
ed54cda
refactor: πŸ’‘ forward props (delegated)
punkbit Mar 16, 2026
d388019
fix: πŸ› checkout design tokens from main
punkbit Mar 16, 2026
0f99b3f
fix: πŸ› if a consumer passes className, it would overwrite the variant…
punkbit Mar 16, 2026
f4f2c74
fix: πŸ› avoid double colmns
punkbit Mar 16, 2026
0bf905a
chore: πŸ€– regenerate tokens
punkbit Mar 16, 2026
0f1452a
fix: πŸ› replace missing by global focus token
punkbit Mar 16, 2026
bb4448d
fix: πŸ› focus rings restored for keyboard users
punkbit Mar 16, 2026
58faa75
refactor: πŸ’‘ follow BEM naming convention
punkbit Mar 16, 2026
a3a21ef
chore: πŸ€– include BEM rule in llm conventions
punkbit Mar 16, 2026
6f28a52
fix: πŸ› empty token
punkbit Mar 16, 2026
0b80bcb
fix: πŸ› .button > span selector to explicit .button__label class
punkbit Mar 16, 2026
4aeb94b
chore: πŸ€– format
punkbit Mar 16, 2026
026fd9d
fix: πŸ› claude review, missing prefers-reduced-motion support for the …
punkbit Mar 17, 2026
60931e7
fix: πŸ› add line
punkbit Mar 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/eight-colts-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@clickhouse/click-ui': minor
---

This library will now use CSS Modules for styling and because it's distributed unbundled, gives the consumer application full control over bundling and optimisations. You'll only include what you actually use, resulting in smaller bundle sizes and better performance!

**Migration:**

Your bundler must be configured to handle `.module.css` imports from `node_modules`. Most popular bundlers (Vite, webpack, Parcel, Rollup with appropriate plugins) support CSS Modules by default or with minimal configuration.

NOTE: We're currently migrating from Styled-Components to CSS Modules. Some components may still use Styled-Components during the transition period.

To learn more about CSS modules support, check our documentation [here](https://github.com/ClickHouse/click-ui?tab=readme-ov-file#css-modules)
20 changes: 20 additions & 0 deletions .llm/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ export interface ComponentProps extends React.HTMLAttributes<HTMLElement> {
- Use transient props (prefixed with `$`) for styled-component internal state
- Use `data-*` attributes for styling hooks instead of generated class names

### CSS Modules (BEM Naming)

When using CSS Modules (migration in progress from styled-components):

- **Follow BEM naming convention**:
- `.button` - Block (component root)
- `.button__icon` - Element (child of block, use double underscore)
- `.button--primary` - Modifier (variant/state, use double dash)
- `.button--primary:hover` - State pseudo-classes

- **Example structure**:
```css
.button { /* base styles */ }
.button__icon { /* icon element */ }
.button--primary { /* primary variant */ }
.button--primary:focus-visible { /* keyboard focus state */ }
```
- Use CSS custom properties from theme tokens: `var(--click-button-basic-color-primary-background-default)`
- Always include `:focus-visible` styles for keyboard accessibility, never use `outline: none` without replacement

### Accessibility (Mandatory)

- Interactive elements need `role`, `aria-label`, `aria-describedby`
Expand Down
50 changes: 46 additions & 4 deletions .scripts/js/generate-tokens.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { register } from '@tokens-studio/sd-transforms';
import StyleDictionary from 'style-dictionary';
import config from '../../src/theme/theme.config.json' with { type: 'json' };

const themes = ['dark', 'light'];
const THEME_DATA_ATTRIBUTE = `data-${config.storageKey}`;

await register(StyleDictionary);

Expand All @@ -17,6 +19,38 @@ StyleDictionary.registerTransform({
},
});

StyleDictionary.registerTransform({
type: 'name',
name: 'name/cti/kebab',
transform: (token, options) => {
if (options.prefix && options.prefix.length) {
return [options.prefix].concat(token.path).join('-');
} else {
return token.path.join('-');
}
},
});

StyleDictionary.registerFormat({
name: 'css/themed-variables',
format: function ({ dictionary, file }) {
const themeName = file.destination.replace('tokens-', '').replace('.css', '');
const tokens = dictionary.allTokens
.map(token => {
const varName = token.path.join('-');
const cleanValue = String(token.value).replace(/;+$/, '');
return ` --${varName}: ${cleanValue};`;
})
.join('\n');

if (themeName === 'light') {
return `:root,\n[${THEME_DATA_ATTRIBUTE}="light"] {\n${tokens}\n}`;
} else {
return `[${THEME_DATA_ATTRIBUTE}="dark"] {\n${tokens}\n}`;
}
},
});

StyleDictionary.registerFormat({
name: 'typescript/es6-theme',
format: function ({ dictionary, file }) {
Expand Down Expand Up @@ -46,10 +80,7 @@ StyleDictionary.registerFormat({

for (const theme of themes) {
const sd = new StyleDictionary({
source: [
`./tokens/**/!(${themes.join('|')}).json`,
`./tokens/**/${theme}.json`,
],
source: [`./tokens/**/!(${themes.join('|')}).json`, `./tokens/**/${theme}.json`],
preprocessors: ['tokens-studio'],
platforms: {
ts: {
Expand All @@ -63,6 +94,17 @@ for (const theme of themes) {
},
],
},
css: {
transformGroup: 'tokens-studio',
transforms: ['name/cti/kebab'],
buildPath: 'src/theme/styles/',
files: [
{
destination: `tokens-${theme}.css`,
format: 'css/themed-variables',
},
],
},
},
});

Expand Down
7 changes: 4 additions & 3 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ const config: StorybookConfig = {
},

async viteFinal(config, { configType }) {
// Workaround for Storybook 10.0.7 bug where MDX files generate file:// imports
// See: https://github.com/storybookjs/storybook/issues (mdx-react-shim resolution)
config.plugins = config.plugins || [];
config.plugins = (config.plugins || []).filter((plugin) => {
const pluginName = plugin && typeof plugin === 'object' && 'name' in plugin ? plugin.name : null;
return pluginName !== 'css-external';
});
config.plugins.push({
name: 'fix-storybook-mdx-shim',
resolveId(source) {
Expand Down
79 changes: 39 additions & 40 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useState, useEffect, ReactNode } from 'react';
import type { Preview } from '@storybook/react-vite';
import { Decorator } from '@storybook/react-vite';
import { styled } from 'styled-components';
import { themes } from 'storybook/theming';
import { ClickUIProvider } from '@/providers';
import { useState, useEffect, ReactNode } from "react";
import type { Preview } from "@storybook/react-vite";
import { Decorator } from "@storybook/react-vite";
import { styled } from "styled-components";
import { themes } from "storybook/theming";
import { ClickUIProvider } from "../src/providers";

const ThemeBlock = styled.div<{ $left?: boolean; $bfill?: boolean }>(
({ $left, $bfill: fill, theme }) => `
position: absolute;
top: 0.5rem;
left: ${$left || fill ? 0 : '50vw'};
left: ${$left || fill ? 0 : "50vw"};
right: 0;
height: fit-content;
bottom: 0;
Expand All @@ -22,26 +22,28 @@ const ThemeBlock = styled.div<{ $left?: boolean; $bfill?: boolean }>(

export const globalTypes = {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'system',
name: "Theme",
description: "Global theme for components",
defaultValue: "system",
toolbar: {
icon: 'circlehollow',
icon: "circlehollow",
items: [
{ value: 'system', icon: 'browser', title: 'system' },
{ value: 'dark', icon: 'moon', title: 'dark' },
{ value: 'light', icon: 'sun', title: 'light' },
{ value: "system", icon: "browser", title: "system" },
{ value: "dark", icon: "moon", title: "dark" },
{ value: "light", icon: "sun", title: "light" },
],
showName: true,
},
},
};

const getSystemTheme = (): 'dark' | 'light' => {
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const getSystemTheme = (): "dark" | "light" => {
if (typeof window !== "undefined" && window.matchMedia) {
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
return 'dark';
return "dark";
};

interface ThemeWrapperProps {
Expand All @@ -50,27 +52,24 @@ interface ThemeWrapperProps {
}

const ThemeWrapper = ({ themeSelection, children }: ThemeWrapperProps) => {
const [systemTheme, setSystemTheme] = useState<'dark' | 'light'>(getSystemTheme);
const [systemTheme, setSystemTheme] = useState<"dark" | "light">(getSystemTheme);

// Listen for system theme changes
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => {
setSystemTheme(mediaQuery.matches ? 'dark' : 'light');
setSystemTheme(mediaQuery.matches ? "dark" : "light");
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, []);

// Resolve the actual theme: handle "system" and fallback for undefined/null
const theme =
themeSelection === 'system' || !themeSelection ? systemTheme : themeSelection;
themeSelection === "system" || !themeSelection ? systemTheme : themeSelection;

return (
<ClickUIProvider
theme={theme}
config={{ tooltip: { delayDuration: 0 } }}
>
<ClickUIProvider theme={theme} config={{ tooltip: { delayDuration: 0 } }}>
<ThemeBlock $left>{children}</ThemeBlock>
</ClickUIProvider>
);
Expand All @@ -91,22 +90,22 @@ const preview: Preview = {
parameters: {
options: {
storySort: {
method: 'alphabetical',
method: "alphabetical",
order: [
'Introduction',
'Buttons',
'Cards',
'Layout',
'Forms',
'Display',
'Sidebar',
'Typography',
'Colors',
['Title', 'Text', 'Link'],
"Introduction",
"Buttons",
"Cards",
"Layout",
"Forms",
"Display",
"Sidebar",
"Typography",
"Colors",
["Title", "Text", "Link"],
],
},
},
actions: { argTypesRegex: '^on[A-Z].*' },
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ You can find the official docs for the Click UI design system and component libr
- [Generating design tokens](#generating-design-tokens)
- [Local development](#local-development)
- [Circular dependency check](#circular-dependency-check)
- [CSS Modules](#css-modules)
* [Tests](#Tests)
- [Functional tests](#functional-tests)
- [Visual regression tests](#visual-regression-tests)
Expand Down Expand Up @@ -141,6 +142,39 @@ By avoiding local preview files, we ensure that component experimentation happen

To get started with the development playground, refer to the Storybook section [here](#storybook).

### CSS Modules

This library uses [CSS Modules](https://github.com/css-modules/css-modules) for styling and is distributed unbundled, giving your application full control over bundling and optimizations. This means you only include what you actually use, resulting in smaller bundle sizes and better performance!

Most modern React frameworks support CSS Modules out of the box, including Next.js, Vite, Create React App, and TanStack Start, with no configuration required.

> [!NOTE]
> We're currently migrating from Styled-Components to CSS Modules. Some components may still use Styled-Components during this transition period.
#### Benefits

CSS Modules align naturally with component-level imports. When you import a component like `Button`, its `Button.module.css` is automatically included. If you don't use the component, neither the JavaScript, or CSS will be bundled in your application's output. Only the necessary stylesheets will be included in the output bundle.

#### Custom Build Configurations

Although most modern React setups have CSS Modules built-in, if your build tool doesn't support it by default, you'll need to configure it.

Let's assume you have an old Webpack setup. Here's an example of how that'd look like:

```js
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: { modules: true }
}
]
}
```

For other bundlers, refer to their documentation on CSS Modules configuration.

## Tests

### Functional tests
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"sideEffects": false,
"sideEffects": ["**/*.css", "**/*.module.css"],
"exports": {
".": {
"types": "./dist/types/index.d.ts",
Expand Down Expand Up @@ -410,6 +410,8 @@
"@radix-ui/react-tabs": "1.1.1",
"@radix-ui/react-toast": "1.2.2",
"@radix-ui/react-tooltip": "1.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.19",
"lodash-es": "^4.17.23",
"react-sortablejs": "^6.1.4",
Expand Down Expand Up @@ -471,6 +473,7 @@
"vite": "^7.3.0",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-externalize-deps": "^0.10.0",
"vite-plugin-static-copy": "^3.2.0",
"vite-tsconfig-paths": "^6.0.5",
"vitest": "^2.1.8",
"watch": "^1.0.2"
Expand Down
Loading
Loading