mirror of
https://github.com/vercel/components.build.git
synced 2026-09-14 20:06:39 +08:00
55fae375a4
* Update Geistdocs * Update geistdocs.tsx * Run translation script * Delete toc.tsx * Update route.ts * Update route.ts
280 lines
9.1 KiB
Plaintext
280 lines
9.1 KiB
Plaintext
---
|
||
title: Tyylittely
|
||
description: Ehdollinen ja yhdisteltävä tyylitys Tailwind-luokkien avulla.
|
||
---
|
||
|
||
Nykyaikaiset komponenttikirjastot tarvitsevat joustavia tyylijärjestelmiä, jotka pystyvät käsittelemään monimutkaisia vaatimuksia tinkimättä kehittäjäkokemuksesta. Tailwind CSS:n yhdistäminen älykkääseen luokkien yhdistelyyn on muodostunut tehokkaaksi malliksi räätälöitävien komponenttien rakentamiseen.
|
||
|
||
Tämä lähestymistapa ratkaisee perustavanlaatuisen jännitteen järkevistä oletusarvoista ja täydellisestä mukautettavuudesta - haaste, joka on vaivannut komponenttikirjastoja vuosia.
|
||
|
||
## Ongelma perinteisen tyylityksen kanssa
|
||
|
||
Perinteiset CSS-lähestymistavat johtavat usein spesifisyyssotiin, tyylikonflikteihin ja ennakoimattomiin ylikirjoituksiin. Kun välität komponentille `className="bg-blue-500"`, mutta komponentissa on jo `bg-red-500`, kumpi voittaa?
|
||
|
||
Ilman asianmukaista käsittelyä molemmat luokat voivat päteä ja lopputulos riippuu monista tekijöistä – CSS:n lähdejärjestyksestä, luokkien spesifisyydestä, bundlerin luokkien yhdistelyalgoritmista jne.
|
||
|
||
## Luokkien älykäs yhdistäminen
|
||
|
||
`tailwind-merge`-kirjasto ratkaisee tämän ymmärtämällä Tailwindin luokkarakenteen ja ratkaisemalla konfliktit älykkäästi. Kun kaksi luokkaa kohdistuvat samaan CSS-ominaisuuteen, se pitää vain viimeisen.
|
||
|
||
```tsx title="Without tailwind-merge"
|
||
// Both bg-red-500 and bg-blue-500 apply - unpredictable result
|
||
<Button className="bg-blue-500" />
|
||
// Renders: className="bg-red-500 bg-blue-500"
|
||
```
|
||
|
||
```tsx title="With tailwind-merge"
|
||
import { twMerge } from 'tailwind-merge';
|
||
|
||
// bg-blue-500 wins as it comes last
|
||
const className = twMerge('bg-red-500', 'bg-blue-500');
|
||
// Returns: "bg-blue-500"
|
||
```
|
||
|
||
Tämä toimii kaikkien Tailwind-apuohjelmien kanssa:
|
||
|
||
```tsx
|
||
twMerge('px-4 py-2', 'px-8'); // Returns: "py-2 px-8"
|
||
twMerge('text-sm', 'text-lg'); // Returns: "text-lg"
|
||
twMerge('hover:bg-red-500', 'hover:bg-blue-500'); // Returns: "hover:bg-blue-500"
|
||
```
|
||
|
||
Kirjasto ymmärtää myös Tailwindin modifikaattorijärjestelmän:
|
||
|
||
```tsx
|
||
// Modifiers are handled correctly
|
||
twMerge('hover:bg-red-500 focus:bg-red-500', 'hover:bg-blue-500');
|
||
// Returns: "focus:bg-red-500 hover:bg-blue-500"
|
||
```
|
||
|
||
## Ehdolliset luokat
|
||
|
||
Usein tarvitsee soveltaa luokkia ehdollisesti propsien tai tilan perusteella. `clsx`-kirjasto tarjoaa selkeän rajapinnan tähän:
|
||
|
||
```tsx title="Using clsx"
|
||
import clsx from 'clsx';
|
||
|
||
// Basic conditionals
|
||
clsx('base', isActive && 'active');
|
||
// Returns: "base active" (if isActive is true)
|
||
|
||
// Object syntax
|
||
clsx('base', {
|
||
'active': isActive,
|
||
'disabled': isDisabled,
|
||
});
|
||
|
||
// Arrays
|
||
clsx(['base', isLarge ? 'text-lg' : 'text-sm']);
|
||
|
||
// Mixed
|
||
clsx(
|
||
'base',
|
||
['array-item'],
|
||
{ 'object-conditional': true },
|
||
isActive && 'conditional'
|
||
);
|
||
```
|
||
|
||
Yleinen käytäntö on yhdistää oletusarvoinen luokkajoukko saapuviin props-arvoihin sekä mahdolliseen omaan logiikkaamme:
|
||
|
||
```tsx title="component.tsx"
|
||
const Component = ({ className, ...props }: ComponentProps) => {
|
||
const [isOpen, setIsOpen] = useState(false);
|
||
|
||
return (
|
||
<div
|
||
className={cn(
|
||
"rounded-lg border bg-white shadow-sm",
|
||
isOpen && "bg-blue-500",
|
||
className
|
||
)}
|
||
{...props}
|
||
/>
|
||
);
|
||
};
|
||
```
|
||
|
||
## `cn`-apuohjelma
|
||
|
||
`cn`-funktio, jonka tunnetuksi teki [shadcn/ui](https://ui.shadcn.com/), yhdistää `clsx`- ja `tailwind-merge`-kirjastot tarjoten sekä ehdollisen logiikan että älykkään yhdistelyn:
|
||
|
||
```tsx title="lib/utils.ts"
|
||
import { type ClassValue, clsx } from 'clsx';
|
||
import { twMerge } from 'tailwind-merge';
|
||
|
||
export function cn(...inputs: ClassValue[]) {
|
||
return twMerge(clsx(inputs));
|
||
}
|
||
```
|
||
|
||
Teho syntyy järjestyksestä - perusmuotoilu ensin, ehdolliset tyylit toisena, käyttäjän ylikirjoitukset viimeisenä. Tämä varmistaa ennakoitavan käyttäytymisen samalla kun säilytetään täysi mukautettavuus.
|
||
|
||
## Class Variance Authority (CVA)
|
||
|
||
Monimutkaisille komponenteille, joilla on paljon variantteja, ehdollisten luokkien manuaalinen hallinta käy nopeasti työlääksi. Class Variance Authority (CVA) tarjoaa deklaratiivisen rajapinnan komponenttivarianttien määrittelyyn.
|
||
|
||
Esimerkiksi tässä on ote shadcn/ui:n [Button](https://ui.shadcn.com/docs/components/button)-komponentista:
|
||
|
||
```tsx title="@/components/ui/button.tsx"
|
||
const buttonVariants = cva(
|
||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||
{
|
||
variants: {
|
||
variant: {
|
||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||
destructive:
|
||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||
outline:
|
||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||
secondary:
|
||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||
ghost:
|
||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||
link: "text-primary underline-offset-4 hover:underline",
|
||
},
|
||
size: {
|
||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||
icon: "size-9",
|
||
},
|
||
},
|
||
defaultVariants: {
|
||
variant: "default",
|
||
size: "default",
|
||
},
|
||
}
|
||
)
|
||
```
|
||
|
||
## Parhaat käytännöt
|
||
|
||
### 1. Järjestyksellä on merkitystä
|
||
|
||
Sovella luokkia aina tässä järjestyksessä:
|
||
1. Perusmuotoilut (aina käytössä)
|
||
2. Varianttien tyylit (propsien perusteella)
|
||
3. Ehdolliset tyylit (tilan perusteella)
|
||
4. Käyttäjän ylikirjoitukset (className-prop)
|
||
|
||
```tsx
|
||
className={cn(
|
||
'base-styles', // 1. Base
|
||
variant && variantStyles, // 2. Variants
|
||
isActive && 'active', // 3. Conditionals
|
||
className // 4. User overrides
|
||
)}
|
||
```
|
||
|
||
### 2. Dokumentoi varianttisi
|
||
|
||
Käytä TypeScriptiä ja JSDocia kuvaamaan, mitä kukin variantti tekee:
|
||
|
||
```tsx
|
||
type ButtonProps = {
|
||
/**
|
||
* The visual style of the button
|
||
* @default "primary"
|
||
*/
|
||
variant?: 'primary' | 'secondary' | 'destructive' | 'ghost';
|
||
|
||
/**
|
||
* The size of the button
|
||
* @default "md"
|
||
*/
|
||
size?: 'sm' | 'md' | 'lg';
|
||
};
|
||
```
|
||
|
||
### 3. Ota toistuvat mallit talteen
|
||
|
||
Jos huomaat kirjoittavasi samaa ehdollista logiikkaa toistuvasti, erottele se:
|
||
|
||
```tsx title="utils/styles.ts"
|
||
export const focusRing = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
|
||
export const disabled = 'disabled:pointer-events-none disabled:opacity-50';
|
||
|
||
// Use in components
|
||
className={cn(focusRing, disabled, className)}
|
||
```
|
||
|
||
## Migraatio-opas
|
||
|
||
Jos siirryt eri tyylityslähestymistavasta, tässä on miten sopeuttaa yleisiä malleja:
|
||
|
||
### CSS Modulesista
|
||
|
||
```tsx title="Before - CSS Modules"
|
||
import styles from './Button.module.css';
|
||
|
||
<button className={`${styles.button} ${styles[variant]} ${className}`} />
|
||
```
|
||
|
||
```tsx title="After - cn + Tailwind"
|
||
import { cn } from '@/lib/utils';
|
||
|
||
<button className={cn(
|
||
'px-4 py-2 rounded-lg',
|
||
variant === 'primary' && 'bg-blue-500 text-white',
|
||
className
|
||
)} />
|
||
```
|
||
|
||
### styled-componentsista
|
||
|
||
```tsx title="Before - styled-components"
|
||
const Button = styled.button<{ $primary?: boolean }>`
|
||
padding: 8px 16px;
|
||
background: ${props => props.$primary ? 'blue' : 'gray'};
|
||
`;
|
||
```
|
||
|
||
```tsx title="After - cn + Tailwind"
|
||
function Button({ primary, className, ...props }) {
|
||
return (
|
||
<button
|
||
className={cn(
|
||
'px-4 py-2',
|
||
primary ? 'bg-blue-500' : 'bg-gray-500',
|
||
className
|
||
)}
|
||
{...props}
|
||
/>
|
||
);
|
||
}
|
||
```
|
||
|
||
## Suorituskykylähtökohdat
|
||
|
||
Sekä `clsx` että `tailwind-merge` ovat erittäin optimoituja, mutta pidä nämä vinkit mielessä:
|
||
|
||
1. **Määrittele variantit komponentin ulkopuolella** - CVA-variantit tulisi määritellä komponentin ulkopuolelle, jotta niitä ei luoda uudelleen jokaisella renderöinnillä.
|
||
|
||
2. **Muistimuistioi (memoize) monimutkaiset laskelmat** - Jos sinulla on raskasta ehdollista logiikkaa, harkitse muistimuistion käyttöä:
|
||
|
||
```tsx
|
||
const className = useMemo(
|
||
() => cn(
|
||
baseStyles,
|
||
expensiveComputation(props),
|
||
className
|
||
),
|
||
[props, className]
|
||
);
|
||
```
|
||
|
||
3. **Käytä CSS-muuttujia dynaamisille arvoille** - Dynaamisten arvojen luokkien generoimisen sijaan käytä CSS-muuttujia:
|
||
|
||
```tsx title="Prefer CSS variables"
|
||
// Good
|
||
<div
|
||
className="bg-[var(--color)]"
|
||
style={{ '--color': dynamicColor } as React.CSSProperties}
|
||
/>
|
||
|
||
// Avoid
|
||
<div className={`bg-[${dynamicColor}]`} />
|
||
```
|
||
|
||
Tailwind CSS:n, älykkään luokkien yhdistelyn ja variantti-rajapintojen yhdistelmä tarjoaa vankan perustan komponenttien tyylittämiselle. Tämä lähestymistapa skaalautuu yksinkertaisista painikkeista monimutkaisiin design-järjestelmiin säilyttäen ennakoitavuuden ja hyvän kehittäjäkokemuksen. |