diff --git a/CHANGELOG.md b/CHANGELOG.md index 38ee1d6115e0..b860a13276e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Experimental_: Add `wrap-anywhere`, `wrap-break-word`, and `wrap-normal` utilities ([#12128](https://github.com/tailwindlabs/tailwindcss/pull/12128)) - _Experimental_: Add `@source inline(…)` ([#17147](https://github.com/tailwindlabs/tailwindcss/pull/17147)) - _Experimental_: Add `@source not` ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255)) +- _Experimental_: Add `text-shadow-*` utilities ([#17389](https://github.com/tailwindlabs/tailwindcss/pull/17389)) - Added new `bg-{top,bottom}-{left,right}` utilities ([#17378](https://github.com/tailwindlabs/tailwindcss/pull/17378)) ### Fixed diff --git a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap index c9a759bebe78..21ec15f3c562 100644 --- a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap @@ -7756,6 +7756,75 @@ exports[`getClassList 1`] = ` "text-nowrap", "text-pretty", "text-right", + "text-shadow", + "text-shadow-current", + "text-shadow-current/0", + "text-shadow-current/5", + "text-shadow-current/10", + "text-shadow-current/15", + "text-shadow-current/20", + "text-shadow-current/25", + "text-shadow-current/30", + "text-shadow-current/35", + "text-shadow-current/40", + "text-shadow-current/45", + "text-shadow-current/50", + "text-shadow-current/55", + "text-shadow-current/60", + "text-shadow-current/65", + "text-shadow-current/70", + "text-shadow-current/75", + "text-shadow-current/80", + "text-shadow-current/85", + "text-shadow-current/90", + "text-shadow-current/95", + "text-shadow-current/100", + "text-shadow-inherit", + "text-shadow-inherit/0", + "text-shadow-inherit/5", + "text-shadow-inherit/10", + "text-shadow-inherit/15", + "text-shadow-inherit/20", + "text-shadow-inherit/25", + "text-shadow-inherit/30", + "text-shadow-inherit/35", + "text-shadow-inherit/40", + "text-shadow-inherit/45", + "text-shadow-inherit/50", + "text-shadow-inherit/55", + "text-shadow-inherit/60", + "text-shadow-inherit/65", + "text-shadow-inherit/70", + "text-shadow-inherit/75", + "text-shadow-inherit/80", + "text-shadow-inherit/85", + "text-shadow-inherit/90", + "text-shadow-inherit/95", + "text-shadow-inherit/100", + "text-shadow-initial", + "text-shadow-none", + "text-shadow-transparent", + "text-shadow-transparent/0", + "text-shadow-transparent/5", + "text-shadow-transparent/10", + "text-shadow-transparent/15", + "text-shadow-transparent/20", + "text-shadow-transparent/25", + "text-shadow-transparent/30", + "text-shadow-transparent/35", + "text-shadow-transparent/40", + "text-shadow-transparent/45", + "text-shadow-transparent/50", + "text-shadow-transparent/55", + "text-shadow-transparent/60", + "text-shadow-transparent/65", + "text-shadow-transparent/70", + "text-shadow-transparent/75", + "text-shadow-transparent/80", + "text-shadow-transparent/85", + "text-shadow-transparent/90", + "text-shadow-transparent/95", + "text-shadow-transparent/100", "text-start", "text-transparent", "text-transparent/0", diff --git a/packages/tailwindcss/src/feature-flags.ts b/packages/tailwindcss/src/feature-flags.ts index d00d1d45b1ae..f5c00bf656a9 100644 --- a/packages/tailwindcss/src/feature-flags.ts +++ b/packages/tailwindcss/src/feature-flags.ts @@ -6,5 +6,6 @@ export const enableSafeAlignment = process.env.FEATURES_ENV !== 'stable' export const enableScripting = process.env.FEATURES_ENV !== 'stable' export const enableSourceInline = process.env.FEATURES_ENV !== 'stable' export const enableSourceNot = process.env.FEATURES_ENV !== 'stable' +export const enableTextShadows = process.env.FEATURES_ENV !== 'stable' export const enableUserValid = process.env.FEATURES_ENV !== 'stable' export const enableWrapAnywhere = process.env.FEATURES_ENV !== 'stable' diff --git a/packages/tailwindcss/src/theme.ts b/packages/tailwindcss/src/theme.ts index b72b99a09b0c..74375b6f723d 100644 --- a/packages/tailwindcss/src/theme.ts +++ b/packages/tailwindcss/src/theme.ts @@ -21,10 +21,11 @@ const ignoredThemeKeyMap = new Map([ '--text', [ '--text-color', - '--text-underline-offset', - '--text-indent', - '--text-decoration-thickness', '--text-decoration-color', + '--text-decoration-thickness', + '--text-indent', + '--text-shadow', + '--text-underline-offset', ], ], ]) diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 64917430606d..9cce40da3471 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -15354,6 +15354,160 @@ test('text', async () => { ).toEqual('') }) +test('text-shadow', async () => { + expect( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.1); + --text-shadow-sm: 0px 1px 2px rgb(0 0 0 / 0.06), 0px 2px 2px rgb(0 0 0 / 0.06); + } + @tailwind utilities; + `, + [ + // Shadows + 'text-shadow-2xs', + 'text-shadow-sm', + 'text-shadow-none', + 'text-shadow-[12px_12px_#0088cc]', + 'text-shadow-[10px_10px]', + 'text-shadow-[var(--value)]', + 'text-shadow-[shadow:var(--value)]', + + // Colors + 'text-shadow-red-500', + 'text-shadow-red-500/50', + 'text-shadow-red-500/2.25', + 'text-shadow-red-500/2.5', + 'text-shadow-red-500/2.75', + 'text-shadow-red-500/[0.5]', + 'text-shadow-red-500/[50%]', + 'text-shadow-current', + 'text-shadow-current/50', + 'text-shadow-current/[0.5]', + 'text-shadow-current/[50%]', + 'text-shadow-inherit', + 'text-shadow-transparent', + 'text-shadow-[#0088cc]', + 'text-shadow-[#0088cc]/50', + 'text-shadow-[#0088cc]/[0.5]', + 'text-shadow-[#0088cc]/[50%]', + 'text-shadow-[color:var(--value)]', + 'text-shadow-[color:var(--value)]/50', + 'text-shadow-[color:var(--value)]/[0.5]', + 'text-shadow-[color:var(--value)]/[50%]', + ], + ), + ).toMatchInlineSnapshot(` + ":root, :host { + --color-red-500: #ef4444; + } + + .text-shadow-2xs { + text-shadow: 0px 1px 0px var(--tw-text-shadow-color, #0000001a); + } + + .text-shadow-\\[\\#0088cc\\] { + --tw-text-shadow-color: #08c; + } + + .text-shadow-\\[\\#0088cc\\]\\/50, .text-shadow-\\[\\#0088cc\\]\\/\\[0\\.5\\], .text-shadow-\\[\\#0088cc\\]\\/\\[50\\%\\] { + --tw-text-shadow-color: oklab(59.9824% -.06725 -.12414 / .5); + } + + .text-shadow-\\[10px_10px\\] { + text-shadow: 10px 10px var(--tw-text-shadow-color, currentcolor); + } + + .text-shadow-\\[12px_12px_\\#0088cc\\] { + text-shadow: 12px 12px var(--tw-text-shadow-color, #08c); + } + + .text-shadow-\\[color\\:var\\(--value\\)\\] { + --tw-text-shadow-color: var(--value); + } + + .text-shadow-\\[color\\:var\\(--value\\)\\]\\/50, .text-shadow-\\[color\\:var\\(--value\\)\\]\\/\\[0\\.5\\], .text-shadow-\\[color\\:var\\(--value\\)\\]\\/\\[50\\%\\] { + --tw-text-shadow-color: color-mix(in oklab, var(--value) 50%, transparent); + } + + .text-shadow-\\[shadow\\:var\\(--value\\)\\], .text-shadow-\\[var\\(--value\\)\\] { + text-shadow: var(--value); + } + + .text-shadow-current { + --tw-text-shadow-color: currentColor; + } + + .text-shadow-current\\/50, .text-shadow-current\\/\\[0\\.5\\], .text-shadow-current\\/\\[50\\%\\] { + --tw-text-shadow-color: color-mix(in oklab, currentColor 50%, transparent); + } + + .text-shadow-inherit { + --tw-text-shadow-color: inherit; + } + + .text-shadow-none { + text-shadow: none; + } + + .text-shadow-red-500 { + --tw-text-shadow-color: var(--color-red-500); + } + + .text-shadow-red-500\\/2\\.5 { + --tw-text-shadow-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); + } + + .text-shadow-red-500\\/2\\.25 { + --tw-text-shadow-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); + } + + .text-shadow-red-500\\/2\\.75 { + --tw-text-shadow-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); + } + + .text-shadow-red-500\\/50, .text-shadow-red-500\\/\\[0\\.5\\], .text-shadow-red-500\\/\\[50\\%\\] { + --tw-text-shadow-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); + } + + .text-shadow-sm { + text-shadow: 0px 1px 2px var(--tw-text-shadow-color, #0000000f), 0px 2px 2px var(--tw-text-shadow-color, #0000000f); + } + + .text-shadow-transparent { + --tw-text-shadow-color: transparent; + } + + @property --tw-text-shadow-color { + syntax: "*"; + inherits: false + }" + `) + expect( + await run([ + '-shadow-xl', + '-shadow-none', + '-shadow-red-500', + '-shadow-red-500/50', + '-shadow-red-500/[0.5]', + '-shadow-red-500/[50%]', + '-shadow-current', + '-shadow-current/50', + '-shadow-current/[0.5]', + '-shadow-current/[50%]', + '-shadow-inherit', + '-shadow-transparent', + '-shadow-[#0088cc]', + '-shadow-[#0088cc]/50', + '-shadow-[#0088cc]/[0.5]', + '-shadow-[#0088cc]/[50%]', + '-shadow-[var(--value)]', + ]), + ).toEqual('') +}) + test('shadow', async () => { expect( await compileCss( diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index dfe572d03930..a83494a86a3a 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -11,7 +11,12 @@ import { } from './ast' import type { Candidate, CandidateModifier, NamedUtilityValue } from './candidate' import type { DesignSystem } from './design-system' -import { enableBaselineLast, enableSafeAlignment, enableWrapAnywhere } from './feature-flags' +import { + enableBaselineLast, + enableSafeAlignment, + enableTextShadows, + enableWrapAnywhere, +} from './feature-flags' import type { Theme, ThemeKey } from './theme' import { compareBreakpoints } from './utils/compare-breakpoints' import { DefaultMap } from './utils/default-map' @@ -4242,6 +4247,97 @@ export function createUtilities(theme: Theme) { }, ]) + if (enableTextShadows) { + let textShadowProperties = () => { + return atRoot([property('--tw-text-shadow-color')]) + } + + staticUtility('text-shadow-initial', [ + textShadowProperties, + ['--tw-text-shadow-color', 'initial'], + ]) + + utilities.functional('text-shadow', (candidate) => { + if (!candidate.value) { + let value = theme.get(['--text-shadow']) + if (value === null) return + + return [ + textShadowProperties(), + decl( + 'text-shadow', + replaceShadowColors(value, (color) => `var(--tw-text-shadow-color, ${color})`), + ), + ] + } + + if (candidate.value.kind === 'arbitrary') { + let value: string | null = candidate.value.value + let type = candidate.value.dataType ?? inferDataType(value, ['color']) + + switch (type) { + case 'color': { + value = asColor(value, candidate.modifier, theme) + if (value === null) return + + return [textShadowProperties(), decl('--tw-text-shadow-color', value)] + } + default: { + return [ + textShadowProperties(), + decl( + 'text-shadow', + replaceShadowColors(value, (color) => `var(--tw-text-shadow-color, ${color})`), + ), + ] + } + } + } + + switch (candidate.value.value) { + case 'none': + if (candidate.modifier) return + return [textShadowProperties(), decl('text-shadow', 'none')] + } + + // Shadow size + { + let value = theme.get([`--text-shadow-${candidate.value.value}`]) + if (value) { + if (candidate.modifier) return + return [ + textShadowProperties(), + decl( + 'text-shadow', + replaceShadowColors(value, (color) => `var(--tw-text-shadow-color, ${color})`), + ), + ] + } + } + + // Shadow color + { + let value = resolveThemeColor(candidate, theme, ['--text-shadow-color', '--color']) + if (value) { + return [textShadowProperties(), decl('--tw-text-shadow-color', value)] + } + } + }) + + suggest('text-shadow', () => [ + { + values: ['current', 'inherit', 'transparent'], + valueThemeKeys: ['--text-shadow-color', '--color'], + modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), + }, + { + values: ['none'], + valueThemeKeys: ['--text-shadow'], + hasDefaultValue: true, + }, + ]) + } + { let cssBoxShadowValue = [ 'var(--tw-inset-shadow)', diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts index 64057877b7bf..89dd467e3ebe 100644 --- a/packages/tailwindcss/tests/ui.spec.ts +++ b/packages/tailwindcss/tests/ui.spec.ts @@ -410,6 +410,46 @@ test('inset shadow colors', async ({ page }) => { ) }) +test('text shadow colors', async ({ page }) => { + let { getPropertyValue } = await render( + page, + html` +
+
+
+
Hello world
+
+ Hello world +
+ `, + ) + + expect(await getPropertyValue('#a', 'text-shadow')).toEqual('rgb(255, 0, 0) 0px 1px 1px') + expect(await getPropertyValue('#b', 'text-shadow')).toEqual( + 'rgb(255, 0, 0) 0px 1px 2px, rgb(255, 0, 0) 0px 3px 2px, rgb(255, 0, 0) 0px 4px 8px', + ) + expect(await getPropertyValue('#c', 'text-shadow')).toEqual('rgb(255, 0, 0) 0px 2px 4px') + + expect(await getPropertyValue('#d', 'text-shadow')).toEqual('rgb(255, 0, 0) 0px 1px 1px') + + await page.locator('#d').hover() + + expect(await getPropertyValue('#d', 'text-shadow')).toEqual( + 'rgb(255, 0, 0) 0px 1px 2px, rgb(255, 0, 0) 0px 3px 2px, rgb(255, 0, 0) 0px 4px 8px', + ) + + expect(await getPropertyValue('#e', 'text-shadow')).toEqual('rgb(255, 0, 0) 0px 1px 1px') + + await page.locator('#e').hover() + + expect(await getPropertyValue('#e', 'text-shadow')).toEqual( + 'rgba(0, 0, 0, 0.1) 0px 1px 2px, rgba(0, 0, 0, 0.1) 0px 3px 2px, rgba(0, 0, 0, 0.1) 0px 4px 8px', + ) +}) + test('filter', async ({ page }) => { let { getPropertyValue } = await render( page, diff --git a/packages/tailwindcss/theme.css b/packages/tailwindcss/theme.css index 6f40223e4f5e..25411806d710 100644 --- a/packages/tailwindcss/theme.css +++ b/packages/tailwindcss/theme.css @@ -374,6 +374,15 @@ --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1); --drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15); + --text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.15); + --text-shadow-xs: 0px 1px 1px rgb(0 0 0 / 0.2); + --text-shadow-sm: + 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075); + --text-shadow-md: + 0px 1px 1px rgb(0 0 0 / 0.1), 0px 1px 2px rgb(0 0 0 / 0.1), 0px 2px 4px rgb(0 0 0 / 0.1); + --text-shadow-lg: + 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1); + --ease-in: cubic-bezier(0.4, 0, 1, 1); --ease-out: cubic-bezier(0, 0, 0.2, 1); --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);