├── .gitignore ├── Cargo.toml ├── README.md ├── styled ├── Cargo.toml ├── README.md ├── rust-toolchain └── src │ └── lib.rs └── styled_macro ├── .gitignore ├── Cargo.toml ├── rust-toolchain └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "styled", 5 | ] 6 | 7 | [workspace.dependencies] 8 | styled = { path = "./styled" } 9 | styled_macro = { path = "./styled_macro" } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Styled: Easy Styling for Leptos Components 2 | 3 | If you're looking for an easy way to apply scoped styles to your [`Leptos`](https://github.com/leptos-rs/leptos) components, `Styled` is the Leptos macro you need. With `Styled`, you can apply high-level selectors like `button` or `div` to specific components, keeping your markup clean and organized. 4 | 5 | ## Installation 6 | 7 | Use `cargo add` in your project root 8 | 9 | ```bash 10 | cargo add styled stylist 11 | ``` 12 | 13 | Then make sure that your `Cargo.toml` is properly configured, adding the feature flags for Styled 14 | 15 | ```toml 16 | [features] 17 | csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr", "styled/csr"] 18 | ssr = [ 19 | "dep:actix-files", 20 | "dep:actix-web", 21 | "dep:leptos_actix", 22 | "leptos/ssr", 23 | "leptos_meta/ssr", 24 | "leptos_router/ssr", 25 | "stylist/ssr", 26 | "styled/ssr", 27 | ] 28 | ``` 29 | 30 | ## Usage 31 | 32 | First create a basic `Leptos` component. This will serve as the foundation for this little guide. 33 | 34 | ```rust 35 | #[component] 36 | pub fn MyComponent(cx: Scope) -> impl IntoView{ 37 | view! { 38 | cx, 39 |
"hello"
40 | } 41 | } 42 | ``` 43 | 44 | Next, import the `style` macro, powered by an awesome crate called [`Stylist`](https://github.com/futursolo/stylist-rs), to create your styles. 45 | Just add this to the top of your file. 46 | 47 | ```rust 48 | use styled::style; 49 | ``` 50 | 51 | You can then use the `style` macro to create a `Result` containing your styles. Let's modify our component: 52 | 53 | ```rust 54 | #[component] 55 | pub fn MyComponent(cx: Scope) -> impl IntoView{ 56 | 57 | let styles = style!( 58 | div { 59 | background-color: red; 60 | color: white; 61 | } 62 | ); 63 | 64 | view! { 65 | cx, 66 |
"hello"
67 | } 68 | } 69 | ``` 70 | 71 | Now, let's apply those styles with our `styled::view!` macro! 72 | 73 | ```rust 74 | #[component] 75 | pub fn MyComponent(cx: Scope) -> impl IntoView { 76 | 77 | let styles = style!( 78 | div { 79 | background-color: red; 80 | color: white; 81 | } 82 | ); 83 | 84 | styled::view! { 85 | cx, 86 | styles, 87 |
"This text should be red with white text."
88 | } 89 | } 90 | ``` 91 | 92 | Now we can define another component that also uses the `div` CSS selector but it's styles will only apply to the elements inside of it's enclosing `styled::view!` macro. 93 | 94 | ```rust 95 | #[component] 96 | pub fn AnotherComponent(cx: Scope) -> impl IntoView { 97 | 98 | // note were using a plain div selector and it wont clash with MyComponent's div style! 99 | let styles = style!( 100 | div { 101 | background-color: blue; 102 | color: gray; 103 | } 104 | ); 105 | 106 | styled::view! { 107 | cx, 108 | styles, 109 |
"This text should be blue with gray text."
110 | } 111 | } 112 | ``` 113 | 114 | ## Longer Example 115 | 116 | ```rust 117 | // /src/components/button.rs 118 | 119 | use crate::theme::get_theme; 120 | use leptos::*; 121 | use styled::style; 122 | 123 | #[derive(PartialEq)] 124 | pub enum Variant { 125 | PRIMARY, 126 | SECONDARY, 127 | ALERT, 128 | DISABLED, 129 | } 130 | 131 | impl Variant { 132 | pub fn is(&self, variant: &Variant) -> bool { 133 | self == variant 134 | } 135 | } 136 | 137 | struct ButtonColors { 138 | text: String, 139 | background: String, 140 | border: String, 141 | } 142 | 143 | fn get_colors(variant: &Variant) -> ButtonColors { 144 | let theme = get_theme().unwrap(); 145 | match variant { 146 | Variant::PRIMARY => ButtonColors { 147 | text: theme.white(), 148 | background: theme.black(), 149 | border: theme.transparent(), 150 | }, 151 | Variant::SECONDARY => ButtonColors { 152 | text: theme.black(), 153 | background: theme.white(), 154 | border: theme.gray.lightest(), 155 | }, 156 | Variant::ALERT => ButtonColors { 157 | text: theme.white(), 158 | background: theme.red(), 159 | border: theme.transparent(), 160 | }, 161 | Variant::DISABLED => ButtonColors { 162 | text: theme.white(), 163 | background: theme.red(), 164 | border: theme.transparent(), 165 | }, 166 | } 167 | } 168 | 169 | #[component] 170 | pub fn Button(cx: Scope, variant: Variant) -> impl IntoView { 171 | let disabled = variant.is(&Variant::DISABLED); 172 | 173 | let styles = styles(&variant); 174 | 175 | styled::view! { 176 | cx, 177 | styles, 178 | 179 | } 180 | } 181 | 182 | fn styles<'a>(variant: &Variant) -> styled::Result { 183 | let colors = get_colors(variant); 184 | 185 | style!( 186 | button { 187 | color: ${colors.text}; 188 | background-color: ${colors.background}; 189 | border: 1px solid ${colors.border}; 190 | outline: none; 191 | height: 48px; 192 | min-width: 154px; 193 | font-size: 14px; 194 | font-weight: 700; 195 | text-align: center; 196 | box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px; 197 | position: relative; 198 | box-sizing: border-box; 199 | vertical-align: middle; 200 | text-align: center; 201 | text-overflow: ellipsis; 202 | text-transform: uppercase; 203 | overflow: hidden; 204 | cursor: pointer; 205 | transition: box-shadow 0.2s; 206 | margin: 10px; 207 | } 208 | 209 | & button:active { 210 | transform: scale(0.99); 211 | } 212 | 213 | 214 | & button::-moz-focus-inner { 215 | border: none; 216 | } 217 | 218 | & button::before { 219 | content: ""; 220 | position: absolute; 221 | top: 0; 222 | bottom: 0; 223 | left: 0; 224 | right: 0; 225 | background-color: rgb(255, 255, 255); 226 | opacity: 0; 227 | transition: opacity 0.2s; 228 | } 229 | 230 | & button::after { 231 | content: ""; 232 | position: absolute; 233 | left: 50%; 234 | top: 50%; 235 | border-radius: 50%; 236 | padding: 50%; 237 | background-color: ${colors.text}; 238 | opacity: 0; 239 | transform: translate(-50%, -50%) scale(1); 240 | transition: opacity 1s, transform 0.5s; 241 | } 242 | 243 | & button:hover, 244 | & button:focus { 245 | box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12); 246 | } 247 | 248 | & button:hover::before { 249 | opacity: 0.08; 250 | } 251 | 252 | & button:hover:focus::before { 253 | opacity: 0.3; 254 | } 255 | 256 | & button:active { 257 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); 258 | } 259 | 260 | & button:active::after { 261 | opacity: 0.32; 262 | transform: translate(-50%, -50%) scale(0); 263 | transition: transform 0s; 264 | } 265 | 266 | & button:disabled { 267 | color: rgba(0, 0, 0, 0.28); 268 | background-color: rgba(0, 0, 0, 0.12); 269 | box-shadow: none; 270 | cursor: initial; 271 | } 272 | 273 | & button:disabled::before { 274 | opacity: 0; 275 | } 276 | 277 | & button:disabled::after { 278 | opacity: 0; 279 | } 280 | 281 | ) 282 | } 283 | 284 | ``` 285 | 286 | ```rust 287 | // /src/theme/mod.rs 288 | use csscolorparser::Color; 289 | 290 | pub fn get_theme() -> Result { 291 | let theme = Theme { 292 | teal: Colors { 293 | main: Color::from_html("#6FDDDB")?, 294 | darker: Color::from_html("#2BB4B2")?, 295 | lighter: Color::from_html("#7EE1DF")?, 296 | lightest: Color::from_html("#B2EDEC")?, 297 | }, 298 | pink: Colors { 299 | main: Color::from_html("#E93EF5")?, 300 | darker: Color::from_html("#C70BD4")?, 301 | lighter: Color::from_html("#F5A4FA")?, 302 | lightest: Color::from_html("#FCE1FD")?, 303 | }, 304 | green: Colors { 305 | main: Color::from_html("#54D072")?, 306 | darker: Color::from_html("#30AF4F")?, 307 | lighter: Color::from_html("#82DD98")?, 308 | lightest: Color::from_html("#B4EAC1")?, 309 | }, 310 | purple: Colors { 311 | main: Color::from_html("#8C18FB")?, 312 | darker: Color::from_html("#7204DB")?, 313 | lighter: Color::from_html("#B162FC")?, 314 | lightest: Color::from_html("#D0A1FD")?, 315 | }, 316 | yellow: Colors { 317 | main: Color::from_html("#E1E862")?, 318 | darker: Color::from_html("#BAC31D")?, 319 | lighter: Color::from_html("#EFF3AC")?, 320 | lightest: Color::from_html("#FAFBE3")?, 321 | }, 322 | gray: Colors { 323 | main: Color::from_html("#4a4a4a")?, 324 | darker: Color::from_html("#3d3d3d")?, 325 | lighter: Color::from_html("#939393")?, 326 | lightest: Color::from_html("#c4c4c4")?, 327 | }, 328 | red: Color::from_html("#FF5854")?, 329 | black: Color::from_html("#000000")?, 330 | white: Color::from_html("#FFFFFF")?, 331 | transparent: Color::from_html("transparent")?, 332 | }; 333 | 334 | Ok(theme) 335 | } 336 | 337 | pub struct Theme { 338 | pub teal: Colors, 339 | pub pink: Colors, 340 | pub green: Colors, 341 | pub purple: Colors, 342 | pub yellow: Colors, 343 | pub gray: Colors, 344 | pub red: Color, 345 | pub black: Color, 346 | pub white: Color, 347 | pub transparent: Color, 348 | } 349 | 350 | pub struct Colors { 351 | pub main: Color, 352 | pub darker: Color, 353 | pub lighter: Color, 354 | pub lightest: Color, 355 | } 356 | 357 | impl Colors { 358 | pub fn main(&self) -> String { 359 | self.main.to_hex_string() 360 | } 361 | pub fn darker(&self) -> String { 362 | self.darker.to_hex_string() 363 | } 364 | pub fn lighter(&self) -> String { 365 | self.lighter.to_hex_string() 366 | } 367 | pub fn lightest(&self) -> String { 368 | self.lightest.to_hex_string() 369 | } 370 | } 371 | 372 | impl Theme { 373 | pub fn red(&self) -> String { 374 | self.red.to_hex_string() 375 | } 376 | pub fn black(&self) -> String { 377 | self.black.to_hex_string() 378 | } 379 | pub fn white(&self) -> String { 380 | self.white.to_hex_string() 381 | } 382 | pub fn transparent(&self) -> String { 383 | self.transparent.to_hex_string() 384 | } 385 | } 386 | 387 | 388 | ``` 389 | 390 | ```rust 391 | // /src/app.rs 392 | 393 | #[component] 394 | fn HomePage(cx: Scope) -> impl IntoView { 395 | view! { cx, 396 | 162 | } 163 | } 164 | 165 | fn styles<'a>(variant: &Variant) -> styled::Result { 166 | let colors = get_colors(variant); 167 | 168 | style!( 169 | button { 170 | color: ${colors.text}; 171 | background-color: ${colors.background}; 172 | border: 1px solid ${colors.border}; 173 | outline: none; 174 | height: 48px; 175 | min-width: 154px; 176 | font-size: 14px; 177 | font-weight: 700; 178 | text-align: center; 179 | box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px; 180 | position: relative; 181 | box-sizing: border-box; 182 | vertical-align: middle; 183 | text-align: center; 184 | text-overflow: ellipsis; 185 | text-transform: uppercase; 186 | overflow: hidden; 187 | cursor: pointer; 188 | transition: box-shadow 0.2s; 189 | margin: 10px; 190 | } 191 | 192 | & button:active { 193 | transform: scale(0.99); 194 | } 195 | 196 | 197 | & button::-moz-focus-inner { 198 | border: none; 199 | } 200 | 201 | & button::before { 202 | content: ""; 203 | position: absolute; 204 | top: 0; 205 | bottom: 0; 206 | left: 0; 207 | right: 0; 208 | background-color: rgb(255, 255, 255); 209 | opacity: 0; 210 | transition: opacity 0.2s; 211 | } 212 | 213 | & button::after { 214 | content: ""; 215 | position: absolute; 216 | left: 50%; 217 | top: 50%; 218 | border-radius: 50%; 219 | padding: 50%; 220 | background-color: ${colors.text}; 221 | opacity: 0; 222 | transform: translate(-50%, -50%) scale(1); 223 | transition: opacity 1s, transform 0.5s; 224 | } 225 | 226 | & button:hover, 227 | & button:focus { 228 | box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12); 229 | } 230 | 231 | & button:hover::before { 232 | opacity: 0.08; 233 | } 234 | 235 | & button:hover:focus::before { 236 | opacity: 0.3; 237 | } 238 | 239 | & button:active { 240 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); 241 | } 242 | 243 | & button:active::after { 244 | opacity: 0.32; 245 | transform: translate(-50%, -50%) scale(0); 246 | transition: transform 0s; 247 | } 248 | 249 | & button:disabled { 250 | color: rgba(0, 0, 0, 0.28); 251 | background-color: rgba(0, 0, 0, 0.12); 252 | box-shadow: none; 253 | cursor: initial; 254 | } 255 | 256 | & button:disabled::before { 257 | opacity: 0; 258 | } 259 | 260 | & button:disabled::after { 261 | opacity: 0; 262 | } 263 | 264 | ) 265 | } 266 | 267 | ``` 268 | 269 | ```rust 270 | // /src/theme/mod.rs 271 | use csscolorparser::Color; 272 | 273 | pub fn get_theme() -> Result { 274 | let theme = Theme { 275 | teal: Colors { 276 | main: Color::from_html("#6FDDDB")?, 277 | darker: Color::from_html("#2BB4B2")?, 278 | lighter: Color::from_html("#7EE1DF")?, 279 | lightest: Color::from_html("#B2EDEC")?, 280 | }, 281 | pink: Colors { 282 | main: Color::from_html("#E93EF5")?, 283 | darker: Color::from_html("#C70BD4")?, 284 | lighter: Color::from_html("#F5A4FA")?, 285 | lightest: Color::from_html("#FCE1FD")?, 286 | }, 287 | green: Colors { 288 | main: Color::from_html("#54D072")?, 289 | darker: Color::from_html("#30AF4F")?, 290 | lighter: Color::from_html("#82DD98")?, 291 | lightest: Color::from_html("#B4EAC1")?, 292 | }, 293 | purple: Colors { 294 | main: Color::from_html("#8C18FB")?, 295 | darker: Color::from_html("#7204DB")?, 296 | lighter: Color::from_html("#B162FC")?, 297 | lightest: Color::from_html("#D0A1FD")?, 298 | }, 299 | yellow: Colors { 300 | main: Color::from_html("#E1E862")?, 301 | darker: Color::from_html("#BAC31D")?, 302 | lighter: Color::from_html("#EFF3AC")?, 303 | lightest: Color::from_html("#FAFBE3")?, 304 | }, 305 | gray: Colors { 306 | main: Color::from_html("#4a4a4a")?, 307 | darker: Color::from_html("#3d3d3d")?, 308 | lighter: Color::from_html("#939393")?, 309 | lightest: Color::from_html("#c4c4c4")?, 310 | }, 311 | red: Color::from_html("#FF5854")?, 312 | black: Color::from_html("#000000")?, 313 | white: Color::from_html("#FFFFFF")?, 314 | transparent: Color::from_html("transparent")?, 315 | }; 316 | 317 | Ok(theme) 318 | } 319 | 320 | pub struct Theme { 321 | pub teal: Colors, 322 | pub pink: Colors, 323 | pub green: Colors, 324 | pub purple: Colors, 325 | pub yellow: Colors, 326 | pub gray: Colors, 327 | pub red: Color, 328 | pub black: Color, 329 | pub white: Color, 330 | pub transparent: Color, 331 | } 332 | 333 | pub struct Colors { 334 | pub main: Color, 335 | pub darker: Color, 336 | pub lighter: Color, 337 | pub lightest: Color, 338 | } 339 | 340 | impl Colors { 341 | pub fn main(&self) -> String { 342 | self.main.to_hex_string() 343 | } 344 | pub fn darker(&self) -> String { 345 | self.darker.to_hex_string() 346 | } 347 | pub fn lighter(&self) -> String { 348 | self.lighter.to_hex_string() 349 | } 350 | pub fn lightest(&self) -> String { 351 | self.lightest.to_hex_string() 352 | } 353 | } 354 | 355 | impl Theme { 356 | pub fn red(&self) -> String { 357 | self.red.to_hex_string() 358 | } 359 | pub fn black(&self) -> String { 360 | self.black.to_hex_string() 361 | } 362 | pub fn white(&self) -> String { 363 | self.white.to_hex_string() 364 | } 365 | pub fn transparent(&self) -> String { 366 | self.transparent.to_hex_string() 367 | } 368 | } 369 | 370 | 371 | ``` 372 | 373 | ```rust 374 | // /src/app.rs 375 | 376 | #[component] 377 | fn HomePage(cx: Scope) -> impl IntoView { 378 | view! { cx, 379 |