or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

component-management.mddata-display-components.mdfeedback-components.mdform-components.mdindex.mdlayout-components.mdnavigation-components.mdvisual-effects.md

visual-effects.mddocs/

0

# Visual Effects

1

2

Visual enhancement components including ripple effects and animations that provide tactile feedback and smooth transitions. These effects enhance the user experience by providing immediate visual feedback for interactions.

3

4

## Capabilities

5

6

### Material Ripple

7

8

Touch ripple effect component that creates expanding circular animations on user interactions.

9

10

```javascript { .api }

11

/**

12

* Material Design ripple effect component

13

* CSS Class: mdl-js-ripple-effect

14

* Widget: false

15

*/

16

interface MaterialRipple {

17

/**

18

* Get current animation frame count

19

* @returns Current frame count number

20

*/

21

getFrameCount(): number;

22

23

/**

24

* Set animation frame count

25

* @param frameCount - New frame count value

26

*/

27

setFrameCount(frameCount: number): void;

28

29

/**

30

* Get the DOM element used for ripple effect

31

* @returns HTMLElement representing the ripple

32

*/

33

getRippleElement(): HTMLElement;

34

35

/**

36

* Set ripple animation coordinates

37

* @param x - X coordinate for ripple center

38

* @param y - Y coordinate for ripple center

39

*/

40

setRippleXY(x: number, y: number): void;

41

42

/**

43

* Set ripple styling for animation phase

44

* @param start - Whether this is the start or end of animation

45

*/

46

setRippleStyles(start: boolean): void;

47

48

/** Handle animation frame updates */

49

animFrameHandler(): void;

50

}

51

```

52

53

**HTML Structure:**

54

55

```html

56

<!-- Button with ripple effect -->

57

<button class="mdl-button mdl-js-button mdl-js-ripple-effect">

58

Ripple Button

59

</button>

60

61

<!-- Checkbox with ripple effect -->

62

<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="checkbox-ripple">

63

<input type="checkbox" id="checkbox-ripple" class="mdl-checkbox__input">

64

<span class="mdl-checkbox__label">Check with ripple</span>

65

</label>

66

67

<!-- Menu with ripple effect -->

68

<ul class="mdl-menu mdl-menu--bottom-left mdl-js-menu mdl-js-ripple-effect"

69

for="demo-menu-button">

70

<li class="mdl-menu__item">Menu Item 1</li>

71

<li class="mdl-menu__item">Menu Item 2</li>

72

</ul>

73

74

<!-- Custom element with ripple -->

75

<div class="custom-element mdl-js-ripple-effect" tabindex="0">

76

Click me for ripple effect

77

</div>

78

```

79

80

**Usage Examples:**

81

82

Since ripple effects are largely automatic, direct API usage is rare, but here are some advanced use cases:

83

84

```javascript

85

// Access ripple instance (rarely needed)

86

const rippleElement = document.querySelector('.mdl-js-ripple-effect');

87

// Note: MaterialRipple instances are typically managed internally

88

89

// Programmatically trigger ripple effect

90

function triggerRipple(element, x, y) {

91

// Create a synthetic mouse event at specific coordinates

92

const event = new MouseEvent('mousedown', {

93

clientX: x,

94

clientY: y,

95

bubbles: true

96

});

97

98

element.dispatchEvent(event);

99

100

// Clean up with mouseup

101

setTimeout(() => {

102

const upEvent = new MouseEvent('mouseup', {

103

bubbles: true

104

});

105

element.dispatchEvent(upEvent);

106

}, 100);

107

}

108

109

// Add ripple effect to custom elements

110

function addRippleToElement(element) {

111

if (!element.classList.contains('mdl-js-ripple-effect')) {

112

element.classList.add('mdl-js-ripple-effect');

113

componentHandler.upgradeElement(element);

114

}

115

}

116

117

// Remove ripple effect

118

function removeRippleFromElement(element) {

119

element.classList.remove('mdl-js-ripple-effect');

120

// Remove ripple container if it exists

121

const rippleContainer = element.querySelector('.mdl-ripple-container');

122

if (rippleContainer) {

123

rippleContainer.remove();

124

}

125

}

126

127

// Custom ripple colors

128

function setRippleColor(element, color) {

129

const style = document.createElement('style');

130

const className = 'ripple-' + Math.random().toString(36).substr(2, 9);

131

132

element.classList.add(className);

133

134

style.textContent = `

135

.${className} .mdl-ripple {

136

background: ${color};

137

}

138

`;

139

140

document.head.appendChild(style);

141

}

142

143

// Usage examples

144

const customButton = document.querySelector('#custom-button');

145

addRippleToElement(customButton);

146

setRippleColor(customButton, '#ff4081');

147

148

// Trigger ripple on center of element

149

const rect = customButton.getBoundingClientRect();

150

const centerX = rect.left + rect.width / 2;

151

const centerY = rect.top + rect.height / 2;

152

triggerRipple(customButton, centerX, centerY);

153

```

154

155

### Ripple Effect Customization

156

157

```javascript

158

// Custom ripple implementation for non-standard elements

159

class CustomRippleManager {

160

constructor() {

161

this.ripples = new Map();

162

this.setupGlobalListeners();

163

}

164

165

setupGlobalListeners() {

166

document.addEventListener('mousedown', (event) => {

167

if (event.target.matches('[data-custom-ripple]')) {

168

this.createRipple(event.target, event);

169

}

170

});

171

172

document.addEventListener('mouseup', () => {

173

this.fadeAllRipples();

174

});

175

176

document.addEventListener('mouseleave', () => {

177

this.fadeAllRipples();

178

});

179

}

180

181

createRipple(element, event) {

182

const rect = element.getBoundingClientRect();

183

const size = Math.max(rect.width, rect.height);

184

const x = event.clientX - rect.left - size / 2;

185

const y = event.clientY - rect.top - size / 2;

186

187

const ripple = document.createElement('div');

188

ripple.className = 'custom-ripple';

189

ripple.style.cssText = `

190

position: absolute;

191

width: ${size}px;

192

height: ${size}px;

193

left: ${x}px;

194

top: ${y}px;

195

background: rgba(255, 255, 255, 0.3);

196

border-radius: 50%;

197

transform: scale(0);

198

pointer-events: none;

199

transition: transform 0.6s, opacity 0.6s;

200

`;

201

202

// Ensure element has relative positioning

203

if (getComputedStyle(element).position === 'static') {

204

element.style.position = 'relative';

205

}

206

207

// Ensure element has overflow hidden

208

element.style.overflow = 'hidden';

209

210

element.appendChild(ripple);

211

212

// Store ripple reference

213

this.ripples.set(ripple, { element, startTime: Date.now() });

214

215

// Trigger animation

216

requestAnimationFrame(() => {

217

ripple.style.transform = 'scale(2)';

218

});

219

}

220

221

fadeAllRipples() {

222

this.ripples.forEach((info, ripple) => {

223

const elapsed = Date.now() - info.startTime;

224

225

// Only fade if ripple has been visible for minimum time

226

if (elapsed > 100) {

227

ripple.style.opacity = '0';

228

229

setTimeout(() => {

230

if (ripple.parentNode) {

231

ripple.parentNode.removeChild(ripple);

232

}

233

this.ripples.delete(ripple);

234

}, 600);

235

}

236

});

237

}

238

}

239

240

// Initialize custom ripple manager

241

const customRippleManager = new CustomRippleManager();

242

243

// Usage: Add data-custom-ripple attribute to elements

244

// <div data-custom-ripple class="my-button">Custom Ripple</div>

245

```

246

247

### Performance Optimization

248

249

```javascript

250

// Optimized ripple effect with requestAnimationFrame

251

class OptimizedRipple {

252

constructor(element) {

253

this.element = element;

254

this.isAnimating = false;

255

this.setupListeners();

256

}

257

258

setupListeners() {

259

this.element.addEventListener('mousedown', (event) => {

260

if (!this.isAnimating) {

261

this.startRipple(event);

262

}

263

});

264

265

this.element.addEventListener('mouseup', () => {

266

this.endRipple();

267

});

268

269

this.element.addEventListener('mouseleave', () => {

270

this.endRipple();

271

});

272

}

273

274

startRipple(event) {

275

this.isAnimating = true;

276

277

const rect = this.element.getBoundingClientRect();

278

const rippleContainer = this.getRippleContainer();

279

280

const ripple = document.createElement('div');

281

ripple.className = 'optimized-ripple';

282

283

const size = Math.max(rect.width, rect.height) * 2;

284

const x = event.clientX - rect.left - size / 2;

285

const y = event.clientY - rect.top - size / 2;

286

287

ripple.style.cssText = `

288

position: absolute;

289

width: ${size}px;

290

height: ${size}px;

291

left: ${x}px;

292

top: ${y}px;

293

background: rgba(255, 255, 255, 0.3);

294

border-radius: 50%;

295

transform: scale(0);

296

opacity: 1;

297

pointer-events: none;

298

`;

299

300

rippleContainer.appendChild(ripple);

301

this.currentRipple = ripple;

302

303

// Use requestAnimationFrame for smooth animation

304

this.animateRipple(ripple, 0);

305

}

306

307

animateRipple(ripple, startTime) {

308

if (!startTime) startTime = performance.now();

309

310

const elapsed = performance.now() - startTime;

311

const duration = 600;

312

const progress = Math.min(elapsed / duration, 1);

313

314

// Easing function

315

const easeOut = 1 - Math.pow(1 - progress, 3);

316

317

ripple.style.transform = `scale(${easeOut})`;

318

319

if (progress < 1 && this.isAnimating) {

320

requestAnimationFrame(() => this.animateRipple(ripple, startTime));

321

}

322

}

323

324

endRipple() {

325

if (this.currentRipple && this.isAnimating) {

326

this.isAnimating = false;

327

328

// Fade out

329

this.currentRipple.style.transition = 'opacity 0.3s';

330

this.currentRipple.style.opacity = '0';

331

332

setTimeout(() => {

333

if (this.currentRipple && this.currentRipple.parentNode) {

334

this.currentRipple.parentNode.removeChild(this.currentRipple);

335

}

336

this.currentRipple = null;

337

}, 300);

338

}

339

}

340

341

getRippleContainer() {

342

let container = this.element.querySelector('.ripple-container');

343

344

if (!container) {

345

container = document.createElement('div');

346

container.className = 'ripple-container';

347

container.style.cssText = `

348

position: absolute;

349

top: 0;

350

left: 0;

351

right: 0;

352

bottom: 0;

353

overflow: hidden;

354

pointer-events: none;

355

`;

356

357

this.element.appendChild(container);

358

359

// Ensure parent has relative positioning

360

if (getComputedStyle(this.element).position === 'static') {

361

this.element.style.position = 'relative';

362

}

363

}

364

365

return container;

366

}

367

}

368

369

// Apply optimized ripple to elements

370

function addOptimizedRipple(element) {

371

if (!element.optimizedRipple) {

372

element.optimizedRipple = new OptimizedRipple(element);

373

}

374

}

375

376

// Usage

377

document.querySelectorAll('[data-optimized-ripple]').forEach(addOptimizedRipple);

378

```

379

380

## Ripple Constants

381

382

```javascript { .api }

383

/**

384

* Material Ripple constants and configuration

385

*/

386

interface RippleConstants {

387

/** Initial scale transform for ripple start */

388

INITIAL_SCALE: 'scale(0.0001, 0.0001)';

389

390

/** Initial size for ripple element */

391

INITIAL_SIZE: '1px';

392

393

/** Initial opacity for ripple start */

394

INITIAL_OPACITY: '0.4';

395

396

/** Final opacity for ripple end */

397

FINAL_OPACITY: '0';

398

399

/** Final scale transform for ripple end */

400

FINAL_SCALE: '';

401

}

402

```

403

404

### Animation Utilities

405

406

```javascript

407

// Utility functions for working with animations

408

class AnimationUtils {

409

static easeOutCubic(t) {

410

return 1 - Math.pow(1 - t, 3);

411

}

412

413

static easeInOutCubic(t) {

414

return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;

415

}

416

417

static animate(element, properties, duration, easing = 'easeOut') {

418

const startTime = performance.now();

419

const startValues = {};

420

421

// Get initial values

422

Object.keys(properties).forEach(prop => {

423

const currentValue = this.getNumericValue(element, prop);

424

startValues[prop] = currentValue;

425

});

426

427

const easingFunction = typeof easing === 'string' ?

428

this[easing] || this.easeOutCubic : easing;

429

430

const step = (currentTime) => {

431

const elapsed = currentTime - startTime;

432

const progress = Math.min(elapsed / duration, 1);

433

const easedProgress = easingFunction(progress);

434

435

Object.keys(properties).forEach(prop => {

436

const startValue = startValues[prop];

437

const endValue = properties[prop];

438

const currentValue = startValue + (endValue - startValue) * easedProgress;

439

440

this.setProperty(element, prop, currentValue);

441

});

442

443

if (progress < 1) {

444

requestAnimationFrame(step);

445

}

446

};

447

448

requestAnimationFrame(step);

449

}

450

451

static getNumericValue(element, property) {

452

const style = getComputedStyle(element);

453

const value = style[property];

454

return parseFloat(value) || 0;

455

}

456

457

static setProperty(element, property, value) {

458

switch (property) {

459

case 'scale':

460

element.style.transform = `scale(${value})`;

461

break;

462

case 'opacity':

463

element.style.opacity = value;

464

break;

465

default:

466

element.style[property] = value + 'px';

467

}

468

}

469

}

470

471

// Usage with ripple effects

472

function createAnimatedRipple(element, event) {

473

const rect = element.getBoundingClientRect();

474

const ripple = document.createElement('div');

475

476

// Setup ripple

477

const size = Math.max(rect.width, rect.height) * 2;

478

const x = event.clientX - rect.left - size / 2;

479

const y = event.clientY - rect.top - size / 2;

480

481

ripple.style.cssText = `

482

position: absolute;

483

width: ${size}px;

484

height: ${size}px;

485

left: ${x}px;

486

top: ${y}px;

487

background: rgba(255, 255, 255, 0.3);

488

border-radius: 50%;

489

transform: scale(0);

490

opacity: 0.4;

491

pointer-events: none;

492

`;

493

494

element.appendChild(ripple);

495

496

// Animate with custom easing

497

AnimationUtils.animate(ripple, { scale: 1 }, 600, AnimationUtils.easeOutCubic);

498

499

// Fade out after delay

500

setTimeout(() => {

501

AnimationUtils.animate(ripple, { opacity: 0 }, 300, (t) => t);

502

503

setTimeout(() => {

504

if (ripple.parentNode) {

505

ripple.parentNode.removeChild(ripple);

506

}

507

}, 300);

508

}, 400);

509

}

510

```

511

512

### Accessibility Considerations

513

514

```javascript

515

// Respect user preferences for reduced motion

516

function respectMotionPreferences() {

517

const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

518

519

if (prefersReducedMotion) {

520

// Disable ripple effects

521

document.querySelectorAll('.mdl-js-ripple-effect').forEach(element => {

522

element.classList.remove('mdl-js-ripple-effect');

523

element.classList.add('mdl-js-ripple-effect--disabled');

524

});

525

526

// Add CSS to disable animations

527

const style = document.createElement('style');

528

style.textContent = `

529

.mdl-js-ripple-effect--disabled .mdl-ripple,

530

.custom-ripple,

531

.optimized-ripple {

532

animation: none !important;

533

transition: none !important;

534

}

535

`;

536

document.head.appendChild(style);

537

}

538

}

539

540

// Initialize on page load

541

document.addEventListener('DOMContentLoaded', respectMotionPreferences);

542

543

// Handle dynamic preference changes

544

window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', respectMotionPreferences);

545

```

546

547

### Ripple Effect Themes

548

549

```javascript

550

// Different ripple themes for various contexts

551

const RippleThemes = {

552

light: {

553

background: 'rgba(0, 0, 0, 0.1)',

554

duration: 600

555

},

556

dark: {

557

background: 'rgba(255, 255, 255, 0.3)',

558

duration: 600

559

},

560

accent: {

561

background: 'rgba(255, 64, 129, 0.3)',

562

duration: 800

563

},

564

success: {

565

background: 'rgba(76, 175, 80, 0.3)',

566

duration: 600

567

},

568

warning: {

569

background: 'rgba(255, 152, 0, 0.3)',

570

duration: 600

571

},

572

error: {

573

background: 'rgba(244, 67, 54, 0.3)',

574

duration: 600

575

}

576

};

577

578

function applyRippleTheme(element, theme) {

579

const themeConfig = RippleThemes[theme];

580

if (!themeConfig) return;

581

582

element.addEventListener('mousedown', (event) => {

583

createThemedRipple(element, event, themeConfig);

584

});

585

}

586

587

function createThemedRipple(element, event, theme) {

588

const rect = element.getBoundingClientRect();

589

const ripple = document.createElement('div');

590

591

const size = Math.max(rect.width, rect.height) * 2;

592

const x = event.clientX - rect.left - size / 2;

593

const y = event.clientY - rect.top - size / 2;

594

595

ripple.style.cssText = `

596

position: absolute;

597

width: ${size}px;

598

height: ${size}px;

599

left: ${x}px;

600

top: ${y}px;

601

background: ${theme.background};

602

border-radius: 50%;

603

transform: scale(0);

604

opacity: 1;

605

pointer-events: none;

606

transition: transform ${theme.duration}ms cubic-bezier(0.4, 0, 0.2, 1),

607

opacity ${theme.duration * 0.5}ms ease-out;

608

`;

609

610

element.appendChild(ripple);

611

612

requestAnimationFrame(() => {

613

ripple.style.transform = 'scale(1)';

614

});

615

616

setTimeout(() => {

617

ripple.style.opacity = '0';

618

setTimeout(() => {

619

if (ripple.parentNode) {

620

ripple.parentNode.removeChild(ripple);

621

}

622

}, theme.duration * 0.5);

623

}, theme.duration * 0.7);

624

}

625

626

// Usage

627

applyRippleTheme(document.querySelector('#success-button'), 'success');

628

applyRippleTheme(document.querySelector('#error-button'), 'error');

629

```