purse-styles
v0.2.2
Published
A basic styling solution for web apps with an emphasis on simplicity and purity.
Readme
Purse
A basic styling solution for web apps with an emphasis on simplicity and purity. The goal is to build a CSS-in-JS version of Tailwind's classname composition model.
// Define styles outside your components
const truncate = style({
overflow: "hidden",
lineClamp: 1,
textOverflow: "ellipsis",
})
// Compose styles together
const actionButton = style(truncate, text[14], fonts.interface, border[1])
// Use pseudo or other complex selectors
const button = style({
"&:disabled": {
backgroundColor: indigo.indigo8,
},
"&:hover[disabled=false]": {
backgroundColor: indigo.indigo10,
},
})
// Use styles in your components dynamically
const buttonClassName = useStyles(
props.quiet ? quietButtonStyles : buttonStyles,
)
// Can also use CSS properties directly
const blueTextClassName = useStyles({
backgroundColor: "blue",
})Setup
npm i purse-stylesWrap your app in PurseProvider:
<PurseProvider>
<App />
</PurseProvider>That's all! No build step.
Variables
Define typed CSS custom properties with defineVars. Defaults are injected on :root when a PurseProvider is mounted.
import { PurseProvider, defineVars, style, useStyles } from "purse-styles"
const DARK_THEME = ':root[data-theme="dark"]'
const DARK_MEDIA = "@media (prefers-color-scheme: dark)"
export const colors = defineVars({
text: {
default: "black",
[DARK_THEME]: "white",
[DARK_MEDIA]: "white",
},
background: { default: "white", [DARK_THEME]: "black" },
accent: "blue",
})
const container = style({
color: colors.text,
backgroundColor: colors.background,
borderColor: colors.accent,
})
function App() {
const className = useStyles(container)
return <main className={className}>...</main>
}
;<PurseProvider>
<App />
</PurseProvider>Notes:
- Token names are generated (
var(--purse-<hash>-<key>)). Identical definition objects share the same names. - Keys cannot start with
--; Purse always generates the custom property name. - Conditional values use a required
defaultplus CSS at-rule or selector keys. At-rules wrap a:rootrule (for example,@media ...{:root{...}}), while selector conditions receive the declarations directly (for example,:root[data-theme="dark"]{...}). - Set
data-theme="dark"on the root<html>element to activate the selector condition above. - Defaults are root-level, and there is no dedicated
createTheme/ subtree theme API.
Exports
// Declare styles outside React component
export declare function style(
...styleElementsOrCSS: (CSSProperties | StyleElement)[]
): StyleElement
// Use declared styles or ad-hoc styles inside react component and get back a classname
export declare function useStyles(
...styleElementsOrCSS: (CSSProperties | StyleElement)[]
): string
// Inject global styles into the app, you may want to do this as an escape hatch.
// Accepts the same style object shape as style() (including nested / at-rules).
export declare function useInjectGlobalStyles(
selector: string,
cssProperties: CSSProperties,
deps: DependencyList,
): void
// Define a typed variable group; defaults are registered on :root via PurseProvider
export declare function defineVars<
const T extends Record<string, VariableDefinition>,
>(definitions: T): VariableGroup<T>
// Wrap your app in this so Purse can inject styles into the app
export declare function PurseProvider(props: {
children?: React.ReactNode
}): React.JSX.Element
// An in-memory style API for testing
export type InMemoryStyleApi = {
styleRulesRef: {
current: string[]
}
addStyleElement: StyleApi["addStyleElement"]
addGlobalStyle: StyleApi["addGlobalStyle"]
detachVariableGroups: () => void
}
export declare function createInMemoryStyleApi(): InMemoryStyleApiTODO
- [x] E2E tests with Playwright
- [ ] Consider exporting a simple
styledalternative
